mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(producer): seek once per step when discovering video visibility (#3233)
Seek the GSAP timeline once per timestep and sample every auto-start video, instead of re-walking per media element. Keeps probe cost proportional to duration, not video count.
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
detectThreeDTransformUsage,
|
||||
discoverMediaFromBrowser,
|
||||
discoverAudioVolumeAutomationFromTimeline,
|
||||
discoverVideoVisibilityFromTimeline,
|
||||
inlineExternalScripts,
|
||||
localizeRemoteMediaSources,
|
||||
localizeRemoteImageSources,
|
||||
@@ -2200,6 +2201,75 @@ describe("resolveCompositionDurations strict literal timing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("discoverVideoVisibilityFromTimeline", () => {
|
||||
it("returns scene visibility windows for auto-start videos", async () => {
|
||||
const duration = 2;
|
||||
const sampleStep = 0.1;
|
||||
const windows = [
|
||||
{ id: "v0", start: 0.2, end: 0.7 },
|
||||
{ id: "v1", start: 0.5, end: 1.2 },
|
||||
{ id: "v2", start: 1.0, end: 1.8 },
|
||||
{ id: "v3", start: 0.0, end: 0.4 },
|
||||
];
|
||||
|
||||
type SceneEl = { opacityAt: (t: number) => number };
|
||||
const videos = windows.map((win) => {
|
||||
const sceneEl: SceneEl = {
|
||||
opacityAt: (t) => (t >= win.start && t <= win.end ? 1 : 0),
|
||||
};
|
||||
return {
|
||||
id: win.id,
|
||||
closest: (selector: string) => (selector === ".scene" ? sceneEl : null),
|
||||
};
|
||||
});
|
||||
|
||||
let currentTime = 0;
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
|
||||
globalThis.window = {
|
||||
__timelines: {
|
||||
root: {
|
||||
totalTime: (time: number) => {
|
||||
currentTime = time;
|
||||
},
|
||||
},
|
||||
},
|
||||
getComputedStyle: (el: SceneEl) => ({
|
||||
opacity: String(el.opacityAt(currentTime)),
|
||||
}),
|
||||
} as typeof globalThis.window;
|
||||
globalThis.document = {
|
||||
querySelectorAll: (selector: string) =>
|
||||
selector === "video[data-hf-auto-start]" ? videos : [],
|
||||
querySelector: (selector: string) =>
|
||||
selector === "[data-composition-id]"
|
||||
? { getAttribute: (name: string) => (name === "data-composition-id" ? "root" : null) }
|
||||
: null,
|
||||
} as typeof globalThis.document;
|
||||
|
||||
try {
|
||||
const page = {
|
||||
evaluate: async (fn: (arg: number) => unknown, arg: number) => fn(arg),
|
||||
};
|
||||
|
||||
const result = await discoverVideoVisibilityFromTimeline(page as never, duration);
|
||||
|
||||
expect(result).toHaveLength(windows.length);
|
||||
for (const win of windows) {
|
||||
const found = result.find((entry) => entry.videoId === win.id);
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.visibleStart).toBeGreaterThanOrEqual(win.start - sampleStep);
|
||||
expect(found!.visibleStart).toBeLessThanOrEqual(win.start + sampleStep);
|
||||
expect(found!.visibleEnd).toBeGreaterThanOrEqual(win.end - sampleStep);
|
||||
expect(found!.visibleEnd).toBeLessThanOrEqual(win.end + sampleStep);
|
||||
}
|
||||
} finally {
|
||||
globalThis.window = previousWindow;
|
||||
globalThis.document = previousDocument;
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("sub-composition variable injection (render path, #2064)", () => {
|
||||
function writeSubCompVarProject(hostVars: string): string {
|
||||
const projectDir = mkdtempSync(join(tmpdir(), "hf-subvar-"));
|
||||
|
||||
@@ -2346,24 +2346,40 @@ export async function discoverVideoVisibilityFromTimeline(
|
||||
const SAMPLE_STEP = 0.1;
|
||||
const BINARY_PRECISION = 1 / 60;
|
||||
|
||||
// Seek once per timestep and sample every video — seeking dominates and is
|
||||
// independent of which element we read.
|
||||
const entries: {
|
||||
id: string;
|
||||
sceneEl: Element;
|
||||
firstVisible: number | null;
|
||||
lastVisible: number | null;
|
||||
}[] = [];
|
||||
for (const videoEl of videos) {
|
||||
const id = videoEl.id;
|
||||
if (!id) continue;
|
||||
entries.push({
|
||||
id,
|
||||
sceneEl: videoEl.closest(".scene") || videoEl,
|
||||
firstVisible: null,
|
||||
lastVisible: null,
|
||||
});
|
||||
}
|
||||
if (entries.length === 0) return results;
|
||||
|
||||
const sceneEl = videoEl.closest(".scene") || videoEl;
|
||||
|
||||
let firstVisible: number | null = null;
|
||||
let lastVisible: number | null = null;
|
||||
|
||||
for (let t = 0; t <= duration; t += SAMPLE_STEP) {
|
||||
seekTl(t);
|
||||
const opacity = parseFloat(window.getComputedStyle(sceneEl).opacity);
|
||||
for (let t = 0; t <= duration; t += SAMPLE_STEP) {
|
||||
seekTl(t);
|
||||
for (const entry of entries) {
|
||||
const opacity = parseFloat(window.getComputedStyle(entry.sceneEl).opacity);
|
||||
if (opacity > 0) {
|
||||
if (firstVisible === null) firstVisible = t;
|
||||
lastVisible = t;
|
||||
if (entry.firstVisible === null) entry.firstVisible = t;
|
||||
entry.lastVisible = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-video boundary refinement (cheap: O(log(step)) seeks each).
|
||||
for (const entry of entries) {
|
||||
const { id, sceneEl, firstVisible, lastVisible } = entry;
|
||||
if (firstVisible === null || lastVisible === null) continue;
|
||||
|
||||
// Binary search left boundary
|
||||
|
||||
Reference in New Issue
Block a user