154 lines
3.6 KiB
TypeScript
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);
|
|
});
|