mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(player): drive composition ticks from widget-frame rAF via postMessage (#805)
Chromium throttles requestAnimationFrame in deeply nested cross-origin iframes. In Claude desktop (Electron), the composition iframe's own rAF loop stalls, so GSAP is never seeked and animation freezes even when TransportClock.isPlaying() is true. The correct fix is to drive ticks from the widget-frame rAF, which lives one level up and is not subject to the same throttling. When play() takes the runtime bridge path (no direct timeline adapter), the player now starts a parent-frame rAF loop that sends "tick" postMessages to the composition iframe on every frame. The runtime's control bridge handles "tick" by calling seekTimelineAndAdapters(clock.now()) if the clock is playing — identical to what transportTick does on each rAF, just driven from outside. The composition iframe's own rAF loop is unchanged and keeps running normally in standard browsers. Seeking GSAP twice per frame is idempotent, so there is no regression on claude.ai or any other non-throttled environment. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
30348af3f4
commit
c08e8b2322
@@ -6,6 +6,7 @@ function createMockDeps() {
|
||||
onPlay: vi.fn(),
|
||||
onPause: vi.fn(),
|
||||
onSeek: vi.fn(),
|
||||
onTick: vi.fn(),
|
||||
onSetMuted: vi.fn(),
|
||||
onSetVolume: vi.fn(),
|
||||
onSetMediaOutputMuted: vi.fn(),
|
||||
@@ -110,6 +111,13 @@ describe("installRuntimeControlBridge", () => {
|
||||
expect(deps.onSetPlaybackRate).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("dispatches tick command", () => {
|
||||
const deps = createMockDeps();
|
||||
const handler = installRuntimeControlBridge(deps);
|
||||
handler(makeControlMessage("tick"));
|
||||
expect(deps.onTick).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("dispatches enable-pick-mode", () => {
|
||||
const deps = createMockDeps();
|
||||
const handler = installRuntimeControlBridge(deps);
|
||||
|
||||
@@ -5,6 +5,7 @@ type BridgeDeps = {
|
||||
onPlay: () => void;
|
||||
onPause: () => void;
|
||||
onSeek: (frame: number, seekMode: "drag" | "commit") => void;
|
||||
onTick: () => void;
|
||||
onSetMuted: (muted: boolean) => void;
|
||||
onSetVolume: (volume: number) => void;
|
||||
onSetMediaOutputMuted: (muted: boolean) => void;
|
||||
@@ -39,6 +40,10 @@ export function installRuntimeControlBridge(deps: BridgeDeps): (event: MessageEv
|
||||
deps.onSeek(Number(data.frame ?? 0), data.seekMode ?? "commit");
|
||||
return;
|
||||
}
|
||||
if (action === "tick") {
|
||||
deps.onTick();
|
||||
return;
|
||||
}
|
||||
if (action === "set-muted") {
|
||||
deps.onSetMuted(Boolean(data.muted));
|
||||
return;
|
||||
|
||||
@@ -1602,6 +1602,27 @@ export function initSandboxRuntimeModular(): void {
|
||||
applyPlaybackRate(rate);
|
||||
if (state.transportClock) state.transportClock.setRate(state.playbackRate);
|
||||
},
|
||||
onTick: () => {
|
||||
if (state.tornDown || !clock.isPlaying()) return;
|
||||
const t = clock.now();
|
||||
state.currentTime = t;
|
||||
seekTimelineAndAdapters(t);
|
||||
if (clock.reachedEnd()) {
|
||||
webAudio.stopAll();
|
||||
clock.detachAudioSource();
|
||||
clock.pause();
|
||||
state.isPlaying = false;
|
||||
const dur = clock.getDuration();
|
||||
if (Number.isFinite(dur)) {
|
||||
clock.seek(dur);
|
||||
state.currentTime = dur;
|
||||
seekTimelineAndAdapters(dur);
|
||||
}
|
||||
runAdapters("pause");
|
||||
syncMediaForCurrentState();
|
||||
postState(true);
|
||||
}
|
||||
},
|
||||
onEnablePickMode: () => picker.enablePickMode(),
|
||||
onDisablePickMode: () => picker.disablePickMode(),
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export type RuntimeBridgeControlAction =
|
||||
| "play"
|
||||
| "pause"
|
||||
| "seek"
|
||||
| "tick"
|
||||
| "set-muted"
|
||||
| "set-volume"
|
||||
| "set-media-output-muted"
|
||||
|
||||
Reference in New Issue
Block a user