Auto-transcode on upload, show transcode status to user

This commit is contained in:
CallMeVerity
2026-06-03 04:38:51 +01:00
parent e50aee1e62
commit 97da0d435a
6 changed files with 144 additions and 47 deletions
+47 -11
View File
@@ -9,6 +9,7 @@ import {
getVideoByMapAndPlayer,
updateVideoPB,
updateTranscodedKey,
updateTranscodeStatus,
} from "../services/db";
import {
uploadObject,
@@ -335,6 +336,26 @@ 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),
@@ -349,6 +370,16 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
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,
@@ -360,18 +391,23 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
};
}
const transcodedKey = await transcodeVideo(video.videoKey);
updateTranscodedKey(id, transcodedKey);
// Mark as pending and return immediately
updateTranscodeStatus(id, "pending");
return {
...video,
transcodedKey,
videoUrl: getStreamUrl(video.id),
mtvUrl: getObjectUrl(video.mtvKey),
thumbnailUrl: video.thumbnailKey
? getObjectUrl(video.thumbnailKey)
: undefined,
};
// 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 }) => {
+18 -1
View File
@@ -60,6 +60,11 @@ 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 {
@@ -145,7 +150,6 @@ function rowToEntry(row: any): VideoEntry {
videoKey: row.video_key,
mtvKey: row.mtv_key,
thumbnailKey: row.thumbnail_key ?? undefined,
transcodedKey: row.transcoded_key ?? undefined,
tier: row.tier ?? undefined,
mapId: row.map_id ?? undefined,
jsonStats: row.json_stats ?? undefined,
@@ -153,9 +157,22 @@ function rowToEntry(row: any): VideoEntry {
previousPbs: row.previous_pbs
? 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,
+3
View File
@@ -230,6 +230,7 @@ export async function transcodeVideo(originalKey: string): Promise<string> {
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,
@@ -267,6 +268,7 @@ export async function transcodeVideo(originalKey: string): Promise<string> {
}
// Run ffmpeg to transcode
console.log(`[transcode] Running ffmpeg on ${inputPath}...`);
const proc = Bun.spawn(
[
"ffmpeg",
@@ -307,6 +309,7 @@ export async function transcodeVideo(originalKey: string): Promise<string> {
: 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
+2 -1
View File
@@ -51,12 +51,13 @@ export interface VideoEntry {
videoKey: string;
mtvKey: string;
thumbnailKey?: string;
transcodedKey?: string;
tier?: number | null;
mapId?: number | null;
jsonStats?: string;
createdAt: string;
previousPbs?: PreviousPB[];
transcodedKey?: string;
transcodeStatus?: "pending" | "processing" | "done" | "failed" | null;
}
export interface VideoListItem {