Skip to content

Commit 48406e4

Browse files
committed
fix(core): a scene's media plays past a shorter root animation again
1 parent d848c42 commit 48406e4

2 files changed

Lines changed: 60 additions & 51 deletions

File tree

‎packages/core/src/runtime/init.subCompositionPlacement.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,23 @@ describe("runtime sub-composition placement", () => {
304304
expect(sourceTimeOf("scene-split-split", "v3")).toBe(4);
305305
});
306306

307+
it("lets a scene's video play past a shorter root animation when the root authors no length", () => {
308+
document.body.innerHTML =
309+
`<div data-composition-id="main" data-root="true">` +
310+
`<div class="clip" data-composition-id="scene" data-composition-file="compositions/scene.html" data-start="0" data-duration="10">` +
311+
`<video id="long" data-start="0" data-duration="10"></video></div></div>`;
312+
stubDuration(video("long"), 10);
313+
Object.defineProperty(video("long"), "currentTime", { value: -1, writable: true });
314+
video("long").play = vi.fn(() => Promise.resolve());
315+
video("long").pause = vi.fn();
316+
// The root's own timeline is a 2 s title fade.
317+
window.__timelines = { main: createMockTimeline(2) };
318+
initSandboxRuntimeModular();
319+
320+
const at = (t: number) => (window.__player?.renderSeek(t), video("long").currentTime);
321+
expect([at(1), at(5), at(8)]).toEqual([1, 5, 8]);
322+
});
323+
307324
it("keeps a video in a composition nested inside the half silent until the half starts", () => {
308325
loadSecondHalf(
309326
`<div data-composition-id="inner" data-composition-file="compositions/inner.html" data-start="0" data-duration="12">` +

‎packages/core/src/runtime/startResolver.ts‎

Lines changed: 43 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -53,60 +53,51 @@ export function createRuntimeStartTimeResolver(params: {
5353
);
5454
};
5555

56-
const resolveDurationForElement = (element: Element): number | null => {
57-
const cached = durationCache.get(element);
58-
if (cached !== undefined) return cached;
59-
let resolved: number | null = null;
60-
const durationTiming = resolveAuthoredTimingWindow({
56+
const authoredAttr = (element: Element, name: string) =>
57+
includeAuthoredTimingAttrs ? element.getAttribute(name) : null;
58+
59+
const resolveAuthoredDuration = (element: Element): number | null =>
60+
resolveAuthoredTimingWindow({
6161
start: 0,
6262
duration: element.getAttribute("data-duration"),
63-
authoredDuration: includeAuthoredTimingAttrs
64-
? element.getAttribute("data-hf-authored-duration")
65-
: null,
66-
});
67-
if (durationTiming?.duration != null && durationTiming.duration > 0) {
68-
resolved = durationTiming.duration;
63+
authoredDuration: authoredAttr(element, "data-hf-authored-duration"),
64+
})?.duration ??
65+
resolveAuthoredTimingWindow({
66+
start: resolveStartForElementInternal(element, 0),
67+
end: element.getAttribute("data-end"),
68+
authoredEnd: authoredAttr(element, "data-hf-authored-end"),
69+
})?.duration ??
70+
null;
71+
72+
const positive = (value: number | null | undefined): number | null =>
73+
value != null && Number.isFinite(value) && value > 0 ? value : null;
74+
75+
const resolveRegisteredTimelineDuration = (element: Element): number | null => {
76+
const timeline = timelineRegistry[element.getAttribute("data-composition-id") ?? ""];
77+
if (!timeline || typeof timeline.duration !== "function") return null;
78+
try {
79+
return positive(Number(timeline.duration()));
80+
} catch (err) {
81+
// ignore broken timeline impls
82+
swallow("runtime.startResolver.site1", err);
83+
return null;
6984
}
70-
if (resolved == null || resolved <= 0) {
71-
const start = resolveStartForElementInternal(element, 0);
72-
const endTiming = resolveAuthoredTimingWindow({
73-
start,
74-
end: element.getAttribute("data-end"),
75-
authoredEnd: includeAuthoredTimingAttrs
76-
? element.getAttribute("data-hf-authored-end")
85+
};
86+
87+
const resolveDurationForElement = (element: Element): number | null => {
88+
const cached = durationCache.get(element);
89+
if (cached !== undefined) return cached;
90+
const resolved =
91+
resolveAuthoredDuration(element) ??
92+
positive(
93+
isMediaElement(element)
94+
? resolveNaturalMediaTimelineDuration(element, element.duration)
7795
: null,
78-
});
79-
if (endTiming?.duration != null && endTiming.duration > 0) {
80-
resolved = endTiming.duration;
81-
}
82-
}
83-
if ((resolved == null || resolved <= 0) && isMediaElement(element)) {
84-
resolved = resolveNaturalMediaTimelineDuration(element, element.duration);
85-
}
86-
if (resolved == null || resolved <= 0) resolved = resolveTimedImageDurationSeconds(element);
87-
if (resolved == null || resolved <= 0) {
88-
const compositionId = element.getAttribute("data-composition-id");
89-
if (compositionId) {
90-
const timeline = timelineRegistry[compositionId] ?? null;
91-
if (timeline && typeof timeline.duration === "function") {
92-
try {
93-
const timelineDuration = Number(timeline.duration());
94-
if (Number.isFinite(timelineDuration) && timelineDuration > 0) {
95-
resolved = timelineDuration;
96-
}
97-
} catch (err) {
98-
// ignore broken timeline impls
99-
swallow("runtime.startResolver.site1", err);
100-
}
101-
}
102-
}
103-
}
104-
if (resolved != null && Number.isFinite(resolved) && resolved > 0) {
105-
durationCache.set(element, resolved);
106-
return resolved;
107-
}
108-
durationCache.set(element, null);
109-
return null;
96+
) ??
97+
positive(resolveTimedImageDurationSeconds(element)) ??
98+
resolveRegisteredTimelineDuration(element);
99+
durationCache.set(element, resolved);
100+
return resolved;
110101
};
111102

112103
// A mounted composition root without its own `data-start` takes its timing from the host it was
@@ -201,12 +192,13 @@ export function createRuntimeStartTimeResolver(params: {
201192
});
202193
};
203194

195+
// Only an authored length cuts, as in the render; a GSAP length would cut what still plays.
204196
const resolveHostSlotsForElement = (element: Element): HostSlot[] => {
205197
const slots: HostSlot[] = [];
206198
let root = element.closest("[data-composition-id]");
207199
for (; root; root = root.parentElement?.closest("[data-composition-id]") ?? null) {
208200
const start = resolveStartForElementInternal(root, 0);
209-
const duration = resolveDurationForElement(root);
201+
const duration = resolveAuthoredDuration(root);
210202
slots.push({
211203
start,
212204
end: duration ? start + duration : Infinity,

0 commit comments

Comments
 (0)