Auto-transcode on upload, show transcode status to user
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
getVideoByMapAndPlayer,
|
||||
updateVideoPB,
|
||||
updateTranscodedKey,
|
||||
updateTranscodeStatus,
|
||||
} from "../services/db";
|
||||
import {
|
||||
uploadObject,
|
||||
@@ -335,6 +336,25 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
|
||||
|
||||
await completeMultipartUpload(video.videoKey, uploadId, s3Parts);
|
||||
|
||||
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 +369,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 +390,21 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
|
||||
};
|
||||
}
|
||||
|
||||
const transcodedKey = await transcodeVideo(video.videoKey);
|
||||
updateTranscodedKey(id, transcodedKey);
|
||||
updateTranscodeStatus(id, "pending");
|
||||
|
||||
return {
|
||||
...video,
|
||||
transcodedKey,
|
||||
videoUrl: getStreamUrl(video.id),
|
||||
mtvUrl: getObjectUrl(video.mtvKey),
|
||||
thumbnailUrl: video.thumbnailKey
|
||||
? getObjectUrl(video.thumbnailKey)
|
||||
: undefined,
|
||||
};
|
||||
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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -224,6 +224,7 @@ export async function transcodeVideo(originalKey: string): Promise<string> {
|
||||
const outputPath = join(tmpdir(), `transcode-output-${Date.now()}.mp4`);
|
||||
|
||||
try {
|
||||
console.log(`[transcode] Downloading ${originalKey} from S3...`);
|
||||
const getCmd = new GetObjectCommand({
|
||||
Bucket: RUSTFS_BUCKET,
|
||||
Key: originalKey,
|
||||
@@ -260,6 +261,7 @@ export async function transcodeVideo(originalKey: string): Promise<string> {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[transcode] Running ffmpeg on ${inputPath}...`);
|
||||
const proc = Bun.spawn(
|
||||
[
|
||||
"ffmpeg",
|
||||
@@ -298,6 +300,7 @@ export async function transcodeVideo(originalKey: string): Promise<string> {
|
||||
? originalKey.substring(0, lastDot) + ".mp4"
|
||||
: originalKey + ".mp4";
|
||||
|
||||
console.log(`[transcode] Uploading ${transcodedKey} to S3...`);
|
||||
const outputFile = Bun.file(outputPath);
|
||||
const fileSize = outputFile.size;
|
||||
const CHUNK_SIZE = 80 * 1024 * 1024;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user