mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
feat(runtime): render media treatments deterministically
This commit is contained in:
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user