Skip to content

fix(web-ui): batch init segment appends to fix silent audio on TS streams#528

Merged
stackia merged 2 commits into
mainfrom
cursor/a431919f
Jun 12, 2026
Merged

fix(web-ui): batch init segment appends to fix silent audio on TS streams#528
stackia merged 2 commits into
mainfrom
cursor/a431919f

Conversation

@stackia

@stackia stackia commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the addSourceBuffer race that makes audio+video MPEG-TS streams play without sound on Chrome (reproduced 100% locally; likely behind the "CCTV 4K 无声音" report in #448).

Root cause

The race dates back to the mpegts.js fork 3.0 rewrite (Feb 2026), which moved transmuxing permanently into a Worker with one postMessage per event. Upstream mpegts.js ran the demux→remux→MSE chain synchronously on the main thread, so both addSourceBuffer calls always happened in a single event-loop task and the race could not occur.

With the Worker architecture, each init segment arrives in its own task. The player appended the video init segment as soon as its message arrived, then yielded; the media engine finished parsing it and locked the SourceBuffer set. When the audio init-segment message arrived in the next task, addSourceBuffer threw QuotaExceededError ("This MediaSource has reached the limit of SourceBuffer objects"), and the audio track was silently dropped.

Instrumented timeline before the fix (Chrome):

t=43ms addSourceBuffer(video/mp4; avc1.42c01e)  ok
t=43ms appendBuffer(video init, 659 B)          ok
t=44ms addSourceBuffer(audio/mp4; mp4a.40.2)    QuotaExceededError

Fix

  • The player now stashes init-segment messages and flushes them together right before the first non-init message, creating all SourceBuffers in a single task — restoring the invariant upstream had implicitly. The MSE buffer-append algorithm runs as a queued task, so no init segment parse can complete between the two addSourceBuffer calls.
  • Mid-stream codec changes now use SourceBuffer.changeType() instead of addSourceBuffer(), which always throws for an existing track once the media engine has initialized.

Notes / known limitation

  • Startup latency impact is negligible: the first media-segment message follows the init segments within the same worker parse round, and playback cannot start without media data anyway.
  • The pre-open (pendingSourceBufferInit) and ManagedMediaSource paths already batched init appends and are unchanged.
  • A stream whose audio PES starts only after video media segments have been flowing (PMT advertises audio but data arrives seconds late) would still hit the same error — that exotic case was equally broken before this fix and would need PMT-based track pre-announcement to solve; left out to keep the change minimal.

Verification

Same instrumented harness after the fix — both SourceBuffers created in one task, zero player errors, and webkitAudioDecodedByteCount confirms audio is actually decoding:

t=55ms addSourceBuffer(video)   ok
t=55ms appendBuffer(video init) ok
t=55ms addSourceBuffer(audio)   ok
t=55ms appendBuffer(audio init) ok
audioDecoded: 96523 bytes (was 0)

Also verified against the spliced catchup stream from #525: plays to the end with audio, single sane buffered range.

Test plan

stackia and others added 2 commits June 13, 2026 07:09
…eation from racing media engine init

Each worker message is delivered in its own event-loop task. Appending the
video init segment and then yielding lets the UA finish parsing it and lock
the SourceBuffer set, so the audio addSourceBuffer that arrives in the next
task throws QuotaExceededError ("reached the limit of SourceBuffer objects")
and the stream plays without sound. Reproduced 100% on Chrome with any
TS audio+video stream since the worker-based pipeline (#524).

The player now stashes init-segment messages and flushes them together right
before the first non-init message, creating all SourceBuffers in a single
task — the buffer append algorithm runs as a queued task, so no init segment
parse can complete in between.

Also switch mid-stream codec changes to SourceBuffer.changeType(): calling
addSourceBuffer for an existing track always throws once the media engine
has initialized.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

Azure Static Web Apps: Your stage site is ready! Visit it here: https://thankful-water-0a297bf00-528.eastasia.1.azurestaticapps.net

@stackia stackia marked this pull request as ready for review June 12, 2026 23:18
@stackia stackia merged commit 4058c77 into main Jun 12, 2026
10 checks passed
@stackia stackia deleted the cursor/a431919f branch June 12, 2026 23:18
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