diff --git a/packages/engine/src/services/frameCapture.test.ts b/packages/engine/src/services/frameCapture.test.ts index e9f6d28b10..9f9fe880cc 100644 --- a/packages/engine/src/services/frameCapture.test.ts +++ b/packages/engine/src/services/frameCapture.test.ts @@ -5,6 +5,7 @@ import type { CaptureSession } from "./frameCapture.js"; import type { SubTimelineWaitMemo } from "../types.js"; import { buildZeroDurationDiagnostic, + captureDeVerificationFrames, pollHfReady, captureFramesBatchPipelined, captureFrameToBufferPipelined, @@ -726,3 +727,38 @@ describe("HF_READY_DIAGNOSTIC_EXPR (evaluated as real JS, not via a fake fixture expect(result.rejectedBuildReadyKeys).toEqual([]); }); }); + +describe("captureDeVerificationFrames", () => { + it("waits for the colour-grading LUTs after injecting video frames, before every truth screenshot", async () => { + const log: string[] = []; + const cdp = { + send: vi.fn(async () => { + log.push("screenshot"); + return { data: "" }; + }), + }; + const page = { + createCDPSession: async () => cdp, + evaluate: vi.fn(async (fn: unknown) => { + const src = String(fn); + if (src.includes("waitForActiveLuts")) log.push("luts"); + else if (src.includes("data-start")) return []; + return undefined; + }), + }; + const session = { + options: { fps: 30, width: 8, height: 8, format: "jpeg", compositionDurationSeconds: 10 }, + onBeforeCapture: async () => { + log.push("inject"); + }, + } as unknown as CaptureSession; + + await captureDeVerificationFrames(session, page as never, () => {}); + + expect(session.deVerifyFrames?.size).toBeGreaterThan(0); + log.forEach((entry, i) => { + if (entry === "inject") expect(log.slice(i + 1, i + 3)).toEqual(["luts", "screenshot"]); + }); + expect(log).toContain("inject"); + }); +}); diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index 9b367979f1..48d7859692 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -3269,14 +3269,7 @@ async function prepareFrameForCapture( session.captureStep = "waiting for page seek handlers"; await waitForPendingSeekCompletion(page); session.captureStep = "loading color-grading LUTs"; - await page.evaluate(async () => { - const runtime = ( - window as Window & { - __hf?: { colorGrading?: { waitForActiveLuts?: () => Promise } }; - } - ).__hf?.colorGrading; - await runtime?.waitForActiveLuts?.(); - }); + await waitForColorGradingLuts(page); const beforeCaptureMs = Date.now() - beforeCaptureStart; // Page-side compositing three-phase protocol: @@ -3346,6 +3339,18 @@ export function computeAuthoredClipBoundaryFrames( return frames; } +/** A graded frame paints ungraded until the LUT has loaded, so wait before every screenshot. */ +async function waitForColorGradingLuts(page: Page): Promise { + await page.evaluate(async () => { + const runtime = ( + window as Window & { + __hf?: { colorGrading?: { waitForActiveLuts?: () => Promise } }; + } + ).__hf?.colorGrading; + await runtime?.waitForActiveLuts?.(); + }); +} + async function computeClipBoundaryFrames(page: Page, fps: number): Promise> { const schedule = await page.evaluate(() => Array.from(document.querySelectorAll("[data-start]")).map((el) => ({ @@ -5408,7 +5413,7 @@ export async function seekDeVerifyTruth( ); } -async function captureDeVerificationFrames( +export async function captureDeVerificationFrames( session: CaptureSession, page: Page, logInitPhase: (phase: string) => void, @@ -5493,6 +5498,7 @@ async function captureDeVerificationFrames( // it,