Files
remotion_service/server/index.ts
T
2026-05-14 02:23:02 +03:00

154 lines
3.6 KiB
TypeScript

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);
});