diff --git a/backend/bun.lock b/backend/bun.lock index 486c555..0604f54 100644 --- a/backend/bun.lock +++ b/backend/bun.lock @@ -12,7 +12,9 @@ "elysia": "latest", }, "devDependencies": { + "@types/bun": "^1.3.14", "bun-types": "latest", + "typescript": "^6.0.3", }, }, }, @@ -113,6 +115,8 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -153,6 +157,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], diff --git a/backend/package.json b/backend/package.json index 246f987..d644991 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,8 @@ "@aws-sdk/s3-request-presigner": "^3.700.0" }, "devDependencies": { - "bun-types": "latest" + "@types/bun": "^1.3.14", + "bun-types": "latest", + "typescript": "^6.0.3" } } diff --git a/backend/src/routes/videos.ts b/backend/src/routes/videos.ts index f55b632..b8e5d51 100644 --- a/backend/src/routes/videos.ts +++ b/backend/src/routes/videos.ts @@ -9,7 +9,6 @@ import { getVideoByMapAndPlayer, updateVideoPB, updateTranscodedKey, - updateTranscodeStatus, } from "../services/db"; import { uploadObject, @@ -21,7 +20,6 @@ import { completeMultipartUpload, abortMultipartUpload, getVideoStream, - transcodeVideo, } from "../services/rustfs"; import { parseMtvFile } from "../services/mtv-parser"; import { getMapInfo } from "../services/momentum-api"; @@ -336,26 +334,6 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" }) await completeMultipartUpload(video.videoKey, uploadId, s3Parts); - // Auto-transcode in background - if ( - !video.transcodedKey && - video.transcodeStatus !== "pending" && - video.transcodeStatus !== "processing" - ) { - updateTranscodeStatus(id, "pending"); - const videoKey = video.videoKey; - transcodeVideo(videoKey) - .then((transcodedKey) => { - updateTranscodedKey(id, transcodedKey); - updateTranscodeStatus(id, "done"); - console.log(`[transcode] Done: ${id} -> ${transcodedKey}`); - }) - .catch((err) => { - console.error(`[transcode] Failed: ${id}`, err); - updateTranscodeStatus(id, "failed"); - }); - } - return { ...video, videoUrl: getStreamUrl(video.id), @@ -366,50 +344,6 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" }) }; }) - .post("/:id/transcode", async ({ params: { id } }) => { - const video = getVideoById(id); - if (!video) return status(404, { error: "Not found" }); - - if ( - video.transcodeStatus === "processing" || - video.transcodeStatus === "pending" - ) { - return status(409, { - error: "Transcode already in progress", - transcodeStatus: video.transcodeStatus, - }); - } - - if (video.transcodedKey) { - return { - ...video, - videoUrl: getStreamUrl(video.id), - mtvUrl: getObjectUrl(video.mtvKey), - thumbnailUrl: video.thumbnailKey - ? getObjectUrl(video.thumbnailKey) - : undefined, - }; - } - - // Mark as pending and return immediately - updateTranscodeStatus(id, "pending"); - - // Run transcode in background - const videoKey = video.videoKey; - transcodeVideo(videoKey) - .then((transcodedKey) => { - updateTranscodedKey(id, transcodedKey); - updateTranscodeStatus(id, "done"); - console.log(`[transcode] Done: ${id} -> ${transcodedKey}`); - }) - .catch((err) => { - console.error(`[transcode] Failed: ${id}`, err); - updateTranscodeStatus(id, "failed"); - }); - - return { id, transcodeStatus: "pending" }; - }) - .get("/:id/stream", async ({ params: { id }, request, set }) => { const video = getVideoById(id); if (!video) return status(404, { error: "Not found" }); diff --git a/backend/src/services/db.ts b/backend/src/services/db.ts index 1ceb314..f6f2b82 100644 --- a/backend/src/services/db.ts +++ b/backend/src/services/db.ts @@ -60,11 +60,6 @@ export function initDb(): void { if (!columns.some((col) => col.name === "transcoded_key")) { db.exec("ALTER TABLE videos ADD COLUMN transcoded_key TEXT"); } - if (!columns.some((col) => col.name === "transcode_status")) { - db.exec( - "ALTER TABLE videos ADD COLUMN transcode_status TEXT DEFAULT NULL", - ); - } } function getDb(): Database { @@ -158,21 +153,9 @@ function rowToEntry(row: any): VideoEntry { ? JSON.parse(row.previous_pbs) : undefined, transcodedKey: row.transcoded_key ?? undefined, - transcodeStatus: row.transcode_status ?? undefined, }; } -export function updateTranscodeStatus( - id: string, - status: "pending" | "processing" | "done" | "failed" | null, -): boolean { - const d = getDb(); - const result = d - .prepare("UPDATE videos SET transcode_status = ? WHERE id = ?") - .run(status, id); - return result.changes > 0; -} - export function updateThumbnailKey( id: string, thumbnailKey: string | null, diff --git a/backend/src/services/rustfs.ts b/backend/src/services/rustfs.ts index b8710d3..1f54aec 100644 --- a/backend/src/services/rustfs.ts +++ b/backend/src/services/rustfs.ts @@ -9,10 +9,6 @@ import { GetObjectCommand, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { tmpdir } from "os"; -import { join } from "path"; -import { unlink } from "fs/promises"; -import { createWriteStream, createReadStream } from "fs"; const RUSTFS_ENDPOINT = process.env.RUSTFS_ENDPOINT || "http://localhost:9000"; const RUSTFS_ACCESS_KEY = process.env.RUSTFS_ACCESS_KEY || "minioadmin"; @@ -221,145 +217,3 @@ export async function getVideoStream( export function getStreamUrl(videoId: string): string { return `/api/videos/${videoId}/stream`; } - -export async function transcodeVideo(originalKey: string): Promise { - const s3 = getRustFsClient(); - - const inputPath = join(tmpdir(), `transcode-input-${Date.now()}`); - const outputPath = join(tmpdir(), `transcode-output-${Date.now()}.mp4`); - - try { - // Stream download from S3 to temp file (avoids loading whole file into RAM) - console.log(`[transcode] Downloading ${originalKey} from S3...`); - const getCmd = new GetObjectCommand({ - Bucket: RUSTFS_BUCKET, - Key: originalKey, - }); - const response = await s3.send(getCmd); - if (!response.Body) { - throw new Error("Empty response body from RustFS"); - } - - const sdkStream = response.Body as any; - const webStream: ReadableStream = - sdkStream.transformToWebStream - ? sdkStream.transformToWebStream() - : sdkStream; - - const fileWriter = createWriteStream(inputPath); - const reader = webStream.getReader(); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (!fileWriter.write(value)) { - await new Promise((resolve) => - fileWriter.once("drain", resolve), - ); - } - } - } finally { - fileWriter.end(); - reader.releaseLock(); - await new Promise((resolve) => - fileWriter.on("finish", resolve), - ); - } - - // Run ffmpeg to transcode - console.log(`[transcode] Running ffmpeg on ${inputPath}...`); - const proc = Bun.spawn( - [ - "ffmpeg", - "-i", - inputPath, - "-c:v", - "libx264", - "-preset", - "slow", - "-crf", - "18", - "-c:a", - "aac", - "-b:a", - "192k", - "-movflags", - "+faststart", - "-y", - outputPath, - ], - { - stdout: "pipe", - stderr: "pipe", - }, - ); - - const exitCode = await proc.exited; - if (exitCode !== 0) { - const stderr = await new Response(proc.stderr).text(); - throw new Error(`ffmpeg exited with code ${exitCode}: ${stderr}`); - } - - // Compute the transcoded key (replace extension with .mp4) - const lastDot = originalKey.lastIndexOf("."); - const transcodedKey = - lastDot !== -1 - ? originalKey.substring(0, lastDot) + ".mp4" - : originalKey + ".mp4"; - - // Upload transcoded file to S3 using multipart for large files - console.log(`[transcode] Uploading ${transcodedKey} to S3...`); - const outputFile = Bun.file(outputPath); - const fileSize = outputFile.size; - const CHUNK_SIZE = 80 * 1024 * 1024; // 80MB chunks - - if (fileSize < CHUNK_SIZE) { - // Small enough for single upload - const buffer = Buffer.from(await outputFile.arrayBuffer()); - await uploadObject(transcodedKey, buffer, "video/mp4"); - } else { - // Multipart upload for large files - const { uploadId } = await createMultipartUpload( - transcodedKey, - "video/mp4", - ); - const numParts = Math.ceil(fileSize / CHUNK_SIZE); - const parts: { PartNumber: number; ETag: string }[] = []; - - for (let i = 0; i < numParts; i++) { - const start = i * CHUNK_SIZE; - const end = Math.min(start + CHUNK_SIZE, fileSize); - const chunk = Buffer.from( - await outputFile.slice(start, end).arrayBuffer(), - ); - - const partNumber = i + 1; - const uploadCmd = new UploadPartCommand({ - Bucket: RUSTFS_BUCKET, - Key: transcodedKey, - UploadId: uploadId, - PartNumber: partNumber, - Body: chunk, - }); - const result = await s3.send(uploadCmd); - parts.push({ - PartNumber: partNumber, - ETag: result.ETag!, - }); - } - - await completeMultipartUpload(transcodedKey, uploadId, parts); - } - - return transcodedKey; - } finally { - // Clean up temp files - try { - await unlink(inputPath); - } catch {} - try { - await unlink(outputPath); - } catch {} - } -} diff --git a/backend/src/types/video.ts b/backend/src/types/video.ts index e85ac51..778e28a 100644 --- a/backend/src/types/video.ts +++ b/backend/src/types/video.ts @@ -57,7 +57,6 @@ export interface VideoEntry { createdAt: string; previousPbs?: PreviousPB[]; transcodedKey?: string; - transcodeStatus?: "pending" | "processing" | "done" | "failed" | null; } export interface VideoListItem { diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx index c9dd398..9e36edb 100644 --- a/frontend/src/components/VideoPlayer.tsx +++ b/frontend/src/components/VideoPlayer.tsx @@ -1,84 +1,45 @@ import type { VideoDetail } from "../types/video"; import { formatRunTime } from "../api/client"; -function TranscodeBanner({ status }: { status: string }) { - if (status === "done" || !status) return null; - - const isPending = status === "pending" || status === "processing"; - - return ( -
- {isPending ? ( -
-
- - Video is being prepared for playback. This may take a - few minutes depending on file size. - -
- ) : ( - - Video processing failed. The original format may not be - supported by your browser. - - )} -
- ); -} - export default function VideoPlayer({ video }: { video: VideoDetail }) { return ( -
- {(video.transcodeStatus === "pending" || - video.transcodeStatus === "processing" || - video.transcodeStatus === "failed") && ( - - )} +
+ -
- - -
- - - - - .mtv replay - + + + .mtv replay + - - {formatRunTime(video.runTime)} - -
+ + {formatRunTime(video.runTime)} +
); diff --git a/frontend/src/types/video.ts b/frontend/src/types/video.ts index 81c785c..6df3c14 100644 --- a/frontend/src/types/video.ts +++ b/frontend/src/types/video.ts @@ -35,5 +35,4 @@ export interface VideoDetail { jsonStats?: string; createdAt: string; previousPbs?: PreviousPB[]; - transcodeStatus?: "pending" | "processing" | "done" | "failed" | null; }