mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
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:
@@ -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 });
|
||||
|
||||
@@ -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 }> = [];
|
||||
|
||||
Reference in New Issue
Block a user