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:
Val
2026-08-19 23:49:01 -07:00
committed by GitHub
parent a6a9e2f89e
commit d09145faab
2 changed files with 96 additions and 10 deletions
@@ -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-"));
+26 -10
View File
@@ -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