Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/cli/src/commands/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
formatSnapshotTimestamp,
parseZoomScale,
requireSnapshotFfmpeg,
resolveSnapshotVideoClipStart,
resolveSnapshotVideoFrameTime,
tailFrameTime,
} from "./snapshot.js";
Expand Down Expand Up @@ -154,6 +155,26 @@ describe("resolveSnapshotVideoFrameTime", () => {
});
});

describe("resolveSnapshotVideoClipStart", () => {
it("offsets a scene-local video start by its later template host", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 0,
templateHostStart: 3,
}),
).toBe(3);
});

it("keeps top-level video starts unchanged", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 3,
templateHostStart: 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 });
Expand Down
50 changes: 38 additions & 12 deletions packages/cli/src/commands/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ export function resolveSnapshotVideoFrameTime(input: {
return Math.max(0, Math.min(relativeTime, sourceEnd - 1 / 30));
}

/** Convert a video's scene-local authored start into root timeline time when
* it lives inside a template-mounted composition host. Top-level videos have
* no template host and therefore retain their authored start unchanged. */
export function resolveSnapshotVideoClipStart(input: {
authoredStart: number;
templateHostStart: number | null;
}): number {
return input.authoredStart + (input.templateHostStart ?? 0);
}

export function requireSnapshotFfmpeg(ffmpegPath: string | undefined): string {
if (ffmpegPath) return ffmpegPath;
throw new Error(
Expand Down Expand Up @@ -399,10 +409,16 @@ async function captureSnapshots(
if (cameraExpr) await page.evaluate(cameraExpr);

if (injectVideoFramesBatch && syncVideoFrameVisibility) {
const candidates = await page.evaluate((t: number) => {
const candidates = await page.evaluate(() => {
return Array.from(document.querySelectorAll("video[data-start]")).map((el) => {
const v = el as HTMLVideoElement;
const start = parseFloat(v.dataset.start ?? "0") || 0;
const authoredStart = parseFloat(v.dataset.start ?? "0") || 0;
const templateHost = v.closest<HTMLElement>(
"[data-composition-src], [data-composition-file]",
);
const templateHostStart = templateHost
? parseFloat(templateHost.dataset.start ?? "0") || 0
Comment thread
miguel-heygen marked this conversation as resolved.
Outdated
: null;
const rawRate = v.defaultPlaybackRate;
const playbackRate =
Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
Expand All @@ -416,30 +432,40 @@ 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,
templateHostStart,
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 }> = [];
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/runtime/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,55 @@ describe("initSandboxRuntimeModular", () => {
expect(video.currentTime).toBe(5);
});

it("keeps a scene-local video visible inside a later template-mounted host", () => {
Comment thread
miguel-heygen marked this conversation as resolved.
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "6");
root.setAttribute("data-width", "360");
root.setAttribute("data-height", "640");
document.body.appendChild(root);

const firstHost = document.createElement("div");
firstHost.setAttribute("data-composition-id", "first");
firstHost.setAttribute("data-composition-file", "compositions/first.html");
firstHost.setAttribute("data-start", "0");
firstHost.setAttribute("data-duration", "3");
root.appendChild(firstHost);

const firstVideo = document.createElement("video");
firstVideo.setAttribute("data-start", "0");
firstVideo.setAttribute("data-duration", "3");
firstHost.appendChild(firstVideo);

const secondHost = document.createElement("div");
secondHost.setAttribute("data-composition-id", "second");
secondHost.setAttribute("data-composition-file", "compositions/second.html");
secondHost.setAttribute("data-start", "3");
secondHost.setAttribute("data-duration", "3");
root.appendChild(secondHost);

const secondVideo = document.createElement("video");
secondVideo.setAttribute("data-start", "0");
secondVideo.setAttribute("data-duration", "3");
secondHost.appendChild(secondVideo);

window.__timelines = {
main: createMockTimeline(6),
first: createMockTimeline(3),
second: createMockTimeline(3),
};

initSandboxRuntimeModular();
window.__player?.renderSeek(4);

expect(firstHost.style.visibility).toBe("hidden");
expect(firstVideo.style.visibility).toBe("hidden");
expect(secondHost.style.visibility).toBe("visible");
expect(secondVideo.style.visibility).toBe("visible");
});

it("updates visibility for timed elements inside nested compositions", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,10 +628,16 @@ export function initSandboxRuntimeModular(): void {
return false;
}

const start =
tag === "video" || tag === "audio"
? resolveMediaStartSeconds(rawNode, 0)
: resolveStartForElement(rawNode, 0);
const isMedia = tag === "video" || tag === "audio";
const mediaCompositionHost = isMedia
? rawNode.closest("[data-composition-src], [data-composition-file]")
Comment thread
miguel-heygen marked this conversation as resolved.
Outdated
: null;
const mediaCompositionStart = mediaCompositionHost
? resolveStartForElement(mediaCompositionHost, 0)
: 0;
const start = isMedia
? resolveMediaStartSeconds(rawNode, mediaCompositionStart)
: resolveStartForElement(rawNode, 0);
let duration = resolveDurationForElement(rawNode);
const compId = rawNode.getAttribute("data-composition-id");
if (compId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "Later nested template video stays visible",
"description": "Two template-mounted hosts play sequentially, and each template owns a scene-local video with data-start=0 that reads its half of one generated source. Before the fix, producer capture evaluated the later video's start against root time without its host offset, hid it for the entire second host window, and rendered black instead of the generated green half.",
"tags": ["video", "sub-composition", "regression"],
"minPsnr": 25,
"maxFrameFailures": 2,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 24,
"workers": 1
}
}
Git LFS file not shown
Git LFS file not shown
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template id="first-template">
<div
data-composition-id="first-scene"
data-width="160"
data-height="90"
data-no-timeline
style="position: relative; width: 160px; height: 90px; overflow: hidden; background: #000"
>
<video
id="first-video"
class="clip"
data-start="0"
data-duration="1"
data-media-start="0"
data-track-index="0"
src="../media/source.mp4"
muted
playsinline
preload="auto"
style="display: block; width: 160px; height: 90px; object-fit: cover"
></video>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template id="later-template">
<div
data-composition-id="later-scene"
data-width="160"
data-height="90"
data-no-timeline
style="position: relative; width: 160px; height: 90px; overflow: hidden; background: #000"
>
<video
id="later-video"
class="clip"
data-start="0"
data-duration="1"
data-media-start="1"
data-track-index="0"
src="../media/source.mp4"
muted
playsinline
preload="auto"
style="display: block; width: 160px; height: 90px; object-fit: cover"
></video>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=160, height=90" />
<style>
html,
body,
#root {
width: 160px;
height: 90px;
margin: 0;
overflow: hidden;
background: #000;
}

.scene {
position: absolute;
inset: 0;
background: #000;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="160"
data-height="90"
data-no-timeline
>
<div
id="first-host"
class="scene"
data-composition-id="first-scene"
data-composition-src="compositions/first.html"
data-start="0"
data-duration="1"
data-track-index="0"
data-no-timeline
></div>
<div
id="later-host"
class="scene"
data-composition-id="later-scene"
data-composition-src="compositions/later.html"
data-start="1"
data-duration="1"
data-track-index="1"
data-no-timeline
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
</script>
</body>
</html>
Git LFS file not shown
1 change: 1 addition & 0 deletions packages/producer/tests/shard-schedule.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"distributedShardCount": 1,
"timings": {
"animejs-adapter": 17,
"nested-sequential-video-local-start": 8,
"audio-mux-parity": 32,
"chat": 54,
"css-spinner-render-compat": 15,
Expand Down
Loading