import { deckStill, scheduleForPreview, stillTimeOf } from "@/lib/report/onscreen.mjs"; import { resolveReport } from "@/lib/report/serve.mjs"; import { deckOn, validateChrome } from "umtool-report-to-video/deck"; export const dynamic = "force-dynamic"; // A TRUE still of the deck: the composition at one moment, screenshotted by // the same browser engine the render uses, as a PNG of the deck region // (1920 × deck.height, transparent outside the panel). // // The live preview is an iframe in the page's own browser, which is close to // the render and not the same thing -- fonts, text fitting and the QR are // where they differ. This is the answer to "is that what the video will show". // // ?project&variant& one of: // clip= the middle of that entry's segment // at= a moment in the cut's clock // // Drawn from the same schedule the preview uses (the build's when it matches // the cut, else an estimate), saved text only -- a still is of what is saved. export async function GET(request: Request) { const url = new URL(request.url); const r = await resolveReport(url.searchParams.get("project") ?? "", url.searchParams.get("variant")); if ("error" in r) return new Response(r.error, { status: r.status }); const { variantManifest, schedule } = await scheduleForPreview(r.project, r.manifest, r.variant); const render = (variantManifest.render ?? {}) as Record; if (!deckOn(render)) return new Response("the deck is off for this manifest", { status: 409 }); const { chrome, ...rest } = render; const errors = validateChrome(chrome, rest); if (errors.length) return new Response(`render.chrome: ${errors.join("; ")}`, { status: 400 }); const clip = url.searchParams.get("clip"); const atRaw = url.searchParams.get("at"); let t: number | null; if (clip) { t = stillTimeOf(schedule, clip); if (t === null) return new Response(`no entry ${clip} in the ${r.variant} cut`, { status: 404 }); } else if (atRaw !== null && atRaw !== "") { t = Number(atRaw); if (!Number.isFinite(t) || t < 0 || t > schedule.total) { return new Response(`at must be a number of seconds from 0 to ${schedule.total}`, { status: 400 }); } } else { return new Response("name a clip= or an at=", { status: 400 }); } let png: Buffer; try { png = await deckStill(r.project, r.variant, schedule, t); } catch (e) { return new Response(e instanceof Error ? e.message : String(e), { status: 500 }); } return new Response(new Uint8Array(png), { headers: { "content-type": "image/png", "content-length": String(png.length), "cache-control": "no-store", "x-still-at": String(t), "x-schedule-estimated": String(!!schedule.estimated), }, }); }