Skip to content

fix(mp4): only read a sound sample description from an audio track - #2692

Open
dangrier wants to merge 1 commit into
Borewit:masterfrom
dangrier:fix/stsd-sample-description-handler-dispatch
Open

fix(mp4): only read a sound sample description from an audio track#2692
dangrier wants to merge 1 commit into
Borewit:masterfrom
dangrier:fix/stsd-sample-description-handler-dispatch

Conversation

@dangrier

@dangrier dangrier commented Aug 3, 2026

Copy link
Copy Markdown

Resolves #2690.

Fixes the RangeError: Offset is outside the bounds of the DataView thrown while parsing an MP4 stsd box, and the type confusion underneath it.

Independent of #2693, which fixes a separate defect in the same box; either can merge first.

The defect

Sample entries in a sample description box are track-type specific, selected by the handler type of the enclosing track. ISO/IEC 14496-12, 8.5.2 states this as a literal switch:

aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type)
extends FullBox('stsd', 0, 0){
  unsigned int(32) entry_count;
  for (i = 1 ; i <= entry_count ; i++){
    switch (handler_type){
      case 'soun': AudioSampleEntry();     break;
      case 'vide': VisualSampleEntry();    break;
      case 'hint': HintSampleEntry();      break;
      case 'meta': MetadataSampleEntry();  break;
    }
  }
}

MP4Parser implements no such switch — it applied parseSoundSampleDescription to every entry unconditionally. Two consequences follow.

1. A crash on short entries. A MetaDataSampleEntry is extends SampleEntry (codingname) { } — an empty body, so it may be no larger than the 16-byte SampleEntry base. A sound sample description needs 20 bytes beyond that base (SoundSampleDescriptionVersion.len 8 + SoundSampleDescriptionV0.len 12), so reading one overruns:

descrLen Result on master
0 OK — the only case #2340 fixed
1–7 Throws in SoundSampleDescriptionVersion (needs 8)
8–19 Throws in SoundSampleDescriptionV0 (12 bytes at offset 8 → needs 20)
≥20 OK

That is a declared entry size of 17–35 bytes, with two distinct overrun points. #2338 reported this crash and #2340 fixed only the empty-description case — its test is named "Handle empty sample entry description". It reproduces on ordinary DJI drone footage, which writes djmd (telemetry) and dbgi (debug) metadata tracks with 20-byte sample entries.

2. Silent misclassification of larger entries. Entries of 36 bytes or more never threw, but were still decoded as audio. This is visible in this repository's own corpus — test/samples/mp4/Mr. Pickles S02E07 My Dear Boy.mp4 has an avc1 video track that master reports as an audio stream:

BEFORE                                    AFTER
MPEG-4/AAC   AUDIO 2ch 16bit 48000Hz      MPEG-4/AAC   AUDIO 2ch 16bit 48000Hz
<avc1>       AUDIO 0ch 0bit 1916.1076Hz   <avc1>       (not audio)
AC-3         AUDIO 2ch 16bit 48000Hz      AC-3         AUDIO 2ch 16bit 48000Hz
CEA-608      (not audio)                  CEA-608      (not audio)

1916.1076 Hz is not a sampling frequency — it is the video's resolution. SoundSampleDescriptionV0 computes sampleRate as UINT16_BE(off + 8) + UINT16_BE(off + 10) / 10000, which lands on entry offsets 32 and 34. In a VisualSampleEntry those are width and height. That track is 1916 × 1076, giving 1916 + 1076/10000.

The overlay also explains why the parser did not bail out: entry offset 16 is pre_defined = 0 in a VisualSampleEntry, which decodes as version = 0 and satisfies version === 0 || version === 1. Offsets 24 and 26, read as channel count and sample size, fall inside unsigned int(32)[3] pre_defined = 0, hence the 0ch 0bit.

The change

Audio properties are now derived only from a track whose handler is soun (or audi).

  • The handler is resolved during post-processing, so no box order is assumed. 8.5.2 notes that "readers should be prepared to accept any box order", and hdlr sits in mdia while stsd is deeper in minf > stbl — gating at parse time would have relied on an ordering the specification does not guarantee.
  • The entry table is still built for every track, so dataFormat continues to supply trackInfo.codecName and format.codec for non-audio tracks. Skipping non-audio tracks outright would have regressed codec reporting.
  • Where no hdlr is present, the previous sample-description heuristic still applies, so those files behave as before.
  • Both reads are additionally bounds-checked against the description length, so a genuinely truncated audio entry is skipped with a debug() warning rather than throwing.

Tests

test/test-file-mp4.ts gains a Sample Description (stsd) atom suite that builds its fixtures in memory rather than adding binary samples, as the existing TrackHeaderAtom tests do.

Metadata entries of 16, 18, 24, 34 and 36 bytes all parse; a metadata track contributes no audio properties even when its bytes are filled with plausible audio values; a soun track still yields them; a non-audio track still reports its data format; and a trak with hdlr declared after minf behaves identically, covering the box-order requirement.

The five cases covering the defect were confirmed to fail against master before the fix; the remaining four passed throughout as controls, showing the fixtures themselves are valid.

test/test-trackinfo.ts is amended: the expectation for Mr. Pickles S02E07 My Dear Boy.mp4 asserted the avc1 track was TrackType.audio with 0 channels and 1916.1076 Hz. That expectation recorded the defect, so it is updated to expect a codec name only.

Verification

yarn test — 595 passing, 1 pending, 0 failing. yarn typecheck clean. yarn lint:ts exits 0 (7 warnings, all pre-existing on master, none in the changed files).

Not run: yarn build and yarn lint:md.

Sample entries in a sample description box are track-type specific,
selected by the handler type of the enclosing track (ISO/IEC 14496-12,
8.5.2): 'soun' holds an AudioSampleEntry, 'vide' a VisualSampleEntry and
'meta' a MetaDataSampleEntry.

Every entry was read as a sound sample description regardless of handler
type. A MetaDataSampleEntry may be no larger than the 16-byte SampleEntry
base, whereas a sound sample description needs 20 bytes beyond it, so
reading one threw:

  RangeError: Offset is outside the bounds of the DataView

for a declared entry size of 17 to 35 bytes, in either
SoundSampleDescriptionVersion (needs 8 bytes) or SoundSampleDescriptionV0
(a further 12 at offset 8). This completes the partial fix in Borewit#2340,
which handled only an absent description.

Larger non-audio entries did not throw, but were still misread: a video
track was reported as an audio stream, and could be selected as the track
supplying format.sampleRate, bitsPerSample and numberOfChannels. The
expectation for 'Mr. Pickles S02E07 My Dear Boy.mp4' recorded that
behaviour, asserting the avc1 track was audio with 0 channels and a
sampling frequency of 1916.1076 Hz, and is updated accordingly.

Audio properties are now derived only from a track whose handler is
'soun'. The handler is resolved during post-processing, so no box order
is assumed; readers are required to accept any order. Where no handler
box is present, the previous sample-description heuristic still applies.
The data format of every entry is still reported, so codec names for
non-audio tracks are unaffected.

Both reads are additionally bounds-checked against the description
length, so a truncated audio entry is skipped rather than throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread test/test-trackinfo.ts
Comment on lines -259 to -265
audio: {
bitDepth: 0,
channels: 0,
samplingFrequency: 1916.1076
},
codecName: '<avc1>',
type: TrackType.audio

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fixture actually demonstrates the bug

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.

MP4: sample entries are read as sound sample descriptions regardless of handler type

1 participant