import Elysia, { t } from "elysia"; import { Document } from "@/srv/types/DocumentSchema"; import { CaptionStyleSchema } from "@/srv/types/CaptionStyleSchema"; import { S3Service } from "@/srv/services/s3"; import { renderCaptionedVideo } from "@/srv/services/render_video"; import { cancelRender, enqueueRender, renderQueue, startRenderWorker, } from "@/srv/services/render_queue"; import { serverConfig } from "@/srv/config"; // Start the BullMQ worker const worker = startRenderWorker(); console.log( `Render worker started (concurrency: ${serverConfig.MAX_CONCURRENT_RENDERS})`, ); const app = new Elysia({ prefix: "/api" }); app.post( "/render", async ({ body, set }) => { const renderId = body.renderId || crypto.randomUUID(); // If callbackUrl is provided, enqueue async render if (body.callbackUrl) { await enqueueRender({ renderId, folder: body.folder, videoSrc: body.videoSrc, transcription: body.transcription, styleConfig: body.styleConfig, callbackUrl: body.callbackUrl, }); set.status = 202; return { renderId, status: "queued" }; } // Sync fallback (no callbackUrl) — render immediately and return result const s3Service = new S3Service("captioned", body.folder); const filename = s3Service.getFileName(body.videoSrc); const videoUrl = await s3Service.getFileURL(body.videoSrc); const res = await renderCaptionedVideo( body.transcription, videoUrl, filename, body.styleConfig, ); try { const s3OutPath = await s3Service.uploadFile(res.output, filename); return { output: s3OutPath }; } finally { await Bun.file(res.output) .delete() .catch(() => {}); } }, { body: t.Object({ folder: t.Optional(t.String()), renderId: t.Optional(t.String()), videoSrc: t.String(), transcription: Document, styleConfig: t.Optional(CaptionStyleSchema), callbackUrl: t.Optional(t.String()), }), }, ); app.get( "/render/:renderId", async ({ params }) => { const job = await renderQueue.getJob(params.renderId); if (!job) { return { status: "not_found", renderId: params.renderId }; } const state = await job.getState(); const progress = typeof job.progress === "number" ? job.progress : 0; if (state === "completed") { return { status: "done", renderId: params.renderId, progress_pct: 100, output_path: job.returnvalue?.outputPath, callback_delivered: job.returnvalue?.callbackDelivered ?? false, }; } if (state === "failed") { return { status: "failed", renderId: params.renderId, error: job.failedReason, }; } return { status: state, // "waiting", "active", "delayed" renderId: params.renderId, progress_pct: progress, }; }, { params: t.Object({ renderId: t.String(), }), }, ); app.delete( "/render/:renderId", async ({ params, set }) => { const result = await cancelRender(params.renderId); if (result.status === "not_found") { set.status = 404; return result; } return result; }, { params: t.Object({ renderId: t.String(), }), }, ); app.get("/render", async () => "Hello"); app.get("/health", async () => { return { status: "ok" }; }); app.listen(serverConfig.PORT, () => { console.log( `Remotion service listening on ${serverConfig.HOST}:${serverConfig.PORT}`, ); }); // Graceful shutdown process.on("SIGTERM", async () => { console.log("Shutting down render worker..."); await worker.close(); process.exit(0); });