Skip to content

fix(web-ui): clamp non-monotonic video sample duration to survive spliced catchup streams#525

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

fix(web-ui): clamp non-monotonic video sample duration to survive spliced catchup streams#525
stackia merged 2 commits into
mainfrom
cursor/a431919f

Conversation

@stackia

@stackia stackia commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes Web Player 回看失败(开发人员工具中有数据流、无画面和声音) #448: telco catchup recordings (e.g. Hunan/Jiangsu Telecom) splice segments with overlapping timestamps. A mid-batch video dts regression produced a negative sample duration, which underflowed to ~2^32 ms in the trun box, corrupted the MSE buffered range (0.00 – 4294971.67s observed) and killed playback with a decode error → black screen after 3 retries.
  • _remuxVideo now clamps non-positive sample durations to the reference frame duration and re-anchors the remaining samples of the batch via dtsCorrection — mirroring the existing inter-batch re-anchoring and the audio track's monotonicity enforcement. One integer comparison per sample, no extra parsing.
  • Also clears aac_last_incomplete_data_ after consumption (ADTS + LOAS) so a fully-parsed stale buffer is not prepended again on the next PES payload.

Verification

A/B test with an ffmpeg-crafted spliced TS (H.264 + AAC, 2 s timestamp regression at the 6 s splice point), played through createPlayer in Chrome:

buffered range result
before 0.00 – 4294971.67 (corrupted) playhead enters no-data zone
after 0.00 – 11.96 (single sane range) plays through to the end

A normal continuous TS stream plays unchanged (regression check). tsc --noEmit and biome check pass.

Test plan

  • Verify against a real Hunan/Jiangsu Telecom catchup stream (no test stream available locally)
  • Synthetic spliced TS with 2 s dts regression plays without MediaError
  • Normal continuous TS stream regression check
  • type-check + lint

Made with Cursor using Fable 5

stackia and others added 2 commits June 12, 2026 18:29
…iced catchup streams

Telco catchup recordings (e.g. Hunan/Jiangsu Telecom) splice segments with
overlapping timestamps. A mid-batch dts regression produced a negative video
sample duration, which underflowed to ~2^32 ms in the trun box, corrupted the
MSE buffered range and killed playback with a decode error.

Clamp non-positive durations to the reference frame duration and re-anchor the
remaining samples of the batch, mirroring the existing inter-batch dtsCorrection
and the audio track's monotonicity enforcement.

Also clear aac_last_incomplete_data_ after consumption so a fully-parsed stale
buffer is not prepended again on the next PES payload.

Fixes #448

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@stackia stackia marked this pull request as ready for review June 12, 2026 10:32
@github-actions

Copy link
Copy Markdown
Contributor

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

@stackia stackia merged commit 997ec08 into main Jun 12, 2026
10 checks passed
@stackia stackia deleted the cursor/a431919f branch June 12, 2026 10:34
stackia added a commit that referenced this pull request Jun 12, 2026
…eams (#528)

## 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

- [x] Normal H.264+AAC TS stream: audio SourceBuffer created, audio
decodes
- [x] Spliced catchup stream (#448 scenario): plays through with audio
- [x] type-check + biome
- [ ] Smoke test on Safari / Firefox / iOS (ManagedMediaSource pre-open
path already batches via pendingSourceBufferInit)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

Web Player 回看失败(开发人员工具中有数据流、无画面和声音)

1 participant