import { gatewayUrl } from "@/lib/api/gateway"; import type { RenderRequest } from "@/lib/render-schemas"; // ── V2 render service types ────────────────────────────────────────────────── type V2RenderStep = | "Queued" | "Preparing" | "TemplateCache" | "JsxGen" | "Music" | "Rendering" | "Validating" | "Repairing" | "Optimisation" | "Video" | "Mixing" | "Final" | "Uploading" | "Done" | "Failed" | "Cancelled"; export type RenderJobStatus = "queued" | "processing" | "completed" | "failed"; export interface RenderJobRow { id: string; project_id: string; status: RenderJobStatus; progress: number; progress_message: string | null; output_url: string | null; error_message: string | null; /** Base64-encoded PNG preview frame pushed by the node agent. Null until first frame arrives. */ preview_b64: string | null; } // ── Helpers ────────────────────────────────────────────────────────────────── /** Map frontend resolution string to V2 render_quality enum (Low/Medium/High/Full/Lossless). */ function mapQuality(resolution: string): string { switch (resolution) { case "360p": return "Low"; case "540p": return "Medium"; case "720p": return "Medium"; case "1080p": return "High"; case "4K": return "Full"; default: return "High"; } } /** Map V2 step → legacy status for frontend compatibility. */ function stepToStatus(step: V2RenderStep): RenderJobStatus { if (step === "Done") return "completed"; if (step === "Failed" || step === "Cancelled") return "failed"; if (step === "Queued") return "queued"; return "processing"; } function authHeaders(token: string): Record { return { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`, }; } // ── Public API ─────────────────────────────────────────────────────────────── /** * Create a render job in the V2 render orchestrator. * `payload.projectId` must be the UUID of a V2 saved project. */ export async function createRenderJob( payload: RenderRequest, token: string ): Promise<{ jobId: string } | { error: string }> { const res = await fetch(gatewayUrl("/v1/renders"), { method: "POST", cache: "no-store", headers: authHeaders(token), body: JSON.stringify({ saved_project_id: payload.projectId, quality: mapQuality(payload.settings.resolution), resolution: payload.settings.resolution, frame_rate: payload.settings.fps, }), }); if (!res.ok) { const err = (await res.json().catch(() => null)) as { message?: string; } | null; return { error: err?.message ?? "Failed to create render job" }; } const data = (await res.json().catch(() => null)) as { id?: string } | null; if (!data?.id) return { error: "No job ID returned from render service" }; return { jobId: data.id }; } /** * Fetch the current render job status from V2. * If the job is completed, also resolves the presigned export download URL. */ export async function getRenderJob( jobId: string, token: string ): Promise { // 1. Poll progress endpoint const progressRes = await fetch(gatewayUrl(`/v1/renders/${jobId}/progress`), { cache: "no-store", headers: authHeaders(token), }); if (!progressRes.ok) return null; const progress = (await progressRes.json().catch(() => null)) as { step?: V2RenderStep; progress?: number; message?: string; preview_b64?: string | null; } | null; if (!progress) return null; const step = progress.step ?? "Queued"; const status = stepToStatus(step); // 2. If done, resolve the export download URL via MinIO presigned link let outputUrl: string | null = null; if (status === "completed") { const jobRes = await fetch(gatewayUrl(`/v1/renders/${jobId}`), { cache: "no-store", headers: authHeaders(token), }); if (jobRes.ok) { const job = (await jobRes.json().catch(() => null)) as { export_id?: string | null; } | null; if (job?.export_id) { const urlRes = await fetch( gatewayUrl(`/v1/exports/${job.export_id}/download-url`), { cache: "no-store", headers: authHeaders(token) } ); if (urlRes.ok) { const urlData = (await urlRes.json().catch(() => null)) as { url?: string; } | null; outputUrl = urlData?.url ?? null; } } } } return { id: jobId, project_id: "", status, progress: progress.progress ?? 0, progress_message: step, output_url: outputUrl, error_message: status === "failed" ? (progress.message ?? "Render failed") : null, preview_b64: progress.preview_b64 ?? null, }; } /** * No-op in V2 — the render orchestrator dispatches jobs to node agents * automatically. Kept for API compatibility. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function triggerRenderWorker(_jobId: string): Promise { // V2 render service handles dispatch internally. }