d7a74daa96
- 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>
175 lines
5.3 KiB
TypeScript
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.
|
|
}
|