Skip to content

A video sample whose NAL chain overruns it is cut, not handed to Apple's parser (AE#561) - #581

Merged
superuser404notfound merged 1 commit into
mainfrom
fix/nal-chain-sanitizer
Sep 20, 2026
Merged

superuser404notfound merged 1 commit into
mainfrom
fix/nal-chain-sanitizer

Conversation

@superuser404notfound

Copy link
Copy Markdown
Owner

The report

7.8.1 (the segment cut on the container's own axis, #578) did not fix 1096bimu's Blu-ray remux. The reporter then did the decisive work: clipped the failing region, found that an MKVToolNix remux of that clip plays although no track changed, and produced a minimal 0.667 s MKV with one damaged packet, no audio and no subtitles.

What it is

A sample in an mp4 video track is a run of NAL units, each introduced by a big-endian length of the width the avcC / hvcC record declares, and a parser walks that run by addition. That packet's sixth length field declares 384137139 bytes with 350873 left in the packet.

The two consumers answer that differently, which is the whole shape of the report:

  • libavcodec logs Invalid NAL unit size (384137139 > 350873), skips the frame and plays on, so the file plays in mpv.
  • Apple's fMP4 parser answers the whole SEGMENT with CoreMediaErrorDomain -19602, which ends the session, and the stage-2 reload dies on the same segment.
  • MKVToolNix drops the unparsable tail on remux, which is why remuxing fixes it.

That also explains "specific places all through the film, not evenly spaced": each is a damaged sample, and it kills the session at whichever position AVPlayer first has to decode across one.

The fix

The session muxer walks each video sample's chain before the write and cuts it at its last complete NAL, which is the same bytes the remux would have written. A healthy sample is left alone, an Annex B payload is refused rather than walked as lengths, and a sample with no complete unit at all is dropped instead of written empty. The prefix width comes out of the configuration record rather than being assumed to be four, so 1, 2 and 4 byte framing and H.264 are covered along with HEVC.

Measured

Scripts/nal-overrun-fixture.py rewrites ONE length field of an 8 s HEVC encode, so the twin differs in four bytes and the healthy source is a true control arm:

healthy damaged
before VERDICT: OK -19602 at once
after VERDICT: OK, nothing cut VERDICT: OK, one cut logged

On the reporter's own clip: -19602 at once before, plays to the end after, cutting 505491 bytes to 154614, which is byte for byte what MKVToolNix wrote.

Full suite: 3209 tests in 438 suites, green. Nine new unit tests pin the walk itself (healthy run untouched, overrun, trailing stub, zero length, nothing complete, Annex B in both start-code widths, prefix width, and the width read out of hvcC byte 21 / avcC byte 4).

Not covered here

The session dying outright on one unparsable segment, with the reload landing on the same segment, is a second weakness this defect exposed. It does not arise once the sample is cut, so it is left alone here.

🤖 Generated with Claude Code

https://claude.ai/code/session_015PM3xUJB6ZQyqnmGK1fp6F

…n (AE#561)

A sample in an mp4 video track is a run of NAL units introduced by a big-endian
length, and a parser walks that run by addition. A damaged source can carry a
length that reaches past the end of its own sample (a reporter's Blu-ray remux
declared 384137139 bytes with 350873 left in the packet), and the two consumers
answer that differently: libavcodec logs "Invalid NAL unit size", skips the frame
and plays on, while Apple's fMP4 parser answers the whole SEGMENT with
CoreMediaErrorDomain -19602, which ends the session and every reload onto that
segment. Such a file plays in mpv, plays after an MKVToolNix remux (which drops
the unparsable tail), and died here at whichever position AVPlayer first had to
decode across the damaged sample.

The session muxer now walks each video sample's chain before the write and cuts
it at its last complete NAL, which is the same bytes the remux would have
written. A healthy sample is left alone, an Annex B payload is refused rather
than walked as lengths, and a sample with no complete unit at all is dropped
instead of written empty. The prefix width comes out of the avcC / hvcC record
rather than being assumed to be four.

Measured four ways on an 8 s HEVC fixture pair that differs in four bytes
(Scripts/nal-overrun-fixture.py rewrites one length field, so the healthy source
is a true control arm):

  healthy before  VERDICT: OK        healthy after  VERDICT: OK, no cut logged
  damaged before  -19602 at once     damaged after  VERDICT: OK, one cut logged

On the reporter's own 0.667 s clip, which carries one damaged sample and neither
audio nor subtitles: -19602 at once before, plays to the end after, cutting
505491 bytes to 154614, which is byte for byte what MKVToolNix wrote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015PM3xUJB6ZQyqnmGK1fp6F
@superuser404notfound
superuser404notfound merged commit 4fc2b72 into main Sep 20, 2026
7 checks passed
@superuser404notfound
superuser404notfound deleted the fix/nal-chain-sanitizer branch September 20, 2026 20:38
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