diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts
index 02809177e1..eafa8260d9 100644
--- a/packages/core/src/runtime/init.test.ts
+++ b/packages/core/src/runtime/init.test.ts
@@ -3669,6 +3669,61 @@ describe("initSandboxRuntimeModular", () => {
}
});
+ it("keeps a root rebound during playback paused on GSAP's ticker", () => {
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
+ let nowMs = 1000;
+ vi.spyOn(performance, "now").mockImplementation(() => nowMs);
+ const raf = createManualRaf();
+ window.requestAnimationFrame = raf.requestAnimationFrame;
+ window.cancelAnimationFrame = raf.cancelAnimationFrame;
+ document.body.innerHTML =
+ '
' +
+ '
';
+ const video = document.querySelector("video")!;
+ vi.spyOn(video, "play").mockResolvedValue(undefined);
+ vi.spyOn(video, "pause").mockImplementation(() => {});
+ const authored = gsap.timeline({ paused: true });
+ window.gsap = gsap;
+ window.__timelines = { main: authored };
+ const timelines = vi.spyOn(gsap, "timeline");
+ const messages = vi.spyOn(window.parent, "postMessage");
+
+ try {
+ initSandboxRuntimeModular();
+ vi.advanceTimersByTime(0);
+ window.__player!.play();
+ nowMs = 1250;
+ raf.step(250);
+ Object.defineProperty(video, "duration", { value: 12, configurable: true });
+ video.dispatchEvent(new Event("loadedmetadata"));
+ vi.advanceTimersByTime(100);
+
+ expect(
+ messages.mock.calls.some(
+ ([message]) =>
+ (message as { code?: string }).code === "timeline_rebind_after_media_metadata",
+ ),
+ ).toBe(true);
+ const rebound = timelines.mock.results.at(-1)?.value as gsap.core.Timeline;
+ expect(authored.parent).toBe(rebound);
+ expect(window.__player!.isPlaying()).toBe(true);
+ expect(rebound.paused()).toBe(true);
+ const time = rebound.time();
+ gsap.updateRoot(gsap.globalTimeline.time() + 0.5);
+ expect(rebound.time()).toBe(time);
+ expect(window.__player!.getTime()).toBe(0.25);
+ nowMs = 1350;
+ raf.step(100);
+ expect(rebound.time()).toBeCloseTo(0.35, 5);
+ } finally {
+ window.__hfRuntimeTeardown?.();
+ for (const result of timelines.mock.results) {
+ if (result.type === "return") (result.value as gsap.core.Timeline).kill();
+ }
+ authored.kill();
+ }
+ });
+
it("sets __renderReady only after timeline is bound, not at __playerReady time", async () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts
index bbc8f1ac38..f5d2014c25 100644
--- a/packages/core/src/runtime/init.ts
+++ b/packages/core/src/runtime/init.ts
@@ -2294,21 +2294,17 @@ export function initSandboxRuntimeModular(): void {
return false;
}
const previousTime = Math.max(0, state.currentTime || 0);
- const wasPlaying = state.isPlaying;
state.capturedTimeline = resolution.timeline;
if (typeof state.capturedTimeline.timeScale === "function") {
state.capturedTimeline.timeScale(state.playbackRate);
}
try {
// pause guarded separately: a PARTIAL timeline without pause() must not
- // abort the seek/play restore below (the catch would swallow them too).
+ // abort restoring the transport time below (the catch would swallow it too).
pauseTimelineIfPossible(state.capturedTimeline);
if (typeof state.capturedTimeline.seek === "function") {
state.capturedTimeline.seek(previousTime, false);
}
- if (wasPlaying && typeof state.capturedTimeline.play === "function") {
- state.capturedTimeline.play();
- }
} catch (err) {
// keep runtime resilient even if a timeline implementation throws
swallow("runtime.init.site7", err);