Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
113 changes: 113 additions & 0 deletions packages/core/src/runtime/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,119 @@ describe("initSandboxRuntimeModular", () => {
expect(hookHost.style.visibility).toBe("visible");
});

it("shows pip video at correct time when host composition starts late (no double-offset)", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);

const host = document.createElement("div");
host.setAttribute("data-composition-id", "scene-pip");
host.setAttribute("data-start", "45.40");
host.setAttribute("data-duration", "7.06");
root.appendChild(host);

const innerRoot = document.createElement("div");
innerRoot.setAttribute("data-composition-id", "scene-pip");
host.appendChild(innerRoot);

const pipVideo = document.createElement("video");
pipVideo.setAttribute("data-start", "45.40");
pipVideo.setAttribute("data-duration", "7.06");
Object.defineProperty(pipVideo, "paused", { value: true, configurable: true });
Object.defineProperty(pipVideo, "readyState", { value: 0, configurable: true });
Object.defineProperty(pipVideo, "currentTime", {
value: 0,
writable: true,
configurable: true,
});
pipVideo.load = () => {};
innerRoot.appendChild(pipVideo);

(window as Window & { __timelines?: Record<string, RuntimeTimelineLike> }).__timelines = {
main: createMockTimeline(60),
"scene-pip": createMockTimeline(7.06),
};

initSandboxRuntimeModular();

const player = (
window as Window & {
__player?: { seek: (timeSeconds: number) => void };
}
).__player;
expect(player).toBeDefined();

player?.seek(46);
expect(pipVideo.style.visibility).toBe("visible");

player?.seek(53);
expect(pipVideo.style.visibility).toBe("hidden");

player?.seek(44);
expect(pipVideo.style.visibility).toBe("hidden");
});

it("shows auto-start video at host time, not at t=0", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);

const host = document.createElement("div");
host.setAttribute("data-composition-id", "intro");
host.setAttribute("data-start", "10");
host.setAttribute("data-duration", "5");
root.appendChild(host);

const innerRoot = document.createElement("div");
innerRoot.setAttribute("data-composition-id", "intro");
host.appendChild(innerRoot);

const video = document.createElement("video");
video.setAttribute("data-start", "0");
video.setAttribute("data-hf-auto-start", "");
video.setAttribute("data-duration", "5");
Object.defineProperty(video, "paused", { value: true, configurable: true });
Object.defineProperty(video, "readyState", { value: 0, configurable: true });
Object.defineProperty(video, "currentTime", {
value: 0,
writable: true,
configurable: true,
});
video.load = () => {};
innerRoot.appendChild(video);

(window as Window & { __timelines?: Record<string, RuntimeTimelineLike> }).__timelines = {
main: createMockTimeline(30),
intro: createMockTimeline(5),
};

initSandboxRuntimeModular();

const player = (
window as Window & {
__player?: { seek: (timeSeconds: number) => void };
}
).__player;
expect(player).toBeDefined();

player?.seek(12);
expect(video.style.visibility).toBe("visible");

player?.seek(5);
expect(video.style.visibility).toBe("hidden");

player?.seek(16);
expect(video.style.visibility).toBe("hidden");
});

it("plays scheduled child timelines without a captured root timeline when audio has failed", () => {
const raf = createManualRaf();
vi.spyOn(performance, "now").mockImplementation(() => raf.now());
Expand Down
21 changes: 19 additions & 2 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1278,14 +1278,20 @@ export function initSandboxRuntimeModular(): void {
element.hasAttribute("data-start") ||
Boolean(resolveMediaCompositionContext(element).compositionRoot),
resolveStartSeconds: (element) => {
if (!element.hasAttribute("data-hf-auto-start") && element.hasAttribute("data-start")) {
return Math.max(0, Number(element.getAttribute("data-start") ?? 0) || 0);
}
const context = resolveMediaCompositionContext(
element as HTMLVideoElement | HTMLAudioElement,
);
return resolveStartForElement(element, context.inheritedStart ?? 0);
},
resolveDurationSeconds: (element) => {
const context = resolveMediaCompositionContext(element);
const start = resolveStartForElement(element, context.inheritedStart ?? 0);
const start =
!element.hasAttribute("data-hf-auto-start") && element.hasAttribute("data-start")
? Math.max(0, Number(element.getAttribute("data-start") ?? 0) || 0)
: resolveStartForElement(element, context.inheritedStart ?? 0);
const mediaStart =
Number.parseFloat(element.dataset.playbackStart ?? element.dataset.mediaStart ?? "0") ||
0;
Expand Down Expand Up @@ -1329,7 +1335,18 @@ export function initSandboxRuntimeModular(): void {
const tag = rawNode.tagName.toLowerCase();
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") continue;

const start = resolveStartForElement(rawNode, 0);
// Media elements (video/audio) without data-hf-auto-start have their
// data-start authored in global time — the same contract used by the
// render pipeline's discoverMediaFromBrowser. Using resolveStartForElement
// would add the host composition's offset a second time, creating a
// double-offset that keeps the element permanently hidden when its host
// doesn't start at t=0. Auto-injected timing (data-hf-auto-start) is
// composition-local and still needs the resolver.
const isGlobalMediaStart =
(tag === "video" || tag === "audio") && !rawNode.hasAttribute("data-hf-auto-start");
const start = isGlobalMediaStart
? Math.max(0, Number(rawNode.getAttribute("data-start") ?? 0) || 0)
: resolveStartForElement(rawNode, 0);
let duration = resolveDurationForElement(rawNode);
const compId = rawNode.getAttribute("data-composition-id");
if (compId) {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/runtime/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ export function collectRuntimeTimelinePayload(params: {
if (mediaNodes.length === 0) return null;
let maxWindowEndSeconds = 0;
for (const mediaNode of mediaNodes) {
const start = startResolver.resolveStartForElement(mediaNode, 0);
const start = !mediaNode.hasAttribute("data-hf-auto-start")
? Math.max(0, Number(mediaNode.getAttribute("data-start") ?? 0) || 0)
: startResolver.resolveStartForElement(mediaNode, 0);
if (!Number.isFinite(start)) continue;
const duration = resolveMediaElementDurationSeconds(mediaNode);
if (duration == null || duration <= 0) continue;
Expand Down
Loading