Skip to content

fix(audio): rebuild the resampler when a frame's input format changes mid-stream - #453

Merged
superuser404notfound merged 1 commit into
superuser404notfound:mainfrom
tschuegy:fix/audio-swr-mid-stream-format-change
Aug 30, 2026
Merged

fix(audio): rebuild the resampler when a frame's input format changes mid-stream#453
superuser404notfound merged 1 commit into
superuser404notfound:mainfrom
tschuegy:fix/audio-swr-mid-stream-format-change

Conversation

@tschuegy

Copy link
Copy Markdown
Contributor

Fixes #452.

AudioDecoder builds its SwrContext lazily from the first decoded frame and never revisits it. A live splice that swaps the stream's audio configuration mid-session (5.1 ↔ stereo at a program boundary is routine on European broadcast TS) leaves the context configured for the first frame's layout; a context built for more input planes than the frame carries reads a NULL plane inside swr_convert and crashes the demux thread. Full fault analysis (disassembly, registers, KSCrash capture) is in the issue.

The fix: record the input parameters the context was built for (ch_layout copy, format, rate) and compare each received frame against them in decode() and drain(). On a change:

  1. emitPending() first — the accumulator's bytes are in the OLD output format and must be neither dropped nor mixed with the new format's;
  2. swr_free + re-run the existing initResamplerFromFrame(_:), which also rebuilds the CMAudioFormatDescription so the renderer sees the new ASBD on subsequent buffers;
  3. reset the gapless AudioClockAnchor — its emitted-sample count is denominated in the old rate; the next buffer re-anchors from its own container PTS.

A failed rebuild leaves swrContext nil and skips the frame; the next frame retries, mirroring the lazy-init failure mode. The layout compare only runs when the frame carries a valid layout, so an UNSPEC stream that fell back to the synthesised default at init does not rebuild per frame. A rate/format switch that previously resampled garbage silently (an HE-AAC first frame at the core rate, say) now rebuilds too.

Field-verified on device (Apple TV 4K 3rd gen, tvOS 26.6, this branch as a local override on 6.56.5): tuned the crashing channel, hit a real on-air AC-3 5.1 → stereo splice two minutes in —

21:50:11.110 [AudioDecoder] Resampler ready: 48000Hz, 6ch, inFmt=8
21:51:51.735 [AudioDecoder] input format changed mid-stream (48000Hz/6ch/fmt=8 -> 48000Hz/2ch/fmt=8); rebuilding resampler
21:51:51.735 [AudioDecoder] Resampler ready: 48000Hz, 2ch, inFmt=8

One detection, no flapping; the SW diag line holds dclk=1.00 / status=rendering straight through the seam, playback continues cleanly. On 6.56.5 this exact moment is the crash from the issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HewBbYmNEut2WvoEHmeBRU

… mid-stream

A live splice that swaps the stream's audio configuration (5.1 <-> stereo
at a program boundary) leaves the SwrContext configured for the first
frame's layout; a context built for more input planes than the frame
carries reads a NULL plane inside swr_convert and crashes the demux
thread (superuser404notfound#452).

Record the input parameters the context was built for and compare each
received frame against them in decode() and drain(). On a change, emit
the accumulator first (its bytes are in the old output format), reset
the gapless clock (its sample count is denominated in the old rate),
and rebuild the resampler and format description from the new frame. A
rate or format switch that previously resampled garbage silently (an
HE-AAC first frame at the core rate, say) now rebuilds too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HewBbYmNEut2WvoEHmeBRU
@superuser404notfound
superuser404notfound merged commit 654c69e into superuser404notfound:main Aug 30, 2026
4 checks passed
superuser404notfound added a commit that referenced this pull request Aug 30, 2026
…very site that keeps one

#453 closed the crash on the SW path: a live TS splice from 5.1 to stereo left
AudioDecoder's SwrContext reading six planes from a frame that carries two, and
swr_convert faulted on the first NULL one. This carries that rule to the rest of
the surface it belongs to.

The compare and the plane check now live in one place, ResamplerInputParameters,
with unit tests around the cases the crash walked through: the reported splice,
a rate change, a format change, a layout change at the same channel count, and a
frame that states no layout (which must not rebuild the synthesised default on
every frame). A layout comparison that cannot decide counts as changed, matching
AudioBridge, because carrying on with a stale context is the fault being fixed.

AudioTapDecoder had the same gap and is fed from the same live sources. Its
output is pinned to mono 48 kHz, so only the input can move and nothing has to be
flushed across the seam; it now rebuilds on the same signal.

Both decoders also refuse a frame that is missing a plane the context would read.
The compare covers a DECLARED change; corrupt live MPEG-TS decodes to
nb_samples > 0 with a NULL plane and declares nothing, which is the same
dereference by another route. AudioBridge already guarded that case on the
loopback path.

Full suite green (2382 tests, 606 XCTest).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbciAqCSxpaiUpTumrCuA9
@superuser404notfound

Copy link
Copy Markdown
Owner

Merged as 654c69e4. Thanks for this one: the fault analysis was complete enough to review the fix against, and the field capture of the actual on-air splice is what a report like this usually never gets.

Reviewed and run against the full suite here before the merge (2374 tests, green), then hardened in 3c33907a on top:

  • The compare and the plane check moved into one type, ResamplerInputParameters, with unit tests around the cases the crash walked through: the reported 5.1 to stereo splice, a rate change, a format change, a layout change at the same channel count, and a frame that states no layout (which must not rebuild the synthesised default on every frame).
  • The layout comparison now treats an undecidable result (a negative AVERROR, one of the layouts invalid) as changed rather than unchanged, matching what AudioBridge already does. av_channel_layout_compare never returns negative in the FFmpeg we ship, so this is contract, not behaviour, but continuing on a stale context is the exact fault being fixed.
  • AudioTapDecoder had the same gap and is fed from the same live sources, so it rebuilds on the same signal now. Its output is pinned to mono 48 kHz, so nothing has to be flushed across the seam.
  • Both decoders also refuse a frame that is missing a plane the context would read. Your compare covers a declared change; corrupt live MPEG-TS decodes to nb_samples > 0 with a NULL plane and declares nothing, which is the same dereference by another route (AudioBridge already guarded that case on the loopback path).

Full suite green after the follow-up (2382 tests). A release carrying both goes out shortly.

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

Labels

None yet

Projects

None yet

2 participants