@remotion/player: Fix audio hiccups on rapid play/pause toggling#9041
@remotion/player: Fix audio hiccups on rapid play/pause toggling#9041iliya-vidrush wants to merge 7 commits into
@remotion/player: Fix audio hiccups on rapid play/pause toggling#9041Conversation
Only apply the 30ms 0-to-1 gain ramp in resume() when there are nodes in nodesToResume that need to be started. Those begin mid-waveform and can click, which is what the fade is meant to mask. If there is nothing to start, resume() merely unfreezes the nodes that suspend() froze in place, which continues sample-exact. Fading in that case caused an audible dip on every play/pause toggle. Ref remotion-dev#8984
Route the statechange re-anchor through the normal drift guard instead of forcing it. A plain pause does not move the playhead, so the anchor stays put and the queued audio nodes remain valid - suspend/resume becomes a freeze/unfreeze of the same nodes without a teardown. If the drift does exceed the guard, re-anchor and dispatch 'changed' so the media players rebuild their queue against the new anchor. Previously the anchor was moved silently on every suspend, leaving the frozen queue misaligned by the shift. Ref remotion-dev#8984
All call sites now go through the drift guard, so drop the force parameter together with the force-only zero-shift check and the 'forcibly' log wording. Ref remotion-dev#8984
Move the inline parameter type of setGlobalTimeAnchor into a named type for readability. No behavior change.
Name the repeated inline object types: RegisterAudioOptions (shared by the context value, registerAudio and useSharedAudio), UpdateAudioOptions (RegisterAudioOptions plus id) and the props types of the two providers. No behavior change.
Name the usePlayback parameter object (UsePlaybackOptions) and the raf/timeout union used for the queued frame call (QueuedFrameCall). No behavior change.
@remotion/player: Fix audio hiccups on rapid play/pause toggling
There was a problem hiding this comment.
Reviewed changes — fix rapid play/pause audio hiccups in the Player by conditionally skipping the resume fade-in and removing the forced audio-context-suspend re-anchor.
packages/core/src/audio/shared-audio-tags.tsx— only applies the 30 ms master-gain fade-in whennodesToResumehas entries; pure resume/unfreeze paths no longer dip the gain, which removes the per-play fade artifact.packages/player/src/set-global-time-anchor.ts— removes theforceoption and theforce-specific logging, making every anchor update go through the same drift guard.packages/player/src/use-playback.ts— onstatechangeto suspended/running-to-suspended,setGlobalTimeAnchoris now called withoutforce; if the drift exceeds the guard it dispatches'changed', otherwise the queued nodes stay in place.
⚠️ Suspend statechange handler reads live frame, which can mis-represent a pure pause as drift
On a plain pause, suspend() is requested immediately, but the statechange event fires asynchronously once the context actually suspends. By then the Player may have rendered additional frames (or already be resuming). The handler calls getCurrentFrame() at event time to compute the anchor, but the frozen audio nodes were scheduled against the frame that was current when suspend() was requested, not against whatever frame is current when the event eventually fires.
If those two frames differ, setGlobalTimeAnchor can report a shift that is larger than the guard even though the audio queue itself is still aligned with the original anchor. Because this path now dispatches 'changed' on large shifts, it can tear down and rebuild the audio iterator in the middle of a rapid pause/play burst — reintroducing the kind of hiccup the PR is meant to remove.
This is most likely to happen when the user toggles play/pause faster than the audio context's suspend latency.
Technical details
# Suspend statechange handler reads live frame
## Affected sites
- `packages/player/src/use-playback.ts:144` — `absoluteTimeInSeconds: getCurrentFrame() / config.fps` uses the current frame at event time, not the frame that was current when the suspend was requested.
- `packages/player/src/use-playback.ts:153` — if `setGlobalTimeAnchor` returns `true`, the handler dispatches `'changed'`, which in `@remotion/media` triggers `audioSyncAnchorChanged()` -> `destroyIterator()` -> re-prime.
## Required outcome
A pure pause/play cycle should not trigger an anchor change or audio queue rebuild.
The anchor computation on `statechange` should reflect the playhead position that the currently queued audio nodes were scheduled against.
## Suggested approach
Capture the frame at the moment `suspend()` is requested (or at least at the moment playback transitions from playing to paused) in a ref, and use that ref value in the `statechange` handler instead of `getCurrentFrame()`.
For example, the effect that handles `playing` changes could store the current frame in a ref when it calls `sharedAudioContext.suspend()`, and the `statechange` handler could read that ref.
## Open questions for the human
- Is the current behavior already known to be safe on all target browsers, or has it been observed to re-hiccup in practice during very rapid toggling?
- Does the seek effect (keyed on `frame`) already provide enough correction when a real seek happens during the suspended window?Kimi K2 (free via Pullfrog for OSS) | 𝕏
…ive frame The statechange handler computed the audio time anchor from getCurrentFrame() at the moment the 'statechange' event fired. That event is async relative to the suspend() request, so during a rapid play/pause burst it could read a frame from an already-resuming state, measure spurious drift, and dispatch 'changed' - tearing down and rebuilding the queued audio nodes even though no seek occurred. Capture the frame at each suspend() request in a ref and re-anchor against that instead. The suspended nodes were scheduled against that frame, so it is the correct reference and a plain pause/play cycle no longer trips the drift guard.
36b05b7 to
c0e9a30
Compare
There was a problem hiding this comment.
test
Kimi K2 (free via Pullfrog for OSS) | 𝕏
|
Important The latest commit ( Reviewed changes (since prior Pullfrog review) — Added
|
|
Thanks for taking on this! I can still reproduce the issue though (0:08 in the video) The issue is reproducing both when the external audio interface is selected and the embedded one (MacBook speakers) audio-choppiness.mp4 |
|
We are more aggressively closing AI-generated PRs if they are not obvious and in this case, we found an issue. |

Fixes #8984
Problem
Rapidly toggling play/pause in the Player caused audible audio hiccups: the sound stopped and restarted stepwise instead of settling to the final state, and every restart faded in.
Two independent mechanisms caused this:
resume()inshared-audio-tags.tsxramped the master gain 0 to 1 over 30 ms on every resume, so every play toggle produced an audible dip - even when the context merely unfroze nodes thatsuspend()had frozen in place sample-exact.statechangelistener inuse-playback.tsforce-moved the audio sync anchor on every suspend. It never notified the media players, so the already-queued (frozen) nodes stayed scheduled against the old anchor and were misaligned by the shift after resume.Changes
remotion:resume()only applies the fade-in when there are nodes innodesToResumethat need to be started. Those begin mid-waveform and can click, which is what the fade is for. A plain unfreeze is sample-exact and no longer fades.@remotion/player: the suspend re-anchor now goes through the normal drift guard (ALLOWED_GLOBAL_TIME_ANCHOR_SHIFT + latency) instead offorce: true. On a plain pause the drift is below the guard, so the anchor stays put and pause/play becomes a pure freeze/unfreeze of the same scheduled nodes - no teardown, no re-prime. If the drift does exceed the guard, the anchor is corrected and'changed'is dispatched so the media players rebuild their queue against the new anchor (previously the anchor moved silently, leaving the frozen queue misaligned).@remotion/player: removed the now-unusedforceoption fromsetGlobalTimeAnchor.shared-audio-tags.tsx,set-global-time-anchor.tsanduse-playback.tsinto named types. No behavior change.Tradeoffs
Testing
tsgoclean oncore,playerandmedia;oxfmtandeslintclean on the changed files.bun test: 557 pass incore, 30 pass inplayer; themediabrowser suite passes (70 tests, 29 files).@remotion/media: Audio hiccups on rapid play/pause toggling in the Player #8984.