Files
flatrender/src/lib/render-jobs.ts
T
soroush.asadi d7a74daa96 feat(render): 5 quality tiers (360p–4K) + ETA on render page; 24h session
- Render page: resolution picker now 360p/540p/720p/1080p/4K; live ETA
  ('تقریباً … باقی مانده') computed from progress rate; preview+progress bar already wired.
- render-schemas: resolution enum + RESOLUTION_DIMENSIONS add 360p/540p.
- render-jobs.mapQuality: 5-tier → render_quality (Low/Medium/High/Full).
- Session: Jwt__AccessTokenMinutes=1440 (24h) via compose so logins persist
  (refresh middleware + 30d refresh token back it up).

(Real per-tier output height still pending: render-svc r_height is hardcoded 1080 →
node ffmpeg scale — next.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:04:32 +03:30

175 lines
5.3 KiB
TypeScript

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<string, string> {
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<RenderJobRow | null> {
// 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<void> {
// V2 render service handles dispatch internally.
}