Skip to content

feat: demux progressive (non-fragmented) MP4#4

Merged
dooly123 merged 4 commits into
BasisVR:mainfrom
towneh:feat/progressive-mp4
Jul 8, 2026
Merged

feat: demux progressive (non-fragmented) MP4#4
dooly123 merged 4 commits into
BasisVR:mainfrom
towneh:feat/progressive-mp4

Conversation

@towneh

@towneh towneh commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1 (the fMP4/CMAF interleave fix) — same file, and this builds on its emission model. The first two commits here are #1; review the top commit. Once #1 merges this rebases to a single-commit PR.

Summary

A classic progressive MP4ftyp moov mdat with sample tables and no moof fragments, i.e. the most common recorded-file shape on the internet — played nothing. The demuxer was fragmented-MP4-only: it announced the tracks from the moov, then found no fragment runs to slice the mdat with, and emitted zero samples with no error. Symptom: silent black screen.

This adds classic sample-table demuxing:

  • The moov's stts/ctts/stsc/stco/co64/stsz/stss tables are parsed per track (an fMP4 init segment carries these boxes with zero entries, which leaves them inert — the fragmented path is untouched).
  • The mdat is walked as a forward stream, merging both tracks in file-offset order — the muxer's own chunk interleave. The box loop now reads headers first and streams a progressive mdat sample-by-sample instead of buffering whole boxes, so file size is no longer bounded by the 256 MB box cap; fragment mdats keep the buffered path.
  • Faststart layout (moov before mdat) is required — a one-way byte source can't index media that has already streamed past. A trailing-moov file now surfaces a clear error ("remux with faststart") instead of silence.

Verification

Offline A/B against the fragmented-only demuxer (same closed-loop harness as #1):

  • The real progressive recording that surfaced the bug: 0 video / 0 audio before → all 28 video + 42 audio samples after, timestamps matching the file.
  • A generated 12 s faststart H.264+AAC file: fully demuxed (360 video / 564 audio), worst A/V delivery-order gap 233 ms — the muxer's chunk interleave, inside the paced clock's audio floor.
  • A non-faststart file produces the remux-guidance error exactly.
  • fMP4 regression: byte-identical verbose output before/after on the CMAF HLS fixture and on a fragmented video-only stream.
  • In-editor (Windows, desktop): a real ~100 MB CDN-hosted progressive recording (289 KB moov, faststart) plays end to end with no issues — auto-detected as on-demand (Content-Length + Accept-Ranges) and paced at 1×. Pre-fix, the same URL produced a silent black screen. The streaming walk means the ~100 MB mdat never gets buffered whole.

One serving note that surfaced during testing: a finite file served without Accept-Ranges classifies as live and plays unpaced (fast-forward) — that's the delivery auto-detect working as documented, not this demuxer; real CDNs send the header.

Both binaries rebuilt and committed (win-x64 DLL, arm64-v8a .so).

Notes

  • Duration and seek fall out of these same tables naturally (stts totals = exact duration; per-sample offsets = a seek index) — deliberately not wired here, since exposing them needs new engine ABI; this PR is playback only.
  • stz2 (compact sample sizes) isn't handled — rare in practice; such a file keeps the old behaviour.

towneh added 4 commits July 7, 2026 20:56
HLS with fMP4/CMAF segments loaded a single video frame and played no
audio. The fMP4 demuxer emitted a fragment track-major — every video
sample of a moof, then every audio sample — and with delivery pacing on
(always, for HLS) the paced video burst spans the whole fragment
duration in wall time, so the fragment audio behind it arrives a full
fragment late. The decoder trims that audio as stale and its
audio-gated presentation clock never starts: one frame, silence.

Consume a moof mdat as a decode-time-ordered merge across its trun runs
instead, interleaving the tracks at frame granularity the way the
MPEG-TS demuxer's output naturally is. Decode order within each run is
preserved; single-track fragments (split-stream fMP4 legs) are
delivered byte-for-byte as before.

Verified against ffmpeg-generated CMAF (H.264+AAC, 2s fragments): worst
delivery-order PTS gap between tracks drops from ~2.1s (the fragment
duration) to ~190ms (B-frame reorder depth), same access-unit counts
and timestamps; a video-only fMP4 stream demuxes identically before and
after. Rebuilt basis_media_native.dll (win-x64) and
libbasis_media_native.so (arm64-v8a).
basis_avcc_to_annexb is bounds-checked, so an output larger than the
buffer fails the conversion rather than overrunning — but with a 1-3
byte NAL length prefix, an access unit made of many small NALs could
outgrow the fixed 64-byte headroom (each NAL trades its length prefix
for a 4-byte start code) and be dropped. Size the buffer from the
worst-case growth instead: a NAL is at least nal_len_size + 1 bytes, so
growth is bounded by (4 - nal_len_size) * (size / (nal_len_size + 1)).
For the common 4-byte prefix this stays exactly size + 64.

Rebuilt basis_media_native.dll (win-x64) and libbasis_media_native.so
(arm64-v8a).
A classic progressive MP4 (ftyp moov mdat with sample tables, no moof
fragments; the most common recorded-file shape) played nothing: the
demuxer was fragmented-MP4-only, so it announced the tracks from the
moov and then found no trun runs to slice the mdat with. No error
surfaced; the symptom was a silent black screen.

Parse the classic sample tables (stts/ctts/stsc/stco/co64/stsz/stss)
and walk the mdat as a forward stream, merging both tracks in
file-offset order (the muxer's chunk interleave). The box loop now
reads headers first and streams a progressive mdat sample-by-sample
instead of buffering whole boxes, so file size is no longer bounded by
the 256 MB box cap; fragment mdats keep the buffered path. Faststart
layout (moov before mdat) is required by a one-way byte source; a
trailing-moov file now reports a clear remux-with-faststart error
instead of silence.

Verified offline against the fragmented-only demuxer: the progressive
recording that surfaced the bug goes from zero output to all 28 video +
42 audio samples; a generated 12 s faststart file demuxes fully with a
worst A/V delivery-order gap of 233 ms (inside the paced clock's 250 ms
audio floor); fMP4 output (CMAF HLS fixture and a fragmented video-only
stream) is byte-identical before and after; a non-faststart file
produces the remux error. Rebuilt basis_media_native.dll (win-x64) and
libbasis_media_native.so (arm64-v8a).
A malformed (or crafted) stbl repeating stts/ctts/stsc/stss/stsz/stco/co64 overwrote the already-parsed table pointer, leaking the earlier allocation. First box of each kind now wins; the stsz guard keys on sample_count so the constant-size form (which allocates nothing) is covered too. Behaviour on well-formed files is unchanged - harness output is byte-identical on the progressive and CMAF fixtures. Rebuilt both binaries.
@dooly123 dooly123 merged commit 981d412 into BasisVR:main Jul 8, 2026
@towneh towneh deleted the feat/progressive-mp4 branch July 8, 2026 19:23
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.

2 participants