Skip to content

Commit 9701f0d

Browse files
fix(core): keep rebound root timelines clock-driven (#5165)
* fix(core): keep rebound root timelines clock-driven * test(core): verify transport advances the rebound root
1 parent da62598 commit 9701f0d

2 files changed

Lines changed: 56 additions & 5 deletions

File tree

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3669,6 +3669,61 @@ describe("initSandboxRuntimeModular", () => {
36693669
}
36703670
});
36713671

3672+
it("keeps a root rebound during playback paused on GSAP's ticker", () => {
3673+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
3674+
let nowMs = 1000;
3675+
vi.spyOn(performance, "now").mockImplementation(() => nowMs);
3676+
const raf = createManualRaf();
3677+
window.requestAnimationFrame = raf.requestAnimationFrame;
3678+
window.cancelAnimationFrame = raf.cancelAnimationFrame;
3679+
document.body.innerHTML =
3680+
'<div data-composition-id="main" data-root="true" data-start="0" data-duration="1">' +
3681+
'<video data-start="0"></video></div>';
3682+
const video = document.querySelector("video")!;
3683+
vi.spyOn(video, "play").mockResolvedValue(undefined);
3684+
vi.spyOn(video, "pause").mockImplementation(() => {});
3685+
const authored = gsap.timeline({ paused: true });
3686+
window.gsap = gsap;
3687+
window.__timelines = { main: authored };
3688+
const timelines = vi.spyOn(gsap, "timeline");
3689+
const messages = vi.spyOn(window.parent, "postMessage");
3690+
3691+
try {
3692+
initSandboxRuntimeModular();
3693+
vi.advanceTimersByTime(0);
3694+
window.__player!.play();
3695+
nowMs = 1250;
3696+
raf.step(250);
3697+
Object.defineProperty(video, "duration", { value: 12, configurable: true });
3698+
video.dispatchEvent(new Event("loadedmetadata"));
3699+
vi.advanceTimersByTime(100);
3700+
3701+
expect(
3702+
messages.mock.calls.some(
3703+
([message]) =>
3704+
(message as { code?: string }).code === "timeline_rebind_after_media_metadata",
3705+
),
3706+
).toBe(true);
3707+
const rebound = timelines.mock.results.at(-1)?.value as gsap.core.Timeline;
3708+
expect(authored.parent).toBe(rebound);
3709+
expect(window.__player!.isPlaying()).toBe(true);
3710+
expect(rebound.paused()).toBe(true);
3711+
const time = rebound.time();
3712+
gsap.updateRoot(gsap.globalTimeline.time() + 0.5);
3713+
expect(rebound.time()).toBe(time);
3714+
expect(window.__player!.getTime()).toBe(0.25);
3715+
nowMs = 1350;
3716+
raf.step(100);
3717+
expect(rebound.time()).toBeCloseTo(0.35, 5);
3718+
} finally {
3719+
window.__hfRuntimeTeardown?.();
3720+
for (const result of timelines.mock.results) {
3721+
if (result.type === "return") (result.value as gsap.core.Timeline).kill();
3722+
}
3723+
authored.kill();
3724+
}
3725+
});
3726+
36723727
it("sets __renderReady only after timeline is bound, not at __playerReady time", async () => {
36733728
const root = document.createElement("div");
36743729
root.setAttribute("data-composition-id", "main");

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,21 +2294,17 @@ export function initSandboxRuntimeModular(): void {
22942294
return false;
22952295
}
22962296
const previousTime = Math.max(0, state.currentTime || 0);
2297-
const wasPlaying = state.isPlaying;
22982297
state.capturedTimeline = resolution.timeline;
22992298
if (typeof state.capturedTimeline.timeScale === "function") {
23002299
state.capturedTimeline.timeScale(state.playbackRate);
23012300
}
23022301
try {
23032302
// pause guarded separately: a PARTIAL timeline without pause() must not
2304-
// abort the seek/play restore below (the catch would swallow them too).
2303+
// abort restoring the transport time below (the catch would swallow it too).
23052304
pauseTimelineIfPossible(state.capturedTimeline);
23062305
if (typeof state.capturedTimeline.seek === "function") {
23072306
state.capturedTimeline.seek(previousTime, false);
23082307
}
2309-
if (wasPlaying && typeof state.capturedTimeline.play === "function") {
2310-
state.capturedTimeline.play();
2311-
}
23122308
} catch (err) {
23132309
// keep runtime resilient even if a timeline implementation throws
23142310
swallow("runtime.init.site7", err);

0 commit comments

Comments
 (0)