fix: offset nested template video timing (#2859)

* fix: offset nested template video timing

* test(producer): cover nested sequential video render

* fix: share canonical nested media timing
This commit is contained in:
Miguel Ángel
2026-07-29 03:36:04 +02:00
committed by GitHub
parent 4f344c50b0
commit 9bbb6d50a0
13 changed files with 385 additions and 48 deletions
@@ -5,6 +5,7 @@ import {
formatSnapshotTimestamp,
parseZoomScale,
requireSnapshotFfmpeg,
resolveSnapshotVideoClipStart,
resolveSnapshotVideoFrameTime,
tailFrameTime,
} from "./snapshot.js";
@@ -154,6 +155,35 @@ describe("resolveSnapshotVideoFrameTime", () => {
});
});
describe("resolveSnapshotVideoClipStart", () => {
it("offsets a scene-local video start by its later template host", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 0,
runtimeResolvedStart: 3,
}),
).toBe(3);
});
it("uses the runtime's recursively resolved start for deeply nested media", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 1,
runtimeResolvedStart: 8,
}),
).toBe(8);
});
it("keeps authored starts as a compatibility fallback", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 3,
runtimeResolvedStart: null,
}),
).toBe(3);
});
});
describe("computeSnapshotTimes (FINDING [7]: tail is always captured)", () => {
it("default frames: last point is the readable tail, never exact duration", () => {
const { times, appendedTail } = computeSnapshotTimes(8, { frames: 5 });
+39 -13
View File
@@ -93,6 +93,15 @@ export function resolveSnapshotVideoFrameTime(input: {
return Math.max(0, Math.min(relativeTime, sourceEnd - 1 / 30));
}
/** Prefer the runtime's canonical absolute media start. The authored value is
* only a compatibility fallback for pages built with an older runtime. */
export function resolveSnapshotVideoClipStart(input: {
authoredStart: number;
runtimeResolvedStart: number | null;
}): number {
return input.runtimeResolvedStart ?? input.authoredStart;
}
export function requireSnapshotFfmpeg(ffmpegPath: string | undefined): string {
if (ffmpegPath) return ffmpegPath;
throw new Error(
@@ -399,10 +408,14 @@ async function captureSnapshots(
if (cameraExpr) await page.evaluate(cameraExpr);
if (injectVideoFramesBatch && syncVideoFrameVisibility) {
const candidates = await page.evaluate((t: number) => {
return Array.from(document.querySelectorAll("video[data-start]")).map((el) => {
const candidates = await page.evaluate(() => {
const runtimeWindow = window as Window & {
__hfResolveMediaStartSeconds?: (element: Element) => number;
};
return Array.from(document.querySelectorAll("video")).map((el) => {
const v = el as HTMLVideoElement;
const start = parseFloat(v.dataset.start ?? "0") || 0;
const authoredStart = parseFloat(v.dataset.start ?? "0") || 0;
const runtimeResolvedStart = runtimeWindow.__hfResolveMediaStartSeconds?.(v);
const rawRate = v.defaultPlaybackRate;
const playbackRate =
Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
@@ -416,30 +429,43 @@ async function captureSnapshots(
: srcDur > 0
? Math.max(0, (srcDur - mediaStart) / playbackRate)
: Number.POSITIVE_INFINITY;
let relTime = (t - start) * playbackRate + mediaStart;
if (v.loop && srcDur > mediaStart && relTime >= srcDur) {
relTime = mediaStart + ((relTime - mediaStart) % (srcDur - mediaStart));
}
return {
id: v.id,
src: v.currentSrc || v.src,
start,
authoredStart,
runtimeResolvedStart:
runtimeResolvedStart !== undefined && Number.isFinite(runtimeResolvedStart)
? runtimeResolvedStart
: null,
duration,
srcDuration: srcDur,
relTime,
playbackRate,
mediaStart,
loop: v.loop,
};
});
}, time);
});
const active = candidates.flatMap((candidate) => {
const start = resolveSnapshotVideoClipStart(candidate);
let relTime = (time - start) * candidate.playbackRate + candidate.mediaStart;
if (
candidate.loop &&
candidate.srcDuration > candidate.mediaStart &&
relTime >= candidate.srcDuration
) {
relTime =
candidate.mediaStart +
((relTime - candidate.mediaStart) % (candidate.srcDuration - candidate.mediaStart));
}
if (!candidate.id || !candidate.src) return [];
const frameTime = resolveSnapshotVideoFrameTime({
globalTime: time,
clipStart: candidate.start,
clipStart: start,
clipDuration: candidate.duration,
relativeTime: candidate.relTime,
relativeTime: relTime,
sourceDuration: candidate.srcDuration,
});
return frameTime === null ? [] : [{ ...candidate, relTime: frameTime }];
return frameTime === null ? [] : [{ ...candidate, start, relTime: frameTime }];
});
const updates: Array<{ videoId: string; dataUri: string }> = [];