Skip to content

fix(core): Safari stops chopping audio by seeking it over and over during playback - #6854

Open
miguel-heygen wants to merge 3 commits into
mainfrom
fix/safari-audio-seek-loop
Open

miguel-heygen wants to merge 3 commits into
mainfrom
fix/safari-audio-seek-loop

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

In Safari and every WebKit, sounding audio is no longer seeked over and over while it plays. Since #6768, a played edit's audio was seeked about twice a second and stood still for most of the play, which is heard as chopped audio. Chrome is unchanged.

Why

WebKit reports a just-played or just-seeked <audio>'s start time for 150 to 400 ms before its output begins. #6768 made the playhead stop waiting for it, so every start leaves the audio that far behind the playhead. steerMedia seeks sounding audio whose lag holds above 100 ms, and in WebKit that seek starts the same delay again: the audio lands behind by the same amount, is seeked again, and so on for the whole play.

Before #6768 the playhead waited for the audio, which hid the loop most of the time but stopped the picture at cuts (and the loop still took hold in some plays, see below).

How

mediaSteer.ts keeps one more per-element fact: the runtime has just started or seeked this element and it has not yet moved in tolerance (within 40 ms of the playhead, with a settled offset). While that holds, its lag is its own output delay, so it is steered by rate (audio stays within 10%) instead of seeked, unless the lag passes MEDIA_HARD_SYNC_SECONDS (0.5 s): a second audio that stalls far behind is still seeked once. media.ts marks an element when the sync calls play() on it or seeks it; steerMedia marks it when it orders a seek. Audio that was in sync and then drifts past 100 ms is still seeked once. The clock is untouched, so #6768's moving playhead at cuts stays.

The WebKit playbench (webkit-stops.ts) now also reports audio seeking events per play and the share of playing-audio time an element's clock stood still for over 50 ms, and can drive Chromium (--browser chromium) for the comparison.

Compared with Remotion

How each handles a playing <audio> that is behind the playhead. Remotion read at v4.0.539, packages/core/src.

before #6768 main this PR Remotion v4.0.539
seeks sounding audio when its lag holds past 0.1 s 0.1 s 0.1 s, only if it had been in sync 0.65 s (use-media-playback.ts:34, "In Safari, amplified media can lag behind by around 0.4 seconds")
seeks again for the lag its own seek left yes yes, about twice a second in WebKit no no: never twice to the same target (get-media-sync-action.ts:105)
playhead after an audio start or seek waits up to 0.5 s keeps moving keeps moving keeps moving: its wait after a seek is for video only (buffer-until-first-frame.ts:33)
catches a lag up by playback rate up to 10% up to 10% up to 10% never: rate is only the authored rate (use-media-playback.ts:174-192)

Remotion's choice of tolerating the lag over steering it is relevant to the "Not proven" note below: in WebKit the residual standing-still share comes partly from steering audio at 1.10x.

Test plan

  • Unit test: mediaSteer.test.ts steers the lag a start left in audio, but still seeks one past hard sync (0.3 s steered, 0.8 s seeked).
  • Unit test: clock.safariAudio.test.ts plays one audio clip through the real TransportClock and syncRuntimeMedia for 10 s, with an element whose time stays at its start or seek target for 150 ms before it moves, as WebKit's does. It expects no seeks and the audio within 0.1 s of the playhead at the end.
  • fails without the fix: on main clock.safariAudio.test.ts sees 62 seeks in 10 s; with fix(core): the playhead keeps moving at a cut while the incoming take has not started #6768's clock change reverted it sees 0.
  • Passes 3 runs in a row. The core runtime suite passes (110 files, 2657 tests), including fix(core): the playhead keeps moving at a cut while the incoming take has not started #6768's clock tests and the existing "seeks audio that holds a lag past 100 ms" cases. oxfmt, oxlint, core tsc, fallow audit and the comment ratchet clean.
  • Manual testing: the playbench edits (synthetic 720x1280 media from make-media.sh) in Playwright WebKit 27.2 on macOS, 3 plays per side, playhead and audio sampled every frame:

10-take edit (53.08 s):

audio seeks per play sounding audio standing still playhead stops >50 ms
before #6768 78, 88, 18 72%, 69%, 10% 3, 3, 29
main 104, 135, 120 80%, 74%, 76% 8, 1, 0
this PR 11, 12, 14 14%, 14%, 15% 0, 0, 0

3-take edit (24.8 s):

audio seeks per play sounding audio standing still playhead stops >50 ms
before #6768 4, 42, 4 5%, 47%, 6% 6, 3, 6
main 69, 58, 66 67%, 75%, 70% 0, 0, 0
this PR 3, 2, 2 11%, 9%, 9% 0, 0, 0

Chrome 152 (headless shell, Linux), 10-take edit, 6 plays per side: 9 seeks (one per cut) and 0% audio standing still on both; playhead stops 0 in all 6 plays here, 0 to 19 on main in the same interleaved runs on a loaded machine.

Not proven

  • This is Playwright's WebKit, not Safari itself, and not an iPhone. A real Safari run needs "Allow remote automation" turned on.

  • What stays of the "standing still" share in WebKit is each take's own start delay plus 300 ms stretches while the audio is steered at 1.10x: WebKit's audio clock stands still at intervals at that rate. Whether those are heard is not measured. Starting audio early by its learned start delay, as video already is, would remove both and is a separate change.

  • Documentation updated (not applicable)

  • Comments follow CONTRIBUTING.md "Comments"

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 1 important · 0 nits · head c33989f8

The fix itself looks right. I ran it locally: clock.safariAudio.test.ts sees 62 seeks with main's media.ts/mediaSteer.ts and passes with this PR, and the core runtime suite passes (110 files, 2656 tests). The fails without the fix: line is satisfied.

  • blocker: Preflight (lint + format) fails on every workflow: oxfmt --check flags packages/core/src/runtime/clock.safariAudio.test.ts:72. The body says oxfmt is clean. Running oxfmt on that file wraps the attachAudioSource line, and that's the whole fix.
  • important: see the inline comment on mediaSteer.ts:35.
Scope

Read end to end: mediaSteer.ts, the syncRuntimeMedia drift/seek/play block in media.ts, clock.ts now() and _elementTimeNeverBehind, and the new test. I did not review webkit-stops.ts (playbench only) and did not re-run the WebKit numbers. No prior reviews.

Comment thread packages/core/src/runtime/mediaSteer.ts Resolved
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 129 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-pct-r0-nested-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@miguel-heygen
miguel-heygen force-pushed the fix/safari-audio-seek-loop branch from c33989f to 5071aa3 Compare October 30, 2026 16:28

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 2 resolved, 0 open, 0 new · c33989f → 5071aa3. Hold: do not merge until the issue is reproduced in real Safari (Miguel).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant