Skip to content

Commit c003ef6

Browse files
authored
fix(player): pause parent audio proxy on seek to prevent stutter loop (#890)
## Summary - `seek()` only called `seekAll()` under parent audio ownership, leaving the `<audio>` proxy playing while the timeline froze at the new seek target. - The periodic `mirrorTime` drift-correction (`parent-media.ts`) would then yank `currentTime` back to the timeline position every ~80ms of accumulated drift, producing an audible stutter loop while the video frame stayed frozen. - Fix: make `seek()` symmetric with `pause()` — pause the parent proxy before seeking it. ## Repro 1. Use the player in an environment where the runtime posts `media-autoplay-blocked` (mobile / autoplay-restricted contexts), promoting audio ownership to `"parent"`. 2. Start playback with audio. 3. Click anywhere on the scrubber while playing. 4. Before: audio stutters in a short loop while the video frame is frozen. 5. After: audio cleanly pauses at the new seek position. ## Test plan - [x] Added regression test \`seek() while playing pauses parent proxy (prevents mirrorTime stutter loop)\` in \`hyperframes-player.test.ts\`. - [x] \`pnpm --filter @hyperframes/player test\` — 110/110 pass. - [ ] Manual repro on a device where ownership flips to \`parent\`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 1e05d78 commit c003ef6

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

packages/player/src/hyperframes-player.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,26 @@ describe("HyperframesPlayer parent-frame media", () => {
198198
expect(mockAudio.pause).toHaveBeenCalled();
199199
});
200200

201+
it("seek() while playing pauses parent proxy (prevents mirrorTime stutter loop)", () => {
202+
// Regression: previously `seek()` only called `seekAll()`, leaving the
203+
// proxy playing. With the timeline frozen at the new seek target, the
204+
// parent's `mirrorTime` drift-correction would yank `currentTime` back
205+
// every ~80ms of accumulated drift, producing an audible audio stutter
206+
// loop while the video frame stayed frozen. `seek()` must be symmetric
207+
// with `pause()` for the parent-owned audio path.
208+
player.setAttribute("audio-src", "https://cdn.example.com/narration.mp3");
209+
document.body.appendChild(player);
210+
211+
player._promoteToParentProxy?.();
212+
player.play();
213+
expect(mockAudio.play).toHaveBeenCalled();
214+
mockAudio.pause.mockClear();
215+
216+
player.seek(12.5);
217+
expect(mockAudio.pause).toHaveBeenCalled();
218+
expect(mockAudio.currentTime).toBe(12.5);
219+
});
220+
201221
it("promotion is idempotent", () => {
202222
player.setAttribute("audio-src", "https://cdn.example.com/narration.mp3");
203223
document.body.appendChild(player);

packages/player/src/hyperframes-player.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,15 @@ class HyperframesPlayer extends HTMLElement {
264264
this._directTimelineClock.stop();
265265
this._stopParentTickClock();
266266
this._currentTime = timeInSeconds;
267-
if (this._media.audioOwner === "parent") this._media.seekAll(timeInSeconds);
267+
if (this._media.audioOwner === "parent") {
268+
// Pause BEFORE seek: leaving the proxy playing turns the next
269+
// `mirrorTime` drift-correction tick into a perpetual seek→play→drift→seek
270+
// stutter loop, where ~80ms of audio plays past the (now frozen) timeline,
271+
// then mirrorTime yanks `currentTime` back to match it. Symmetric with
272+
// `pause()` below.
273+
this._media.pauseAll();
274+
this._media.seekAll(timeInSeconds);
275+
}
268276
this._paused = true;
269277
this.controlsApi?.updatePlaying(false);
270278
this.controlsApi?.updateTime(this._currentTime, this._duration);

0 commit comments

Comments
 (0)