Recursive AI

ISO base media files (MP4, MOV, 3GP, fragmented MP4, CMAF)

All onemedia docs

How RecursiveAI.OneMedia.IsoBmff reads these files. Specifications: ISO/IEC 14496-12 (clause numbers in the code comments), the QuickTime File Format specification for the atom differences, 3GPP TS 26.244 for 3GP. Codec configuration boxes (avcC, hvcC, esds, …) are carried as opaque bytes; they are parsed by the codec layer.

Layers

LayerTypesNotes
Box structureBoxReader, BoxHeader, BoxNode, BoxTreeLazy tree of byte ranges. Children are read on first use. No payload bytes are kept.
Format modelFileTypeBox, MovieReader → IsoMovie, IsoTrack, SampleEntry, EditListEntry, TrackExtendsHeaders, edit lists, sample descriptions. Sample tables are located, not read.
Neutral demuxIsoBmffDemuxer (seekable and forward-only), IsoBmffAsyncDemuxer (ranged), IsoBmffFormatTracks, packets, seeking.

Box rules

  • 32-bit size, 64-bit largesize (size field 1), size 0 ("to the end of the enclosing range"), and uuid boxes with their 16-byte user type are read (§4.2). Unknown and vendor boxes are kept as ranges.
  • Containers: moov trak mdia minf stbl edts dinf mvex moof traf mfra udta tref sinf schi meta ilst stsd dref and the item boxes inside ilst. stsd and dref start with version, flags and an entry count (8 bytes) before their children. meta is a full box in ISO files and a plain container in QuickTime files; the reader tells them apart by looking for hdlr right after the header.
  • A QuickTime list may end with a 32-bit zero, and writers pad with zero bytes: reported as an Info defect (UnaccountedBytes), nothing more.

Defects

All carry the byte offset of the box (or entry) they concern. Reading continues where it safely can.

CodeKindMeaningWhat happens
isobmff.size-below-headerInvalidSizeDeclared size smaller than the headerThe box list ends there.
isobmff.size-hugeInvalidSize64-bit size above 2^63-1The box list ends there.
isobmff.truncatedSizeExceedsFileTop-level box runs past the end of the fileThe box is clamped to the file.
isobmff.size-exceeds-parentSizeExceedsParentNested box runs past its parentThe box is clamped to the parent.
—TruncatedFewer than 8 bytes left, a cut-off 64-bit size or uuid type, or a source that ends inside a boxThe list ends.
isobmff.no-moovMissingRequired (exception)No movie box in the fileMediaFormatException; TryOpen returns the defect instead.
isobmff.no-mvhd, isobmff.track-incomplete, isobmff.no-stblMissingRequiredA required box is missingA track without track header, media header or handler is skipped.
isobmff.table-count, isobmff.elst-count, isobmff.trun-countTruncatedA table declares more entries than its box holdsThe count is clamped to what the box holds.
isobmff.stts-short, isobmff.stsc-short, isobmff.stsc-order, isobmff.chunk-missingInconsistentSample tables disagree (for example the time table covers fewer samples than the size table)The track ends at the last sample the tables describe. Reported once per track.
isobmff.sample-past-eofTruncatedA sample's bytes lie beyond the end of the fileThe sample is not returned. Reported once per track.
isobmff.sample-limitLimitReachedA sample is larger than MaxPacketBytesThe sample is skipped.
isobmff.short-readTruncatedThe source returned fewer bytes than the sample sizeThe sample is skipped.
isobmff.traf-unknown-track, isobmff.traf-no-tfhdInconsistent, MissingRequiredA track fragment names a track the movie does not declare, or has no headerThe track fragment is skipped.
isobmff.forward-orderInvalidOrderForward-only source: a sample lies before the current positionThe sample is skipped.
isobmff.entry-largeLimitReachedA sample entry is larger than 1 MiBOnly its first mebibyte is read.
isobmff.limitLimitReachedDepth, element count, allocation or sample-table limitThe box list or fragment walk ends.

Limits used: MaxStructureDepth, MaxElementCount, MaxTracks, MaxSampleTableEntries, MaxAllocationBytes (the movie box is only read whole for ranged and forward-only sources; each movie fragment box is read whole), MaxPacketBytes, MaxTagBytes (the gapless tag), MaxDefects. Every table's declared entry count is checked against the box size and the limit before anything is read.

Time

Packet times are on the track's media timeline in the track's timescale, exact: Dts is the running sum of the decoding-time-to-sample deltas (stts) or the fragment's base decode time plus the running sum of run durations; Pts = Dts + composition offset. Composition offsets (ctts version 0 and 1, trun version 0 and 1) are read as signed 32-bit values; a version-0 value above 2^31 is not credible for a real file and old QuickTime writers store negative offsets that way. Duration is the table's duration for the sample.

Edit lists are not applied to packet times. They are reported in MediaTrack.Edit:

  • Edits: every entry (MediaStart unknown for an empty edit, Duration in the movie timescale);
  • StartTime: the total of leading empty edits (movie timescale);
  • Priming: the media time at which the first non-empty edit starts, in the track timescale (§8.6.6). For AAC this is the encoder delay the player skips. When the movie carries an iTunes iTunSMPB tag that parses (three hexadecimal fields: delay, padding, valid frames), it replaces Priming and EndPadding for the first audio track, because the tag states encoder delay and padding exactly where the edit list rounds;
  • EndPadding: for a single non-empty edit, the media after the end of the edit.

Presentation time on the edited timeline of a packet is Pts - Priming + StartTime (rescaled to a common scale). A packet that ends at or before the first edit's start (Pts + Duration <= media_time) carries PacketAttributes.Discard: it is decoded but never shown.

Sample tables without loading them

SampleCursor walks stts, ctts, stss, stsz/stz2, stsc, stco/co64, and sdtp together in sample order through 4 KiB windows. Memory is constant whatever the table length, and moving to the next sample costs no allocation. A seek positions each table directly (binary search in stss, run walk in stts/ctts/stsc, direct indexing in the fixed-size tables). Keyframe seeking: the sync sample at or before (or after) the target; the other tracks land on their last sync sample not after the primary track's landing time. The primary track is the first enabled video track, else the first enabled track.

Keyframes: samples listed in stss; every sample when stss is absent. Repeated stss entries are tolerated. Disposable: sdtp sample_is_depended_on == 2. Sample group boxes (sgpd, sbgp) are kept as ranges and do not change packet flags.

Packet order

Across the enabled tracks, the packet with the lowest source offset comes next. For interleaved files this is the order the data is stored in, so a plain read is sequential; for a file whose tracks are stored one after the other (all video, then all audio), it is the same storage order. Within a track packets are in decode order. A disabled track costs nothing: its cursor is never advanced and its payload is never read.

Payload reads go through a window that starts at 4 KiB and doubles (to 64 KiB) while reads continue where the last one ended. After a jump (over the chunks of a switched-off track, say) the first window is as long as the run that just ended, rounded up by an eighth and to 4 KiB, because the next chunk of an interleaved track is about as long as the last: a chunk then costs one read instead of a climb from 4 KiB. Samples over 32 KiB bypass the window.

Fragmented files

mvex/trex mark a file as fragmented. Packets of the movie box's own tables come first (usually none: empty_moov), then each moof in file order. A fragment is held in memory only while it is read. Defaults inherit per §8.8.7 and §8.8.8: tfhd, then trex; first_sample_flags override the first sample; the base data offset is the explicit base_data_offset, else the start of the moof (default-base-is-moof and the common case), else the end of the previous track fragment's data; a trun without data_offset continues after the previous run. A missing tfdt continues the track's previous decode time; for the first fragment that is the end of the samples the movie box itself describes (a hybrid file, zero for empty_moov). Non-sync samples have bit 16 of the sample flags set.

Seeking in a fragmented file whose movie box holds no samples picks the first enabled video track (else the first enabled track) and scans movie fragment headers (never media data) until the target is passed, then reloads the landing fragment. The scan starts from an index when there is one, so the cost is a few header reads instead of one per fragment:

  1. the movie fragment random access box (mfra, §8.8.9), found through the mfro box that closes the file: two reads at the first seek (16 bytes, then the box), used when its tfra for the track has an entry at or before the target;
  2. else the segment index boxes (sidx, §8.16.3) between moov and the first fragment, read at open, when they are not hierarchical.

The index only names where to start: the scan begins one entry before the best one, and an index is ignored (scan from the first fragment) if its offset is not a moof or that fragment does not state a tfdt for every track fragment, because the running decode times are unknown without the earlier fragments. A file with neither index, or only per-segment sidx boxes (CMAF/DASH live segments), pays the full header scan.

What a probe reads

  • Seekable file: the registry's first look at the file (one read of 4 KiB, to recognise it) is handed to the reader, which starts from those bytes instead of reading them again. After that the read-ahead window (32 KiB) serves the start of each top-level box on the path to moov (ftyp, then mdat is skipped by size, then moov), and the boxes inside moov except the table entries. With moov at the end this is one seek, not a scan of mdat.
  • A movie box of at most 128 KiB is then read once, whole, into a pooled buffer; it is parsed from there and the sample-table windows below read from it too, so a small file opens with a read for the head, one for the box header and one for the box. A larger movie box is read through the windows as before, so the buffer never grows with the file. Reads outside the box go to the file.
  • Table windows: one 4 KiB window per table when a packet cursor is created (at open); from the buffered movie box when there is one.
  • Fragmented file: moov, then the box headers up to the first moof or mdat, to find sidx (served by the same read-ahead window as moov). The total duration is mvhd, else mehd, else the sum of sidx durations; when none states it, Duration is unknown. Knowing it exactly would take one read per fragment (what ffprobe does), so the probe does not. The mfra box at the end is read only on the first seek.
  • Ranged sources (IsoBmffAsyncDemuxer): moov is fetched in one request and parsed from memory; each packet is one request, each fragment one request for its header box.

Measured numbers are in the unit report.

Forward-only sources

IsoBmffDemuxer.Open(IForwardMediaSource) reads boxes in order: moov must come before the media data (fragmented streams, faststart files); otherwise UnsupportedMediaException. After each moof the following mdat header is read so its end is known; samples are read by skipping to their offsets, which must not go backwards (isobmff.forward-order).

Sample entries

SampleEntry keeps the fixed fields that matter (dimensions, channels, bits, sample rate, QuickTime sound version 0–2) and every box after them in Extensions (QuickTime wave children are surfaced too). Configuration is the payload of the codec's configuration box, tagged with the box type: avcC, hvcC, vvcC, av1C, vpcC, esds (also for mp4v), dOps, dfLa, dac3, dec3, damr, alac; for text and metadata entries it is the bytes after the eight common entry bytes. Protected entries (encv, enca, …) report the clear format from sinf/frma as OriginalFormat and set IsProtected.

Codec ids: avc1/avc3 → h264, hvc1/hev1/dvh1/dvhe → hevc, vvc1/vvi1 → vvc, av01 → av1, vp08/vp09 → vp8/vp9, jpeg/mjpa/mjpb → mjpeg, mp4a and mp4v by the esds object type (0x40/0x66–0x68 → aac, 0x69/0x6B → mp3, 0x20 → mpeg4 (MPEG-4 part 2), 0x60–0x65 → mpeg2video, 0x6A → mpeg1video, 0xA5/0xA6 → ac3/eac3, 0xDD → vorbis), .mp3 and ms\0U → mp3, Opus, fLaC, ac-3, ec-3, samr, sawb, alac, the QuickTime PCM codes → pcm, tx3g, wvtt, stpp → ttml. Anything else maps to Unknown and keeps the code in MediaTrack.CodecTag.

Language: the media header's 15-bit packed ISO-639-2/T code, or, for values below 0x400, the QuickTime Macintosh language code mapped to ISO-639-2/T (0 = eng).

AMR-WB (sawb) carries the same damr setup box as AMR-NB (3GPP TS 26.244). Files whose sawb entry has no such box (observed in files written by ffmpeg, which MP4Box calls invalid) get an empty configuration.

Disposition: Default is set on the first enabled track of each kind, within its alternate group when it has one; Disabled marks tracks whose tkhd enabled flag is clear.

PCM audio (sowt, twos, in24, fl32, raw, ...) is stored one sample frame per table sample. The demuxer joins frames that follow each other in the file into packets of up to 1024 frames (the packet size ffprobe uses), so a minute of 48 kHz audio is 2,813 packets rather than 2.9 million. The first packet after a seek starts at the sample the seek landed on. MediaTrack.PacketCount is unknown (-1) for these tracks. Other codecs are never joined: QuickTime tracks that store a framed codec as single bytes (observed with MP3 in multitrack.mov, 17.6 million one-byte samples) are reported per table sample, because finding the frames is codec work.

A track may have several sample descriptions; IsoBmffDemuxer.SampleDescriptionIndex is the 1-based index for the packet last returned.

Differences from ffprobe -show_packets (7.0.2 and 8.0.1), each checked against the files

The comparison tool applies ffprobe's edit-list handling before comparing (it subtracts the first non-empty edit's start from every timestamp and adds leading empty edits) and lists the following as explained, never as agreement:

DifferenceWho follows the spec
Last packet's duration: ffprobe replaces it (parser frame length for MP3/AC-3/AAC, or the edit's end); the table says otherwiseThe table value is what the file stores; OneAudio and MP4Box's sample table agree with us.
Negative composition offsets (ctts v1, trun v1): ffprobe moves DTS earlier by the largest dts - pts for tables in moov and moves PTS later for fragments, so DTS never exceeds PTSWe keep Pts = Dts + offset as §8.6.1.3 defines it.
First fragment's durations that come from the tfhd default: ffprobe prints N/AThe box states them; we use them (§8.8.7).
First audio sample of a fragmented file whose run states 3528 for a 1024-sample frame (files written by one muxer): ffprobe shows N/A or the frame lengthThe run states it.
Edit lists with several segments (or one that cuts the media short): ffprobe applies them to its sample index, dropping the samples outside the segments and repeating those several segments use (a 106-segment video track lists 1,164 packets for 1,100 samples)Edit lists are reported in TrackEditInfo, not applied (see Time). Every packet ffprobe lists is one of ours with the same size and position.
Fragment samples whose stated duration is 0 (frag_every_frame writes it for the last sample it knows of): ffprobe reports the distance to the next sampleThe box states 0; so do we.
Video packets whose duration ffprobe takes from the video parser's frame timing (every duration a fixed multiple of the table's, e.g. 250 against 125 in one H.264 file)The table states the delta.
Data tracks (RTP hint, MPEG-4 systems): ffprobe marks every packet a key frameWe report the track's sync sample table.
A file with samples in the movie box and in fragments, plus an edit list: ffprobe places the movie box's samples on the edited timeline in its own wayTimestamps are the media timeline of the tables (no edit applied).
A sample that straddles the first edit's start or lies outside every edit segment: ffprobe flags it discardable and moves its timestampsWe flag only samples that end before the first edit's start.
Packets whose timeline ffprobe built from guessed durations (frag_every_frame files): repeated decode times and a discard flag on the repeatsTimes come from tfdt and the runs, exact.
A file cut off inside the media data: ffprobe still lists the sample that straddles the cutWe stop before it and record Truncated with the offset.
A decoding-time-to-sample delta far above the track's usual one (a gap in the timeline; 35 of 15,294 video samples in one Blender movie, with deltas of 50 to 600 ticks against the usual 25): ffprobe reports the usual duration for the packet while its DTS still advances by the deltaThe table states the delta; Duration is the delta (§8.6.1.2).
Discard on a fragment sample before the edit start: ffprobe flags it only for samples from moov's tablesThe edit list applies to fragments too (§8.6.6).
H.264/HEVC keyframes: ffprobe also marks IDR pictures that the sync sample table omitsWe use container information only (decode-free); MP4Box's report agrees with the table.
One-frame-per-sample PCM (sowt, twos): ffprobe regroups samples into ~1000-byte packetsWe return the sample table's samples; bytes covered and offsets are identical.
Edit lists that play media twice (a 106-segment video track in one file, an MP3 track in another): ffprobe lists the repeated samples again; it also splits PCM packets at edit boundariesOurs lists each stored sample once; packetset confirms every position and size of ours is among ffprobe's.
A fragmented file whose edit is [0+0] (segment_duration 0): ffprobe drops the first moov-table samples§8.6.6 reads a zero duration as the whole track in a fragmented file; we keep every sample.
QuickTime MP3 stored as one-byte table samples (multitrack.mov, 17.6 million of them): ffprobe reframes by MP3 frame headersOurs reports table samples (finding frames is codec work); bytes covered are the same.
A video track whose sample entry and tkhd say 240x180 while the H.263 picture header says 352x288 (0625rvt6-42.mov)Ours is the container's value; the picture header is payload, read only by tools that parse the stream.
Duration of fragmented files: ffprobe scans the fragments for the real length; mvhd of a fragmented file often states a shorter one or noneOurs stays structural (no scan on open). MediaFile.ContainerDuration is the box value, TracksDuration the longest media header duration; fragment totals are not summed.
Duration of files whose mvhd is shorter than a track (or whose mvhd timescale is wrong): ffprobe reports the longest streamMediaFile.Duration follows ffprobe (longest track), ContainerDuration keeps the box value.

Everything else (size, byte offset, key flag, durations, timestamps) matches on every packet of every compared file.