feat(runtime): render media treatments deterministically

This commit is contained in:
ukimsanov
2026-07-24 11:17:22 -07:00
parent 7778c093b6
commit 944640c328
13 changed files with 2994 additions and 309 deletions
@@ -2356,6 +2356,14 @@ async function prepareFrameForCapture(
if (session.onBeforeCapture) {
await session.onBeforeCapture(page, quantizedTime);
}
await page.evaluate(async () => {
const runtime = (
window as Window & {
__hf?: { colorGrading?: { waitForActiveLuts?: () => Promise<number> } };
}
).__hf?.colorGrading;
await runtime?.waitForActiveLuts?.();
});
const beforeCaptureMs = Date.now() - beforeCaptureStart;
// Page-side compositing three-phase protocol:
@@ -181,9 +181,12 @@ describe("injectVideoFramesBatch replacement layout", () => {
'<html><body><div id="root"><video id="clip" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"></video></div></body></html>',
);
const events: string[] = [];
Object.defineProperty(window.HTMLImageElement.prototype, "decode", {
configurable: true,
value: () => Promise.resolve(),
value: async () => {
events.push("decode");
},
});
const video = document.getElementById("clip") as HTMLVideoElement;
@@ -232,6 +235,10 @@ describe("injectVideoFramesBatch replacement layout", () => {
const previousDocument = globals.document;
globals.window = window;
globals.document = document;
const redraw = vi.fn(() => events.push("redraw"));
(window as unknown as { __hf: { colorGrading: { redraw: () => void } } }).__hf = {
colorGrading: { redraw },
};
try {
const page = {
evaluate: async (
@@ -266,6 +273,8 @@ describe("injectVideoFramesBatch replacement layout", () => {
expect(img?.style.right).toBe("auto");
expect(img?.style.bottom).toBe("auto");
expect(img?.style.inset).toBe("auto");
expect(redraw).toHaveBeenCalledOnce();
expect(events).toEqual(["decode", "redraw"]);
});
});
@@ -569,6 +578,10 @@ describe("video-frame injection respects ancestor visibility", () => {
seededImg.classList.add("__render_frame__");
seededImg.style.opacity = "0";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
const setSourceVisibility = vi.fn();
(setup.window as unknown as { __hf: unknown }).__hf = {
colorGrading: { setSourceVisibility },
};
try {
await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
@@ -578,6 +591,7 @@ describe("video-frame injection respects ancestor visibility", () => {
expect(seededImg.style.opacity).toBe("1");
expect(seededImg.style.visibility).toBe("visible");
expect(setSourceVisibility).toHaveBeenCalledWith(setup.video, true);
});
it("syncVideoFrameVisibility shows the replacement <img> when a plain [data-start] host is visibility:hidden", async () => {
@@ -643,6 +657,10 @@ describe("video-frame injection respects ancestor visibility", () => {
seededImg.style.visibility = "visible";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
const setPropertySpy = vi.spyOn(seededImg.style, "setProperty");
const setSourceVisibility = vi.fn();
(setup.window as unknown as { __hf: unknown }).__hf = {
colorGrading: { setSourceVisibility },
};
try {
await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
@@ -652,6 +670,7 @@ describe("video-frame injection respects ancestor visibility", () => {
expect(seededImg.style.visibility).toBe("hidden");
expect(setPropertySpy).toHaveBeenCalledWith("visibility", "hidden", "important");
expect(setSourceVisibility).toHaveBeenCalledWith(setup.video, false);
});
it("applyDomLayerMask does not revive hidden idless timed descendants of a shown layer", async () => {
@@ -8,13 +8,13 @@
// fallow-ignore-file code-duplication
import { type Page } from "puppeteer-core";
import { type CaptureOptions } from "../types.js";
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
import {
HF_COLOR_GRADING_CANVAS_ID_PREFIX,
MEDIA_VISUAL_STYLE_PROPERTIES,
} from "@hyperframes/core";
export const cdpSessionCache = new WeakMap<Page, import("puppeteer-core").CDPSession>();
const COLOR_GRADING_SOURCE_HIDDEN_ATTR = "data-hf-color-grading-source-hidden";
export async function getCdpSession(page: Page): Promise<import("puppeteer-core").CDPSession> {
let client = cdpSessionCache.get(page);
@@ -789,6 +789,11 @@ export async function injectVideoFramesBatch(
if (pendingDecodes.length > 0) {
await Promise.all(pendingDecodes);
}
if (injectedIds.length > 0) {
const redraw = (window as Window & { __hf?: { colorGrading?: { redraw?: () => void } } })
.__hf?.colorGrading?.redraw;
redraw?.();
}
return injectedIds;
},
updates,
@@ -825,6 +830,13 @@ export async function syncVideoFrameVisibility(
return false;
};
const active = new Set(ids);
const setColorGradingVisibility = (
window as Window & {
__hf?: {
colorGrading?: { setSourceVisibility?: (target: Element, visible: boolean) => boolean };
};
}
).__hf?.colorGrading?.setSourceVisibility;
const videos = Array.from(
document.querySelectorAll("video[data-start]"),
) as HTMLVideoElement[];
@@ -832,7 +844,8 @@ export async function syncVideoFrameVisibility(
const img = video.nextElementSibling as HTMLElement | null;
const hasImg = img && img.classList.contains("__render_frame__");
const ancestorHidden = isVisualAncestorHidden(video);
if (active.has(video.id) && !ancestorHidden) {
const visible = active.has(video.id) && !ancestorHidden;
if (visible) {
// Active video: show injected <img>, hide native <video>.
// Do NOT clobber inline opacity here — GSAP-controlled opacity must
// survive until injectVideoFramesBatch reads it via getComputedStyle.
@@ -859,6 +872,7 @@ export async function syncVideoFrameVisibility(
img.style.setProperty("visibility", "hidden", "important");
}
}
setColorGradingVisibility?.(video, visible);
}
},
activeVideoIds,
@@ -148,25 +148,6 @@ function createFrameSourceCache(
export const __testing = { createFrameSourceCache };
async function redrawRuntimeColorGrading(page: Page): Promise<void> {
await page.evaluate(() => {
const hf = (
window as Window & {
__hf?: {
colorGrading?: { redraw?: () => unknown };
};
}
).__hf;
const redraw = hf?.colorGrading?.redraw;
if (typeof redraw !== "function") return;
try {
redraw();
} catch {
// Optional page-side shader layer.
}
});
}
/**
* Creates a BeforeCaptureHook that injects pre-extracted video frames
* into the page, replacing native <video> elements with frame images.
@@ -248,7 +229,6 @@ export function createVideoFrameInjector(
}, time);
}
}
await redrawRuntimeColorGrading(page);
};
}