# onemedia docs, all pages (OneMedia 0.1.0-preview.1) Every page of https://recursiveai.org/onemedia/docs/ and https://recursiveai.org/onemedia/cookbook/ as markdown, getting started first. Index: https://recursiveai.org/onemedia/llms.txt # Getting started with OneMedia Install OneMedia 0.1.0-preview.1 for .NET 10, open an MP4 or a Matroska file, and read its tracks, codecs, tags and capture facts. OneMedia reads media containers in managed code with no dependency beyond .NET. ## Before you use it - **It decodes nothing.** OneMedia reads containers: tracks, packets, codec headers and tags. It never decodes or encodes a video or audio sample, so it can’t give you frames, thumbnails or pixels. Audio can be decoded by [OneAudio](https://recursiveai.org/oneaudio/) through the `RecursiveAI.OneMedia.Audio` adapter. - **Reading only.** 0.1 reads MP4, MOV, 3GP/3G2, M4A/M4V and fragmented MP4, and Matroska and WebM (MKV, MKA, MK3D). Writing, tag editing, remuxing, subtitles, repair and HLS/DASH packaging are not in this release, nor are other containers. - **A preview.** The API can change between previews. ## Install The packages target `net10.0`, so you need the .NET 10 SDK, and `dotnet add package` needs `--prerelease` or the exact version. ```shell dotnet new console -o OneMediaStart && cd OneMediaStart dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 curl -L -o flower.mp4 https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4 ``` Three packages are on NuGet: | Package | What it is | |---|---| | `RecursiveAI.OneMedia` | the library; depends on nothing but .NET | | `RecursiveAI.OneMedia.Audio` | hands audio tracks to OneAudio for decoding; depends on the core package and `RecursiveAI.OneAudio` | | `RecursiveAI.OneMedia.Tool` | the `onemedia` command-line tool (`dotnet tool install`) | `flower.mp4` is a five-second clip from MDN’s CC0 sample videos (public domain). ## A first program Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; // Media.Open reads the file's structure only: no packet payload is read here. using MediaFile media = Media.Open("flower.mp4"); Console.WriteLine($"Format: {media.Format}"); Console.WriteLine($"Duration: {media.Duration}"); foreach (MediaTrack track in media.Tracks) { string detail = track.Video is { } v ? $"{v.Width}x{v.Height}" : track.Audio is { } a ? $"{a.SampleRate} Hz, {a.Channels} ch" : ""; Console.WriteLine($"Track {track.Index}: {track.Kind} {track.Codec} {detail}"); } // Capture facts are candidate lists: the first candidate is the value. var created = media.CaptureTime.Best; Console.WriteLine($"Created: {created?.Value} (from {created?.Source})"); Console.WriteLine($"Rotation: {media.Rotation.Value.RotationDegrees}°"); ``` ```text Format: isobmff Duration: 00:00:05.0550000 Track 0: Video h264 960x540 Track 1: Audio aac 48000 Hz, 2 ch Created: 2018-03-07T15:21:21Z (from mvhd) Rotation: 0° ``` `Duration` is a `TimeSpan?`: the container’s figure, unless a track without an edit list runs longer. `ExactDuration` is the same as an exact rational `MediaTime`. The format is `isobmff` for the MP4 family and `matroska` for MKV and WebM. `Media.Open` reads by content, not by file extension. ## What else it reads - **Tags.** `media.Tags` is a common view (`Title`, `Artist`, `Comment`, `Encoder` and more) over the native tags: iTunes `ilst`, QuickTime `udta` and `mdta` keys, 3GPP, XMP, and Matroska tags with their targets. `media.Native["ilst", "©nam"]` reads one native tag. See [Read MP4 metadata](https://recursiveai.org/onemedia/cookbook/read-mp4-metadata/). - **Facts.** `CaptureTime`, `Location`, `Device` (make, model, software) and `Rotation` each list every candidate the file holds, best first, with where it came from. See [When and where a video was recorded](https://recursiveai.org/onemedia/cookbook/video-recording-date-location/). - **Packets.** `media.Packets(track)` gives each packet’s bytes with exact timestamps, keyframe flag and byte range in the file. See [Keyframe timestamps](https://recursiveai.org/onemedia/cookbook/video-keyframes/). - **Codec headers.** `CodecParameterReader.TryDescribe` parses the codec configuration: profile, level, bit depth, chroma, color and the RFC 6381 codec string (`avc1.64001F`). See [Duration, resolution and codec](https://recursiveai.org/onemedia/cookbook/get-video-duration-resolution-codec/). - **Remote files.** `Media.OpenAsync(length, readRange)` reads through any ranged reader, such as HTTP range requests. See [Read video metadata from a URL](https://recursiveai.org/onemedia/cookbook/read-video-metadata-from-url/). ## Damaged files A file that isn’t in a format OneMedia reads throws `UnsupportedMediaException`. A file it can open but that has damage opens anyway: each problem is a typed entry in `media.Defects` with its byte offset, and reading continues where it safely can. ## Next The [cookbook](https://recursiveai.org/onemedia/cookbook/) has a complete program for each common task. [MP4, MOV and 3GP](https://recursiveai.org/onemedia/docs/isobmff/) and [Matroska and WebM](https://recursiveai.org/onemedia/docs/matroska-differential/) describe how each container is read and where the result differs from ffprobe, and [Tags and capture facts](https://recursiveai.org/onemedia/docs/tags/) lists the order in which candidates are ranked. # OneMedia **Containers, metadata, subtitles; no decoding.** A managed .NET library for media containers, with no dependency beyond the .NET base class library. OneMedia is unaffiliated with other products or companies named OneMedia. The goal is the depth of libavformat, GPAC, mkvtoolnix, MediaInfo and ExifTool in one library you can call from C#: demux and mux, lossless tag editing, evidence-backed capture time and place, subtitles and captions, repair, and HLS/DASH packaging. It never decodes or encodes an audio or video sample; audio decoding is handed to [OneAudio](https://github.com/recursive-ai-oss/oneaudio-csharp) through the `RecursiveAI.OneMedia.Audio` adapter. ## Status **Preview, version `0.1.0-preview.1`: reading MP4/MOV/3GP (including fragmented MP4) and Matroska/WebM.** Writing, tag editing, every other container, subtitles, repair and packaging are planned and not in this release. The table below lists what works and how it was checked; gaps are listed, not hidden. ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Audio; // package RecursiveAI.OneMedia.Audio using var media = Media.Open("clip.mov"); Console.WriteLine($"{media.Duration} {media.Video?.Codec} {media.Audio?.Codec}"); Console.WriteLine($"{media.Tags.Title} {media.CaptureTime.Value} {media.Location.Value}"); foreach (var candidate in media.CaptureTime.Candidates) Console.WriteLine(candidate); using var pcm = media.Decode(); // the audio track, decoded by OneAudio ``` `onemedia info ` (or `--json`) prints tracks, codecs (RFC 6381 strings), tags and facts with their candidate sources. ### Support (0.1) | Format | Read | Write | Tag edit | Packets vs ffprobe 7.0.2 | Probe + metadata I/O (local reads), ours vs ffprobe / MediaInfo / ExifTool | Documented deviations | |---|---|---|---|---|---|---| | MP4, MOV, 3GP/3G2, M4A/M4V, fragmented MP4 | ✓ demux, codec headers, native tags (`udta`, `ilst`, `mdta`, 3GPP, XMP, Nero chapters), common tags, basic facts | — | — | 121 corpus files, 744,306 packets, 0 unexplained | 4.38 GB file with the index at the end: 165,681 B in 34 reads vs 1,741,689 B / 32 (ffprobe), 23,628,223 B / 138 (MediaInfo), 1,103,868 B / 139 (ExifTool) | [docs/isobmff.md](https://recursiveai.org/onemedia/docs/isobmff/): edit-list repeats and drops, last-packet durations, negative composition offsets, fragmented durations are structural (no fragment scan) | | Matroska, WebM (MKV, MKA, MK3D) | ✓ demux (seekable and live/forward-only), codec headers, tags with targets, attachments, basic facts | — | — | 92 corpus files, 464,934 packets, 0 unexplained | 4.45 GB file: 69,806 B in 20 reads vs 4,877,532 B / 16 (ffprobe), 72,168,087 B / 493 (MediaInfo), 79,600 B / 11 (ExifTool) | [docs/matroska-differential.md](https://recursiveai.org/onemedia/docs/matroska-differential/): attachments are not streams, RealAudio is not de-interleaved, RealVideo timestamps as stored | Bytes and request counts for all 213 corpus files, over HTTP range requests and local reads, are in [docs/io-results.txt](https://github.com/recursive-ai-oss/onemedia-csharp/blob/ece3e04aba6aede01e292b3eba694eea48fd4052/docs/io-results.txt) (method in [docs/harness.md](https://recursiveai.org/onemedia/docs/harness/)). Audio tracks of MP4/3GP equal OneAudio's own MP4 reader packet for packet (250,469 packets over 64 files). Tags and facts agree with ExifTool 13.60 and MediaInfo 26.10 on 116 files, with every difference explained in [docs/tags.md](https://recursiveai.org/onemedia/docs/tags/); the field mapping is [docs/tag-mapping.md](https://recursiveai.org/onemedia/docs/tag-mapping/). The audio adapter is described in [docs/audio-adapter.md](https://recursiveai.org/onemedia/docs/audio-adapter/), the comparison method in [docs/harness.md](https://recursiveai.org/onemedia/docs/harness/), and the benchmarks in [docs/bench.md](https://recursiveai.org/onemedia/docs/bench/). **Speed** against the fastest managed library per task (ratio = OneMedia time ÷ theirs, steady state; lower is better). Full tables, allocations, memory, first-call times and the native tools' times are in [docs/bench-results](https://github.com/recursive-ai-oss/onemedia-csharp/tree/ece3e04aba6aede01e292b3eba694eea48fd4052/docs/bench-results); the method is in [docs/bench.md](https://recursiveai.org/onemedia/docs/bench/). | Task | Compared with | x64 (Ryzen 9 7950X3D) geomean / worst file | arm64 (Apple M2) geomean / worst file | |---|---|---|---| | MP4 demux, every packet | SharpMP4 | 0.10× / 0.21× | 0.09× / 0.20× | | MP4 audio track only | OneAudio `Mp4PacketSource` | 0.53× / 0.82× | 0.49× / 0.61× | | MP4 probe (tracks, duration) | MetadataExtractor, TagLib#, ATL.NET | 0.10× / 0.40× | 0.12× / 0.28× | | Matroska demux | Matroska (NEbml) | 0.26× / 0.31× | 0.25× / 0.41× | | Matroska probe | NEbml element reader | 0.69× / 0.83× | 0.60× / 0.76× | The first open in a process includes JIT time: about 17–23 ms for MP4 and 12–16 ms for Matroska, against 10–14 ms and about 3 ms for those libraries. Every parser is fuzzed; damaged input is reported as typed defects with byte offsets and never surfaces as an untyped exception. Reading pulls packets with zero allocations per packet and constant memory at any file size. ## Packages | Package | What it is | |---|---| | `RecursiveAI.OneMedia` | the library; depends on nothing but .NET | | `RecursiveAI.OneMedia.Audio` | adapter that hands audio tracks to OneAudio; depends on the core package and `RecursiveAI.OneAudio` | | `RecursiveAI.OneMedia.Tool` | the `onemedia` command-line tool (`dotnet tool install`) | Target framework is `net10.0`. The library is NativeAOT, trimming and browser-WebAssembly clean. ## Building ``` dotnet build -c Release dotnet test -c Release dotnet pack -c Release ``` Build output goes to `artifacts/` in the repository root (ignored by git). To build somewhere else, set `ONEMEDIA_SCRATCH` to a directory; output then goes to `$ONEMEDIA_SCRATCH/artifacts`. Test files are written under the system temporary directory (`TMPDIR`). `smoke/OneMedia.AotSmoke` is published with NativeAOT and fails on any trim or AOT warning; `smoke/OneMedia.WasmSmoke` runs the same checks under the browser-wasm runtime (`wasm-tools` workload; run the built `AppBundle/main.mjs` with node). `tools/` holds the differential and I/O harness and the corpus scripts, `fuzz/` the fuzz targets and `bench/` the benchmarks; none of them ship. ## Contributing notes - Clean room: the code is written from published specifications and from observing the behaviour of other tools as black boxes. The source of other media libraries and tools is not consulted. - Every public type and member has an XML doc comment; warnings are errors. - `unsafe` code, `Unsafe.*`, `MemoryMarshal` and intrinsics are not used. ## Licence `MIT OR Apache-2.0`, at your option. Copyright (c) Recursive AI, LLC. See `LICENSE-MIT` and `LICENSE-APACHE`. Sample media notices are in `THIRD-PARTY-NOTICES`. # ISO base media files (MP4, MOV, 3GP, fragmented MP4, CMAF) 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 | Layer | Types | Notes | |---|---|---| | Box structure | `BoxReader`, `BoxHeader`, `BoxNode`, `BoxTree` | Lazy tree of byte ranges. Children are read on first use. No payload bytes are kept. | | Format model | `FileTypeBox`, `MovieReader` → `IsoMovie`, `IsoTrack`, `SampleEntry`, `EditListEntry`, `TrackExtends` | Headers, edit lists, sample descriptions. Sample tables are located, not read. | | Neutral demux | `IsoBmffDemuxer` (seekable and forward-only), `IsoBmffAsyncDemuxer` (ranged), `IsoBmffFormat` | Tracks, 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. | Code | Kind | Meaning | What happens | |---|---|---|---| | `isobmff.size-below-header` | `InvalidSize` | Declared size smaller than the header | The box list ends there. | | `isobmff.size-huge` | `InvalidSize` | 64-bit size above 2^63-1 | The box list ends there. | | `isobmff.truncated` | `SizeExceedsFile` | Top-level box runs past the end of the file | The box is clamped to the file. | | `isobmff.size-exceeds-parent` | `SizeExceedsParent` | Nested box runs past its parent | The box is clamped to the parent. | | — | `Truncated` | Fewer than 8 bytes left, a cut-off 64-bit size or `uuid` type, or a source that ends inside a box | The list ends. | | `isobmff.no-moov` | `MissingRequired` (exception) | No movie box in the file | `MediaFormatException`; `TryOpen` returns the defect instead. | | `isobmff.no-mvhd`, `isobmff.track-incomplete`, `isobmff.no-stbl` | `MissingRequired` | A required box is missing | A track without track header, media header or handler is skipped. | | `isobmff.table-count`, `isobmff.elst-count`, `isobmff.trun-count` | `Truncated` | A table declares more entries than its box holds | The count is clamped to what the box holds. | | `isobmff.stts-short`, `isobmff.stsc-short`, `isobmff.stsc-order`, `isobmff.chunk-missing` | `Inconsistent` | Sample 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-eof` | `Truncated` | A sample's bytes lie beyond the end of the file | The sample is not returned. Reported once per track. | | `isobmff.sample-limit` | `LimitReached` | A sample is larger than `MaxPacketBytes` | The sample is skipped. | | `isobmff.short-read` | `Truncated` | The source returned fewer bytes than the sample size | The sample is skipped. | | `isobmff.traf-unknown-track`, `isobmff.traf-no-tfhd` | `Inconsistent`, `MissingRequired` | A track fragment names a track the movie does not declare, or has no header | The track fragment is skipped. | | `isobmff.forward-order` | `InvalidOrder` | Forward-only source: a sample lies before the current position | The sample is skipped. | | `isobmff.entry-large` | `LimitReached` | A sample entry is larger than 1 MiB | Only its first mebibyte is read. | | `isobmff.limit` | `LimitReached` | Depth, element count, allocation or sample-table limit | The 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: | Difference | Who 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 otherwise | The 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 PTS | We keep `Pts = Dts + offset` as §8.6.1.3 defines it. | | First fragment's durations that come from the `tfhd` default: ffprobe prints `N/A` | The 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 length | The 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 sample | The 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 frame | We 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 way | Timestamps 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 timestamps | We 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 repeats | Times come from `tfdt` and the runs, exact. | | A file cut off inside the media data: ffprobe still lists the sample that straddles the cut | We 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 delta | The 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 tables | The edit list applies to fragments too (§8.6.6). | | H.264/HEVC keyframes: ffprobe also marks IDR pictures that the sync sample table omits | We 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 packets | We 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 boundaries | Ours 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 headers | Ours 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 none | Ours 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 stream | `MediaFile.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. # Matroska and WebM: reading, and how it is checked How `RecursiveAI.OneMedia.Matroska` reads these files and how the result is compared with other implementations. Specifications: RFC 8794 (EBML), RFC 9559 (Matroska); section numbers are in the code comments. WebM is Matroska with `DocType` `webm`; both go through the same reader. ## Layers | Layer | Types | Notes | |---|---|---| | EBML primitives | `Ebml`, `EbmlCrc32` | Variable-length IDs and sizes (RFC 8794 §4), integers, floats (decoded with integer arithmetic), CRC-32. | | Structure | `EbmlDocument`, `EbmlElement` (an `IStructureNode`) | Lazy tree of byte ranges. Children are read on first use; payloads are read on request and never kept. | | Format model | `MatroskaHeader`, `MatroskaInfo`, `MatroskaTrackEntry` and friends, `MatroskaSeekEntry`, `MatroskaCuePoint` | Decoded from the element payloads by tolerant in-memory parsers. Chapters, Tags and Attachments are kept as ranges. | | Neutral demux | `MatroskaDemuxer` (seekable and forward-only), `MatroskaAsyncDemuxer` (ranged), `MatroskaFormat` | One async-first reader drives both; the synchronous demuxer completes its `ValueTask`s synchronously. | ## Reading rules - **Open** reads the EBML header, the Segment header, then the SeekHead (and chained SeekHeads) and follows it to Info, Tracks and Cues. With no SeekHead the reader walks the Segment's children; it stops at the first Cluster once Tracks has been seen. When Tracks only appears after the Clusters and there is no SeekHead, it walks past every Cluster with one 512-byte read of each Cluster header: this is the documented cost of that layout (see "Open cost" below). A forward-only source needs Tracks before the first Cluster (`MediaFormatException`, kind `InvalidOrder`). - **Unknown sizes** are allowed for Segment and Cluster only (RFC 8794 §6.2). An unknown-size Cluster ends at the next element that belongs to the Segment level; an unknown-size element anywhere else is a defect and is cut at its parent's end. - **Time.** `TimestampScale` that divides 10⁹ gives a track time base of `1/(10⁹ / scale)`; otherwise ticks are nanoseconds (`units × scale`, exact). Float `Duration` and `SamplingFrequency` are converted with integer arithmetic. No floating point reaches a packet time. - **CodecDelay** is subtracted from block timestamps (RFC 9559 §11.2 Opus mapping) and also exposed exactly as the track's priming. A block's `DiscardPadding` is attached to the packet (positive: the end of the last frame of the block; negative: the start of the first frame). - **Duration of a packet:** `BlockDuration`, else `DefaultDuration` (× frames in the lace), else unknown. A lace's total is `BlockDuration` or `n × DefaultDuration`; frames are split by cumulative floor so the parts add up to the total. - **DTS** is unknown for video (Matroska stores presentation times only) and equals the PTS for audio, subtitles and data. - **Keyframe** is the SimpleBlock flag, or a BlockGroup without `ReferenceBlock`. The `invisible` flag becomes `Discard`, `discardable` becomes `Disposable`. - **Source range** is the frame's bytes in the file; for laced blocks, the frame's own bytes. When header stripping (`ContentCompAlgo` 3) or zlib (`ContentCompAlgo` 0) is in use, the packet is delivered from a buffer (the stripped prefix put back, or the frame inflated with the BCL's `ZLibStream`, never past `MaxPacketBytes`) and the source range still names the stored bytes. A frame that is not valid zlib is delivered as stored with an `InvalidValue` warning. Other compression (bzlib, lzo) and several stacked encodings are reported as an `Unsupported` defect and the frames are delivered as stored. Undoing content compression is container framing, not sample decoding. - **Encryption** (`ContentEncryption`) marks the track `IsProtected`; no key handling. - **Seeking** by time goes to the Cues entry at or before (or after, for `NextKeyframe`) the target, using the first enabled video track that has cues, otherwise the first track with cues. Without Cues the reader walks Cluster headers (one small read per Cluster) from the start; it lands on a Cluster start. - **Resync after damage.** When a header under the Segment or inside a Cluster is not valid EBML, or a size runs past its parent, the reader scans forward for the next Segment-level element whose header and size are sound, records a defect with the offset, and continues. The lazy tree does the same for junk between the Segment's children. ## Defects Every defect carries the byte offset of the element concerned. Kinds used (the message names the element): | Kind | Severity | When | |---|---|---| | `InvalidSize` | Error | A header is not valid EBML; an unknown size where not allowed. | | `SizeExceedsParent` / `SizeExceedsFile` | Error / Warning | An element is longer than what contains it, or than the file; it is cut there. | | `Truncated` | Error / Warning | The data ends inside an element or a block; a partial block is dropped. | | `UnaccountedBytes` | Error | Bytes that are not an element, skipped to the next sound element; or nothing sound follows. | | `ChecksumMismatch` | Warning | A `CRC-32` element does not match its siblings' bytes. | | `InvalidValue` | Warning | An integer or float element that cannot be read; TimestampScale 0; a negative lace size. The default is used. | | `MissingRequired` | Error / Warning | A TrackEntry without TrackNumber, a CuePoint without CueTime, a BlockGroup without Block, a Segment without Tracks (throws). | | `Duplicate` | Warning | Two tracks with one TrackNumber; blocks go to the first. | | `Inconsistent` | Warning | A block for an undeclared track (skipped); a SeekHead entry that points at another element; a lace whose sizes do not add up. | | `InvalidOrder` | Error | No Segment after the header (scanned for); first Cluster before Tracks on a forward-only source (throws). | | `Unsupported` | Warning | Unknown DocType, a higher `DocTypeReadVersion`, frame compression this reader does not undo. | Damaged input never raises anything but `MediaException` types. Truncation at every byte offset and four byte-value corruptions at every offset of a crafted file are unit tests; a truncation sweep over a 13 312-byte WebM (every cut point) gave 9 586 clean opens and 3 727 typed exceptions, no others. ## Differential method `tools/OneMedia.MatroskaDiff` (a test tool; not shipped) compares our packets with `ffprobe -show_packets -show_entries packet=stream_index,pts,dts,duration,size,pos,flags` and the element structure and track headers with `mkvinfo`. The oracles run as released binaries only (versions in the results table below). `tools/matroska/run-matroska-diff.sh` runs the whole set; `tools/matroska/make-fixtures.sh` writes the synthetic fixtures (ffmpeg- and mkvmerge-written files from generated input) and `tools/matroska/run-matroska-big.sh` the large-file checks. For every stream the packets are compared in order: size, payload checksum (ffprobe's `-show_data_hash adler32` against the Adler-32 of the bytes we deliver), PTS, duration, key flag, and position. Each difference is in an explained class or it is unexplained; the tool exits non-zero on any unexplained difference. ### Explained classes "Follows the spec" says which side matches RFC 9559. | Class | What differs | Follows the spec | |---|---|---| | `duration-from-codec` | ffprobe fills in a duration the file does not store (an audio packet without `BlockDuration` or `DefaultDuration`) by looking at the codec's frame length. Ours stays unknown. | Both: the file stores none; ffprobe derives one. Deriving it needs the codec layer. | | `lace-shared-pts` | Frames of a lace with neither `BlockDuration` nor `DefaultDuration`: ours all carry the block's timestamp (unknown duration); ffprobe spaces them by codec frame length. | Ours (§10.2: a lace has one timestamp). | | `inexact-timebase` | `TimestampScale` does not divide 10⁹: ours are exact nanosecond ticks, ffprobe rounds to an integer time base, so values are within one tick. | Ours (exact). | | `last-block-duration-differs` | The last block's `BlockDuration` is shorter than the codec's frame; ffprobe reports the frame length, ours the stored value. | Ours (the file's value). | | `realvideo-timestamps` | RealVideo (`V_REAL/RV40`): ffprobe takes frame times from the RealVideo stream (blocks at 80 and 81 ms are reported at 40 and 80); ours is the Block time. | Ours (the container states it; the stream's own times are codec work). | | `realaudio-deinterleave` | RealAudio (`cook`, `ra`) blocks hold interleaved sub-packets; ffprobe de-interleaves them into the codec frames (280 bytes where the block is 1400). Ours delivers the stored block. | Ours (the bytes in the file); de-interleaving is codec work. | | `webvtt-block-repacked` | The WebVTT-in-Matroska block (`BlockAdditions` with settings and the cue text): ffprobe repacks it as a `webvtt` packet. Ours is the block's bytes. | Ours (the bytes in the file). | | `pos-queue` | `pos` for packets ffprobe holds back when it interleaves audio with video: it reports an earlier position than the block's own. For video, `pos` equals the Block's start, which is the frame's `SourceRange.Offset` minus the 4-byte block header (one-byte track number). | The position of the block in the file is ours; ffprobe's value is a queue artefact. | ### mkvinfo mkvinfo (`-a -P -z -v -v`) lists elements with offset, header size and data size. Every element mkvinfo lists is found in our tree at the same offset with the same sizes (`differing=0`); the tree may list more (it also lists children that mkvinfo hides at the verbosity used). On the damaged published files the lists differ in one way: test7 contains an element at 451451 whose size runs past its cluster. The tree cuts it at the cluster's end and keeps its bytes as a range; mkvinfo descends into the cut-off payload and lists the elements it sees there, which we do not. Track headers (number, UID, type, codec, language, flags, default duration, video/audio fields) are compared field by field. ## Open cost (I/O) | Layout | Reads at open | |---|---| | SeekHead + Cues (any writer that wrote them) | The first window of the file (32 KiB); Cues are read on the first seek. | | Live (unknown sizes, no SeekHead, Tracks before Clusters) | The first window. | | No SeekHead, Tracks after the Clusters | One 512-byte read per Cluster header until Tracks is found: bytes = clusters × 512. Documented cost; there is no way to find a trailing Tracks without walking. | ## Allocations and memory `TryReadPacket` allocates nothing in Release builds: payloads are read into a reused buffer (initial 1 MiB, doubled only when a larger block arrives, bounded by `MediaLimits.MaxPacketBytes`). Debug builds' async state machines allocate, so the zero-allocation test is skipped there. Open-time `ToArray()` copies are of CodecPrivate, the SegmentUUID, compression settings and the header-stripping prefix. Each is a slice of a master element (Info, Tracks, SeekHead, Cues) that was itself checked against `MediaLimits.MaxAllocationBytes` before it was read, so the copies are bounded by that limit and by the number of tracks: the memory taken at open does not grow with the size of the file. # Codec header layer Behaviour of `RecursiveAI.OneMedia.Codecs.*`: header parsing and repackaging only. No sample is decoded or encoded. ## Rules every parser follows - Input is a `ReadOnlySpan`. A parser returns `false` (or a status) for truncated, reserved or out-of-range data. It never throws and, for the header parsers listed in the allocation gate below, never allocates. - Loops are bounded by constants of the syntax (for example at most 64 short-term reference picture sets) or by the input length. The record parsers that build lists (`avcC`, `hvcC`, `vvcC`) take the `MediaLimits` of the caller where a count could be large. - Emulation-prevention bytes (0x03 after two zero bytes) are skipped while reading, so a NAL unit is parsed as it sits in a stream or sample. `EmulationPrevention.Remove` is for callers that need the unescaped bytes (SEI payloads). - Parsed records keep slices of the memory they were parsed from. Writers take an `IBufferWriter` or a destination span and report the size needed when it is too small. ## What each family provides | Family | Parsed | Built / converted | |---|---|---| | H.264 | NAL header, SPS (profile, level, chroma, bit depth, size with cropping, VUI: SAR, colour, timing, reorder), PPS essentials, SEI framing, `avcC` | `avcC` from Annex B; Annex B ↔ length-prefixed | | H.265 | VPS/SPS/PPS essentials (profile-tier-level, size with conformance window, chroma, bit depth, VUI colour/timing/min spatial segmentation), SEI framing, `hvcC` | `hvcC` from Annex B; Annex B ↔ length-prefixed | | H.266 | SPS (profile/tier/level, size, chroma, bit depth unless subpicture information is present), `vvcC` | `vvcC` write; Annex B ↔ length-prefixed. Provisional: no reference stream was available | | AV1 | OBU header, sequence header, `av1C`, low-overhead / Annex B detection | `av1C` from a sequence header | | VP8 / VP9 | VP8 key frame header; VP9 uncompressed header, superframe index, `vpcC`, WebM feature list | `vpcC` from a key frame; `vpcC` ↔ feature list | | MPEG-2 video | Sequence header and extension, GOP and picture headers, start codes | | | AAC | AudioSpecificConfig (explicit and backward-compatible SBR/PS, program config element), ADTS header, `esds` | ADTS → raw + config; raw + config → ADTS; `esds` write | | Opus | OpusHead, `dOps`, TOC duration | OpusHead ↔ `dOps` | | Vorbis | identification header, setup header modes, block sizes | Xiph lacing split and build | | FLAC | STREAMINFO, metadata block headers, frame header with CRC-8 | `dfLa` ↔ Matroska CodecPrivate | | AC-3 / E-AC-3 | syncframe header | `dac3`, `dec3` from a first frame | | MPEG audio | frame header (Layers I–III, MPEG-1/2/2.5), Xing/Info/VBRI with the LAME delay and padding | | | AMR | storage magic, frame TOC size, `damr` | | `CodecParameterReader` turns a track's configuration (or first sample) into a `CodecParameters` summary with the RFC 6381 string; `CodecConfigMapper` maps Matroska CodecPrivate to the ISO configuration box and back. ## Choices where the specifications or the oracles settle a detail - **Annex B start codes.** From length-prefixed samples, H.264 units get a four-byte start code when first in the sample or when they are SPS/PPS, a three-byte one otherwise; H.265 and H.266 units always get four bytes. Parameter sets stored in the sample entry are put before the first random access picture of a sample that carries none. This is the layout the common tools write (observed with ffmpeg's `h264_mp4toannexb`/`hevc_mp4toannexb` on streams with several slices per picture and with access unit delimiters), and it makes the output byte-comparable. - **`avcC` tail.** The chroma format and bit depth bytes follow the parameter sets for every profile whose SPS has `chroma_format_idc` (100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135). Files written by common tools do this for 4:4:4 as well; older text lists only 100, 110, 122 and 144. - **`hvcC`.** `parallelismType` is written as 0 (unknown), as files from common tools do even when wavefront processing is enabled. `array_completeness` is a parameter of `TryCreateFromAnnexB`; the common writers store 0, the specification asks for 1 when the arrays are complete. SEI units seen before the first slice are stored in prefix/suffix SEI arrays. - **`esds`.** Written with one-byte descriptor sizes, ES_ID 0, and the bit rates the caller passes. The common writers use four-byte sizes, ES_ID 1 and their measured rates. Readers agree on object type, stream type and decoder-specific info, which is what the tests compare. - **VP9 CodecPrivate.** The common Matroska writers omit it. The mapper then derives `vpcC` from the first key frame (level estimated from picture size alone, so frame rate and bit rate limits are not considered), and writes the WebM feature list for ISO → Matroska. - **MPEG audio in ISO.** `esds` with object type 0x6B (MPEG-1) or 0x69 (MPEG-2/2.5) and no decoder-specific info. - **AAC.** `OutputSamplingFrequency` is the SBR extension rate when SBR is signalled, doubling the core rate if the extension rate is absent. ffprobe reports the core rate for implicitly signalled streams; compare `CoreSampleRate` there. - **Channel counts.** AAC configuration 0 takes the count from the program config element; AC-3 counts the LFE channel. ## Allocation gate Per call, 0 bytes: H.264/H.265 SPS, AV1 sequence header, AudioSpecificConfig, ADTS header, VP9 frame header, MPEG-2 sequence header, AC-3 syncframe, MPEG audio header, Opus TOC, Annex B ↔ length-prefixed into a caller buffer. Static lookup tables of multi-byte values are kept in static arrays, because a span literal of `int` or `ushort` allocates each time it is read. ## Differential method `tools/codecs/make-streams.sh` generates the streams from ffmpeg's own test sources; the tests under `tests/OneMedia.Tests/Codecs` compare parsed fields with ffprobe, RFC 6381 strings with MP4Box, and repackaged bytes with ffmpeg's bitstream filters and `-c copy` remuxes. Set `ONEMEDIA_CODEC_STREAMS` (and `ONEMEDIA_FFPROBE`, `ONEMEDIA_FFMPEG`, `ONEMEDIA_MP4BOX`) to run them. Documented deviations: the `esds` encoding, the missing VP9 CodecPrivate, `array_completeness` above. # Tags, common view and basic facts How `RecursiveAI.OneMedia.Tags` and `RecursiveAI.OneMedia.Metadata` read the tags of ISO base media files (MP4, MOV, 3GP) and Matroska/WebM, and what the common view and the basic facts are built from. Writing tags is a later feature; the models are shaped so a writer can round-trip them: every item keeps its source range, and values are fetched from the source by range, never copied into the tree. ## Layers | Layer | Types | Notes | |---|---|---| | Native tags | `NativeTag`, `ITagContainer`, `TagSystems` | One `NativeTag` per item: system, key, track, kind (text, number, real, binary, image), data type, locale or language, byte range of the item and of its value, decoded text or number. Values up to 64 KiB are decoded when read; longer text and all binary values and pictures are read on access (`Text`, `ReadValue()`). | | ISO systems | `ItunesItemList` (`ilst`), `QuickTimeKeys` (`mdta` key table with `ilst`), `QuickTimeUserData` (`udta` atoms, incl. `©`-atoms with language), `ThreeGppAssets` (`titl`, `auth`, `perf`, `gnre`, `dscp`, `cprt`, `yrrc`, `loci`, `albm`, `kywd`, `rtng`, …), `XmpPacket` (`uuid` XMP box and `XMP_` atom) | Plain and `meta`-wrapped. XMP is exposed as the raw packet range plus the flat property list. | | Matroska systems | `MatroskaInfoTags` (Segment Info: `Title`, `MuxingApp`, `WritingApp`, `DateUTC`), `MatroskaTagSet` (`Tags`/`Tag`/`Targets`/`SimpleTag`), `MatroskaAttachments` (`AttachedFile`, cover art as image items) | Targets keep `TargetTypeValue`, `TargetType` and the track UIDs (mapped to track numbers). Nested SimpleTags are keyed `PARENT/CHILD`. | | Common view | `CommonTags`, `CommonField`, `TagPicture` | A view over the native tags through `TagMappingTable` ([tag-mapping.md](https://recursiveai.org/onemedia/docs/tag-mapping/)). Nothing is stored. | | Facts | `MediaFacts`, `Fact`, `FactCandidate`, `MediaDateTime`, `GeoLocation` | Duration, capture time, location, make, model, software, orientation, frame size. Each is an ordered candidate list; the first candidate is the value. | | Entry points | `IMetadataProvider.ReadMetadata()` on `IsoBmffDemuxer`, `IsoBmffAsyncDemuxer` and `MatroskaDemuxer`; `MatroskaAsyncDemuxer.ReadMetadataAsync()` | → `MediaMetadata` (`NativeTags`, `Tags`, `Facts`, `Chapters`, `Containers`, `XmpPackets`, `Defects`, indexer `[system, key]`, `Find`). | ## Reading rules - **I/O.** Only structure is read: the movie box (already loaded by the opener), the top-level box headers up to 64 boxes (stopping at the first `moof`), and for Matroska the Segment's leading elements up to the first Cluster plus the SeekHead entries that point at `Tags` and `Attachments`. Tags after the Clusters cost a header read each, never a scan. Cover art is read when asked for. Over a ranged source `MatroskaAsyncDemuxer.ReadMetadataAsync` fetches the located elements whole (each up to `MaxAttachmentBytes`) so the result stays valid after the source closes; an element over the limit is reported (`tags.limit`) and skipped. - **QuickTime `udta` versus 3GPP boxes.** The same four-character code can have a QuickTime layout (a list of `u16 size, u16 language, text` records) or a 3GPP layout (full box: version/flags, packed language, zero-terminated UTF-8 or UTF-16). A box is read as 3GPP when its first four payload bytes are zero and the language word has its top bit clear; otherwise as QuickTime. - **`ilst` data types** (ISO/IEC 14496-12 and the QuickTime spec): 1 UTF-8, 2 UTF-16BE, 4 and 5 text variants, 13/14/27 JPEG/PNG/BMP pictures, 21–24 and 65–78 integers and reals. `trkn` and `disk` are `00 00, number, total, 00 00` pairs. `gnre` holds the ID3v1 genre number plus one. An unknown code is kept as binary with an `Info` defect. - **Key-table metadata (`mdta`).** `keys` lists reverse-DNS names; each `ilst` item's four-character type is a 1-based index into it. - **Matroska common view.** Only tags that target the file (no track, edition, chapter or attachment UID) feed it, and for a file with exactly one track also that track's tags, except `ENCODER` (which names the stream's encoder). Within a key: tags of the file before tags of the only track; the lowest `TargetTypeValue` of 30 or above first, then values below 30; tags without a language or with `und` before language-specific ones; then file order. Everything else stays on the native list. - **Dates.** `MediaDateTime.TryParse` accepts ISO 8601 (with `Z`, `±hh:mm`, `±hhmm` or no zone) and date-only text; a bare year is not an instant. The zone certainty is `Utc`, `Offset` (with minutes), `Local` or `Unknown`. - **Locations.** ISO 6709 in the `±DD.DDDD±DDD.DDDD[±AAA]/`, `±DDMM.MMM…` and `±DDMMSS.SSS…` forms (with an optional `CRS…` suffix), 3GPP `loci` (16.16 fixed point; an all-zero box means unknown and is ignored), and XMP `exif:GPSLatitude`/`GPSLongitude`. - **Chapters.** Nero `chpl` lists (version 1: 4 reserved bytes, a one-byte count, then per chapter a 64-bit start in 100 ns units and a length-prefixed UTF-8 title) give `MediaMetadata.Chapters`; checked against ffprobe and ExifTool on ffmpeg-written files. Matroska `Chapters` and QuickTime text chapter tracks are not read yet. ## Fact precedence A candidate list is ordered by rank, lower first, then file order. The rank is fixed and documented here; a replaceable resolver is a later feature. | Fact | Rank | Source | |---|---|---| | Capture time | 10 | `com.apple.quicktime.creationdate` (carries an offset) | | | 20 | XMP `exif:DateTimeOriginal`, `xmp:CreateDate`, `photoshop:DateCreated` with a zone | | | 25 | `mdta` `creation_time` | | | 30 | `©day` (`udta`, `ilst`) | | | 40 | the same XMP properties without a zone | | | 45 | Matroska `DATE_RECORDED` tag | | | 50 | Matroska Info `DateUTC` (when the file was muxed) | | | 60 | `mvhd` creation time, as UTC | | Location | 10 / 20 / 30 / 40 | `mdta` ISO 6709 / `©xyz` / 3GPP `loci` / XMP GPS | | Make, model, software | 10 (12 for `com.android.*`) / 20 / 30 | `mdta` / `udta` `©mak`, `©mod`, `©swr` / XMP; Matroska `WritingApp` 50, `MuxingApp` 51 | | Orientation | – | `tkhd` matrix of the first video track → 0/90/180/270 plus mirror | | Frame size | – | first video sample entry (ISO) or `TrackEntry.Video` (Matroska): coded and display size | | Duration | – | movie header or Segment Info | ## Defects All carry the byte offset of the item. Reading continues; nothing here throws on malformed tag data (typed exceptions come only from the structure layers or from `ReadValue()` over a limit). | Code | Kind | Meaning | |---|---|---| | `tags.invalid-utf8`, `tags.invalid-utf16` | `InvalidValue`, `InvalidSize` | Text is not valid; bad sequences are replaced with U+FFFD, an odd trailing byte is dropped. | | `tags.value-too-large`, `tags.limit`, `tags.xmp-limit`, `tags.mkv-large`, `tags.mkv-depth` | `LimitReached` | A value or element is over `MaxTagBytes`/`MaxAttachmentBytes`, a packet has too many properties, SimpleTags nest too deeply; the part is skipped. | | `tags.short-read` | `Truncated` | The source ended inside a value. | | `tags.item-no-data`, `tags.data-short`, `tags.freeform-size`, `tags.int-length`, `tags.type-set`, `tags.data-type` | various | `ilst` item without a `data` box, a `data` box shorter than its type and locale, an unusable `mean`/`name` size, an integer with an impossible length (not reported for `trkn`/`disk` pairs), an unknown type set or code. | | `tags.keys-short`, `tags.keys-count`, `tags.keys-size`, `tags.keys-index` | `Truncated`, `InvalidSize`, `InvalidValue` | Key table shorter than its count, a key whose size does not fit, an item pointing past the table. | | `tags.udta-raw-text`, `tags.udta-text-size` | `Inconsistent`, `SizeExceedsParent` | A `udta` atom without a text-record header (read as plain text); a record longer than its atom. | | `tags.3gpp-short`, `tags.3gpp-unterminated`, `tags.3gpp-location` | `Truncated`, `InvalidValue` | A 3GPP box shorter than its fields, a string without a terminator, a `loci` position out of range. | | `tags.xmp-malformed`, `tags.xmp-gps` | `InvalidValue` | The XMP packet is not well-formed XML (DTDs are prohibited, so entity bombs are refused), or the GPS pair is not a position. | | `tags.datetime`, `tags.iso6709` | `InvalidValue` | A date or position text that does not parse; the fact is not produced. | | `tags.chpl-short` | `Truncated` | A chapter list shorter than its header or count. | | `tags.mkv-header`, `tags.mkv-no-name`, `tags.mkv-attachment`, `tags.mkv-date`, `tags.mkv-target-level` | various | A tag-bearing element without a readable header, a SimpleTag without a name, an attachment without data, an invalid `DateUTC`, a `TargetTypeValue` outside 10–70. | Limits used (`MediaLimits`): `MaxTagBytes` (text and binary values, `udta` atoms, key tables, XMP packets, chapter lists), `MaxAttachmentBytes` (pictures and, over a ranged source, the fetched Matroska elements), `MaxAllocationBytes`, `MaxElementCount` and `MaxStructureDepth` through the structure layers. ## How the reads were compared `tools/OneMedia.Oracles meta` (run with `tools/run-metadata-compare.sh`) reads each file with our reader and with ExifTool 13.60 (`-j -G1 -a -api largefilesupport=1`, once plain and once with `-n`) and MediaInfo 26.10 (`--Output=JSON`), and writes one row per field to `metadata-compare.tsv`: `native` rows (each text tag's value among the oracle's values of the matching group), `common` rows (the common field against the oracle's field of the same meaning; MediaInfo's General section only) and `fact` rows. Dates are compared as instants, positions as numbers (the oracles print degrees-minutes-seconds, decimals or hemisphere letters). Each row records what each oracle did: agree, disagree or does not report. `tools/summarize-metadata-compare.sh` prints the counts. The corpus is 47 fixtures written by ffmpeg 7.0.2, MP4Box 26.07, mkvmerge 103.0 and ExifTool (`tools/gen-metadata-fixtures.sh`; a subset is in `testdata/metadata`) plus public sample files (Blender movies, the Matroska test files, vendor samples) fetched by the corpus scripts. ### Differences from ExifTool and MediaInfo, and why | Difference | Explanation | |---|---| | ExifTool shows one value per tag name and group | It keeps the last of several Matroska tags with the same name (a `TITLE` at two levels) and files every track's tags under one `Track` group, so the second track's `ENCODER`/`DURATION` tags do not appear. We keep all of them on the native list. | | Which of several values is "the" value | For a name repeated at several levels ExifTool takes the last in file order, MediaInfo joins them with ` / `, we take the lowest level of 30 or above first. Both values are reachable (`CommonTags.Values`). | | MediaInfo's `Title` for Matroska with attachments | It lists the attachment file names. Attachments are not titles in the common view. | | Track-targeted Matroska tags | ExifTool and MediaInfo show them as file tags. We keep them native-only unless the file has exactly one track. | | `Encoder` of a remuxed Matroska file | mkvmerge keeps the source's file-level `ENCODER` tag next to its own `WritingApp`; the tag wins over the Segment Info application, MediaInfo reports the application. ExifTool lists both. | | QuickTime `©aut` | MediaInfo and ExifTool call it Performer or Author; the common view has an `Author` field and `Artist` takes `©ART`/`©prf`. | | QuickTime `©swr` | MediaInfo's "Writing application" shows it; we expose it as the Software fact, not as the Encoder field. | | Duration | MediaInfo derives it from the streams where ExifTool and we use the movie header; where a track is longer than the header they differ. | | `mvhd` before 1970 | A writer that stored Unix-epoch seconds in the 1904-epoch field gives 1942 for a 2008 recording. We report the literal value (MediaInfo does too); ExifTool shows 2008. | | 3GPP `loci` | Coordinates are 16.16 fixed point; the quantised value (51.500687 for 51.5007) is what the file holds. | | `trkn`/MP4Box `©trk` | MP4Box adds its track number as a text item `©trk` beside the numeric `trkn`; the numeric pair is the track number. | ### Where it stands On 116 files (63 MP4/MOV/3GP/M4A, 53 Matroska/WebM/MKA) against ExifTool 13.60 and MediaInfo 26.10: 1,266 compared fields. 1,097 agree with every oracle that reports them; 145 more agree with one oracle where the other does not report the field (`native` rows only ExifTool lists: MediaInfo has no key for most tags); 13 are ExifTool's one-value-per-name collapse, 7 are the multi-value and mkvmerge `Encoder` cases, 3 are MediaInfo's stream-based duration, and 1 is a malformed fixture on purpose (`north_of_here` as a position, reported as `tags.iso6709`). Per kind, rows where the oracle reported the field and agreed: native tags ExifTool 625/646 and MediaInfo 496/498 (MediaInfo prints degrees-minutes-seconds positions as if they were decimal degrees); common view ExifTool 196/197 and MediaInfo 200/206; facts ExifTool 295/296 and MediaInfo 317/322 (the rest are the durations, positions and the pre-1970 `mvhd` above). # Tag mapping How the common tag view picks values from the native tag systems. The table is the one the code reads (`TagMappingTable.Rows`); this page is generated from it, and a test fails when they differ. - Rows of one field are tried in table order, and within a row the keys in the order listed. The first value is the field's value; the others stay reachable through `CommonTags.Values`. - Native keys are spelled as the system spells them: four-character atom codes for `ilst` and `udta` (`©nam`), reverse-DNS names for key-table metadata (`com.apple.quicktime.title`), `namespace#property` for XMP, upper-case names for Matroska (nested SimpleTags as `PARENT/CHILD`). Matroska names are matched case-insensitively. - Matroska: only tags that target the file (no track, edition, chapter or attachment UID) feed the view, plus the tags of the only track of a single-track file (except `ENCODER`, which names the stream's encoder). Within a key the lowest target level of 30 or above comes first, then levels below 30; tags with no language or `und` come before language-specific ones; tags of the file itself come before tags of the only track. - 3GPP boxes and QuickTime user-data atoms share four-character codes with different layouts; the reader tells them apart as described in `docs/tags.md`. - Sources: the MusicBrainz Picard tag-mapping table (CC0), the ExifTool QuickTime tag documentation (read as tables of facts), the Matroska tag-name list, Apple's QuickTime File Format specification and 3GPP TS 26.244 §8. | Field | System | Keys (in precedence order) | Document | Note | |---|---|---|---|---| | Title | ilst | `©nam` | Picard tag map, iTunes MP4 | | | Title | mdta | `com.apple.quicktime.title`, `com.apple.quicktime.displayname` | ExifTool QuickTime Keys tags | | | Title | udta | `©nam` | ExifTool QuickTime UserData tags | | | Title | 3gpp | `titl` | 3GPP TS 26.244 §8 | | | Artist | ilst | `©ART` | Picard tag map, iTunes MP4 | | | Artist | mdta | `com.apple.quicktime.artist` | ExifTool QuickTime Keys tags | | | Artist | udta | `©ART`, `©prf` | ExifTool QuickTime UserData tags | ©prf is QuickTime's performer | | Artist | 3gpp | `perf` | 3GPP TS 26.244 §8 | performer | | AlbumArtist | ilst | `aART` | Picard tag map, iTunes MP4 | | | Album | ilst | `©alb` | Picard tag map, iTunes MP4 | | | Album | mdta | `com.apple.quicktime.album` | ExifTool QuickTime Keys tags | | | Album | udta | `©alb` | ExifTool QuickTime UserData tags | | | Album | 3gpp | `albm` | 3GPP TS 26.244 §8 | album title; the optional track number feeds TrackNumber | | Composer | ilst | `©wrt` | Picard tag map, iTunes MP4 | | | Composer | udta | `©wrt`, `©com` | ExifTool QuickTime UserData tags | ©com is QuickTime's composer | | Author | mdta | `com.apple.quicktime.author` | ExifTool QuickTime Keys tags | | | Author | udta | `©aut` | ExifTool QuickTime UserData tags | | | Author | 3gpp | `auth` | 3GPP TS 26.244 §8 | author | | Director | ilst | `©dir` | Picard tag map, iTunes MP4 | | | Director | mdta | `com.apple.quicktime.director` | ExifTool QuickTime Keys tags | | | Director | udta | `©dir` | ExifTool QuickTime UserData tags | | | Genre | ilst | `©gen`, `gnre` | Picard tag map, iTunes MP4 | gnre holds the ID3v1 genre number plus one | | Genre | mdta | `com.apple.quicktime.genre` | ExifTool QuickTime Keys tags | | | Genre | udta | `©gen` | ExifTool QuickTime UserData tags | | | Genre | 3gpp | `gnre` | 3GPP TS 26.244 §8 | | | Comment | ilst | `©cmt` | Picard tag map, iTunes MP4 | | | Comment | mdta | `com.apple.quicktime.comment` | ExifTool QuickTime Keys tags | | | Comment | udta | `©cmt` | ExifTool QuickTime UserData tags | | | Description | ilst | `desc`, `ldes` | ExifTool QuickTime UserData tags | desc is the short description, ldes the long one | | Description | mdta | `com.apple.quicktime.description` | ExifTool QuickTime Keys tags | | | Description | udta | `©inf` | ExifTool QuickTime UserData tags | ©inf is QuickTime's information text | | Description | 3gpp | `dscp` | 3GPP TS 26.244 §8 | | | Date | ilst | `©day` | Picard tag map, iTunes MP4 | stored as written, usually a year or an ISO 8601 date | | Date | mdta | `com.apple.quicktime.year` | ExifTool QuickTime Keys tags | | | Date | udta | `©day` | ExifTool QuickTime UserData tags | | | Date | 3gpp | `yrrc` | 3GPP TS 26.244 §8 | recording year | | Copyright | ilst | `cprt` | Picard tag map, iTunes MP4 | | | Copyright | mdta | `com.apple.quicktime.copyright` | ExifTool QuickTime Keys tags | | | Copyright | udta | `©cpy`, `cprt` | ExifTool QuickTime UserData tags | | | Copyright | 3gpp | `cprt` | 3GPP TS 26.244 §8 | | | Encoder | ilst | `©too` | ExifTool QuickTime UserData tags | Picard files ©too under Encoded By; the atom names the writing tool, so it is the encoder here | | Encoder | mdta | `encoder`, `com.apple.quicktime.encoder` | ExifTool QuickTime Keys tags | ffmpeg writes the bare key encoder | | Encoder | udta | `©too` | ExifTool QuickTime UserData tags | | | EncodedBy | udta | `©enc` | ExifTool QuickTime UserData tags | | | Grouping | ilst | `©grp` | Picard tag map, iTunes MP4 | | | Lyrics | ilst | `©lyr` | Picard tag map, iTunes MP4 | | | Rating | mdta | `com.apple.quicktime.rating.user` | ExifTool QuickTime Keys tags | user rating as a number | | Rating | 3gpp | `rtng` | 3GPP TS 26.244 §8 | rating text | | Keywords | mdta | `com.apple.quicktime.keywords` | ExifTool QuickTime Keys tags | | | Keywords | udta | `©key` | ExifTool QuickTime UserData tags | | | Keywords | 3gpp | `kywd` | 3GPP TS 26.244 §8 | one value per keyword | | TrackNumber | ilst | `trkn` | Picard tag map, iTunes MP4 | trkn: 16-bit number at byte 2 | | TrackNumber | 3gpp | `albm:track` | 3GPP TS 26.244 §8 | optional last byte of albm | | TrackTotal | ilst | `trkn` | Picard tag map, iTunes MP4 | trkn: 16-bit total at byte 4 | | DiscNumber | ilst | `disk` | Picard tag map, iTunes MP4 | disk: 16-bit number at byte 2 | | DiscTotal | ilst | `disk` | Picard tag map, iTunes MP4 | disk: 16-bit total at byte 4 | | Title | xmp | `http://purl.org/dc/elements/1.1/#title` | XMP specification part 1 and 2 (Dublin Core and XMP basic properties) | language alternative: the x-default item | | Artist | xmp | `http://purl.org/dc/elements/1.1/#creator` | XMP specification part 1 and 2 (Dublin Core and XMP basic properties) | first creator | | Description | xmp | `http://purl.org/dc/elements/1.1/#description` | XMP specification part 1 and 2 (Dublin Core and XMP basic properties) | | | Copyright | xmp | `http://purl.org/dc/elements/1.1/#rights` | XMP specification part 1 and 2 (Dublin Core and XMP basic properties) | | | Keywords | xmp | `http://purl.org/dc/elements/1.1/#subject` | XMP specification part 1 and 2 (Dublin Core and XMP basic properties) | | | Date | xmp | `http://ns.adobe.com/xap/1.0/#CreateDate` | XMP specification part 1 and 2 (Dublin Core and XMP basic properties) | | | Title | mkv | `TITLE` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | lowest level of 30 or above first (track before album) | | Title | mkv-info | `Title` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | Segment Info Title, after the tags | | Artist | mkv | `ARTIST`, `LEAD_PERFORMER` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | AlbumArtist | mkv | `ALBUM_ARTIST` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | not in the tag list; ffmpeg and others write it | | Album | mkv | `ALBUM` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | not in the tag list (an album title is TITLE at level 50); ffmpeg and others write ALBUM | | Composer | mkv | `COMPOSER` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Author | mkv | `WRITTEN_BY` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Director | mkv | `DIRECTOR` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Genre | mkv | `GENRE` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Comment | mkv | `COMMENT` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Description | mkv | `DESCRIPTION`, `SUMMARY`, `SYNOPSIS` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Date | mkv | `DATE_RELEASED`, `DATE_RECORDED`, `DATE` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | DATE is written by ffmpeg and others | | Copyright | mkv | `COPYRIGHT` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Encoder | mkv | `ENCODER` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | Info WritingApp last | | Encoder | mkv-info | `WritingApp` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | EncodedBy | mkv | `ENCODED_BY` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Lyrics | mkv | `LYRICS` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Rating | mkv | `RATING` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | Keywords | mkv | `KEYWORDS` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | | | TrackNumber | mkv | `PART_NUMBER`, `TRACK` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | part number at the lowest level of 30 or above; ffmpeg writes TRACK, optionally as number/total | | DiscNumber | mkv | `DISC` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | ffmpeg writes DISC, optionally as number/total | | DiscTotal | mkv | `DISC` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | the part after the slash of DISC | | TrackTotal | mkv | `TOTAL_PARTS`, `TRACK` | Matroska tag names (matroska.org/technical/tagging.html), RFC 9559 §5.1.8 | the part after the slash of TRACK when TOTAL_PARTS is absent | # Audio adapter (`RecursiveAI.OneMedia.Audio`) The adapter presents an audio track of any container OneMedia opens (ISO base media, Matroska/WebM) as a OneAudio `IPacketSource`, registers the two formats with OneAudio as `IContainerFormat`, and offers `Decode()` on an opened file. It depends on core and OneAudio only. It decodes nothing itself and has no second copy of any OneAudio code. ## Entry points - `MediaFile.Decode(...)` (extension): the file's default audio track as OneAudio's `IAudioDecoder`. The decoder uses the file's own demuxer and does not close it. - `MediaAudioExtensions.CreateRegistry()`: a OneAudio registry with the OneMedia containers first, then AAC, Opus, Vorbis, FLAC, AMR and MP3 codecs. `AddOneMedia()` adds just the containers; a registry opens the first container that matches, so adding them before `AddMp4()` makes OneMedia the MP4 reader. - `MediaTrackPacketSource`: one track as a packet source, for hosts that drive a codec themselves. ## Track choice The first audio track marked default, else the first audio track OneAudio has a decoder for. Tracks with codecs OneAudio lacks (AC-3, E-AC-3, PCM, ALAC, protected tracks) raise `UnsupportedAudioFormatException` naming the codec. ## Mapping | OneAudio member | From | |---|---| | `Codec` | `aac`, `opus`, `vorbis`, `flac`, `mp3`, `amr-nb`, `amr-wb` from the track's codec id | | `TimeScale` | the track timescale (the sample rate when the timescale does not fit an int) | | `CodecConfig` | AAC: the AudioSpecificConfig from `esds` or Matroska CodecPrivate, rebuilt from the sample entry when absent; AMR: the `damr` payload | | header packets | Opus: `OpusHead` (from `dOps` or CodecPrivate) and a synthesized `OpusTags`; Vorbis: the three headers split from Xiph lacing; FLAC: the Ogg FLAC identification packet built from `dfLa` or CodecPrivate | | `PrimingFrames` | ISO: leading empty edits and the first edit's start, in frames at the track rate, rounded down (the rule OneAudio's own MP4 reader applies). Matroska: CodecDelay for AAC. Opus reports none: the decoder takes the pre-skip from `OpusHead` | | `LengthFrames` | ISO: the movie-timeline duration after the edit list. Matroska: unknown (-1): the container states a rounded duration only. Opus: the granule total | | end of audio | Opus: from the MP4 edit list, or from Matroska DiscardPadding on the last block, as an end position on the last packet | | `SeekToFrame` | the demuxer's keyframe seek to the last packet at or before the frame | | MP3 | OneAudio reads MP3 from a byte stream only, so the track's packets are concatenated into one stream and handed to its MP3 reader | ## Limitations (`AudioSeamLimitations`) Collected while packets are read, so a host can see what OneAudio's packet interface could not carry: - `LeadingDiscardPadding`: a block asked to cut audio from its start; the audio is kept. - `MidStreamDiscardPadding`: DiscardPadding on a block that is not the track's last. Opus receives it as an end position; other codecs keep the audio. - `EndPaddingNotApplied`: the track ends with DiscardPadding for a codec whose OneAudio decoder takes no end position from a non-Ogg source (Vorbis). The last packet decodes in full. ## Comparison with OneAudio's own paths (`audiodecode`) Where OneAudio reads the same audio through its own reader, the adapter's samples are compared with it. Known, explained differences (the adapter follows the container, the other side follows what its muxer or reader does): - **Opus in Matroska**: ffmpeg's Ogg muxer drops the last block's DiscardPadding, so its twin is longer. The adapter honours it. - **AAC in Matroska**: ffmpeg's copy to MP4 loses CodecDelay, so the twin decodes the priming. The adapter trims it. - **Vorbis in Matroska**: Matroska stores no end position for Vorbis and Ogg trims the last page, so the adapter returns up to 16 more frames. - **MP3 in Matroska**: the adapter decodes every packet. A first packet holding the encoder's `Info` frame is dropped and the LAME delay and padding are trimmed by OneAudio's MP3 reader, as for an `.mp3` file; ffmpeg decodes the same track without that trim (`Info` frame as silence, delay kept) and cuts the final padding by duration. Aligned on that offset, the adapter and ffmpeg agree to float precision (largest difference 3e-6 over 17 M frames). The ffmpeg-written MP3 twin is not a usable reference: the muxer writes its own `Info` header with a skip of several frames, and both OneAudio and ffmpeg honour it. - **First frames of a stream cut mid-bitstream** (`test3.mkv`): the first two granules differ between OneAudio's and ffmpeg's MP3 decoders, which conceal the missing bit reservoir differently; everything after is equal. ## Seam gaps for OneAudio 1. Per-packet discard padding, or a public Vorbis block-size helper, so mid-stream and Vorbis end padding can be expressed. 2. An MP3 codec over packets (today MP3 needs a byte stream). 3. Decoders and `CodecId`s for AC-3, E-AC-3, PCM variants and ALAC (typed `Unsupported` today). 4. The AAC frame-length probe. 5. HE-AAC with explicit SBR/PS and more than two channels. 6. AMR samples that hold several frames (`damr` frames per sample above 1): `AmrStreamDecoder` takes one frame per packet and throws "frame type 7 needs 31 payload bytes, found 127" on a four-frame sample; OneAudio's own MP4 reader fails the same way. # Differential and I/O harness How the readers are checked against reference tools, and how probe cost is measured. The code is the console app `tools/OneMedia.Oracles` plus the scripts under `tools/oracles` and `tools/corpus`. None of it ships in a package. ## Layout State lives under `$ONEMEDIA_HOME` (default `~/onemedia`), never in the repository: | Path | Content | |---|---| | `oracles/bin` | pinned reference tools (ffprobe, ffmpeg, MP4Box, mkvinfo, mkvmerge, mkvextract, mkvpropedit, mkvalidator, mediainfo, exiftool) | | `corpus//` | fetched sample media (`blender`, `matroska-test-files`, `xiph`, `samples`) | | `corpus/generated/` | files written by the tools from synthetic input, with `MANIFEST.tsv` (hash, size, tool and version, command) | | `results//` | one JSON result per file and `summary.txt` | ## Shared machines Scripts that download or generate stop (or wait) while the file named by `ONEMEDIA_BENCH_LOCK` exists, so they never overlap a timing run. `io.sh` runs the I/O measurement as light work: it pauses on that lock and resumes after it. ## Oracles `tools/oracles/install-linux.sh` and `install-macos.sh` download release builds, check each SHA-256 against the pin in the script, unpack user-locally and write small wrappers into `oracles/bin`. They are idempotent: a matching download is reused, an unpacked tool whose pin matches is skipped. `RECORD=1` prints the hash of an entry that has no pin yet. `build-mkvalidator.sh` builds mkvalidator from its published release tarball with CMake and a C compiler and installs the executable. The tools are executed only; their files and sources are never read. ## Corpus `tools/corpus/manifest.tsv` lists every fetched file: URL, archive kind, SHA-256 and size of the download and of the file, licence or notice, and whether it may be committed (`committable`, small cuts with the notice) or is `fetch-only`. `fetch.sh` downloads what is missing, verifies pins, and stops at once if a benchmark lock appears. `generate.sh` writes the generated set; `generated-manifest.tsv` is a copy of its manifest from the last full run. Files that a pinned encoder cannot produce are reported as failed and skipped. Not available in the pinned ffmpeg build: HE-AAC (no AAC encoder beyond the native LC one). ## Packet differential `OneMedia.Oracles diff` runs `ffprobe -show_streams -of json` and `ffprobe -show_packets -show_data_hash SHA256 -of compact` (compact lines stream in constant memory; the output carries the same fields as the JSON form) and our demuxer through the format registry. Both sides are reduced to one record per packet: - track (ffprobe stream index = our track index), - `pts`, `dts`, `duration` as exact `MediaTime` in the stream's time base, unknown when ffprobe prints `N/A`, - payload size, source position (`pos`), keyframe flag, SHA-256 of the payload. Packets are compared per track in order, field by field; a count mismatch is its own difference. A difference is excused only by a line of the committed exceptions file (`tools/OneMedia.Oracles/exceptions.tsv`: format, field, scope, rule, which side follows the specification, reason). Reports separate real differences from excepted ones and keep three examples per track and field. Readers are plugged in with one line in `Readers.Create()`; structure providers with one line in `StructureCompare.Providers`. With no reader registered every file reports `unsupported` and the ffprobe packet counts. `--audio` adds the OneAudio comparisons: audio tracks of MP4/3GP against `Mp4PacketSource` (size, payload hash, decode time and duration in the media timescale), and Matroska audio against `OggPacketSource` on the Ogg twin generated in `generated/ogg` (payload size and bytes only; Ogg packets carry no container timestamps). `structure` compares the box listing of `MP4Box -diso` (type and size, in file order) with the tree a registered provider returns, and reads mkvinfo's element listing for Matroska. `selftest` checks the harness itself: a fake format that replays ffprobe's packets must produce zero differences, and one with planted mistakes (shifted times, short payloads, flipped flags, a missing packet) must produce exactly the planted counts. `--audio` ends with a summary of the OneAudio comparisons, printed separately from the ffprobe one. The `last` exception scope matches only the final packet of a track. ### ISO times ffprobe reports MP4 times on the edited timeline: leading empty edits add to every time and the first non-empty edit's start subtracts. The harness shifts our times by that amount per track (in the track timescale) before comparing, and applies the same shift to OneAudio's times, which are unshifted. ### `audiodecode` `audiodecode FILES` decodes the default audio stream through the adapter and through OneAudio's own path: its MP4 reader for MP4-family files; for Matroska, the stream copied by ffmpeg to Ogg, FLAC, M4A or MP3 and read by OneAudio. It reports frame counts, the first differing sample and the largest difference, and the frame count ffprobe's duration implies. When the sample streams differ it looks for a shift among the usual codec delays and reports `shifted-equal`; equal on the shorter length is `prefix-equal`. The stream is the one ffprobe marks default, as `MediaFile.Audio` picks. `audiopcm FILE OUT.f32` writes the adapter's decode as float32 for comparison with any other decoder (`ONEMEDIA_NATIVE=1` uses OneAudio's MP4 reader instead). The twin made by ffmpeg is not always a good reference (see docs/audio-adapter.md); decode the same stream with ffmpeg to float32 and compare with an offset search in those cases. ### `infocheck` `infocheck FILES` compares `Media.Open` (tracks, codec, size, rate, channels, language, duration) with ffprobe. Attachment streams are ignored; durations may differ by one tick of the container's own clock. Differences that remain on the 213-file corpus are all explained, none is hidden by the tool except those it accepts above: | Difference | Class | |---|---| | Fragmented files (about 20): ffprobe scans the fragments for the length; the movie header states none or a shorter one | Ours stays structural: no scan on open. `ContainerDuration` is the box value. | | `Fraunhofer...small.mp4`: ffprobe 136 s, ours 4,352 s | The movie header's figure is 32 times the tracks' (a wrong movie timescale); ours reports the header, `TracksDuration` is 136 s. | | `apple_cv.mov`, `ctk_cpro.mov` | ffprobe reports "missing mandatory atoms, broken header" and falls back to a length it derives from packets; ours is the movie header. | | `bedroses.mov`, `mp3audioproblem.mp4`, `discont-frag.mp4` | Edit-list classes (docs/isobmff.md). | | `0625rvt6-42.mov` size 352x288 vs 240x180 | ffprobe reads the H.263 picture header; the track header and sample entry say 240x180. | | `atlantis405-test.mkv` channels 6 vs 2 | The AC-3 track header says 2; ffprobe reads 5.1 from the bitstream. Ours is the header. | | `azumi.mkv`, `ticket-a_aac.mkv` stream count | ffprobe lists attachments as streams. | | `650vid.3g2` | Fixed (QCELP is mono). | | Matroska AAC with SBR | Ours is `OutputSamplingFrequency` (the rate heard); ffprobe prints the core rate. Accepted when ours is twice theirs. | | Matroska language | Ours prefers `LanguageBCP47` over `Language`; accepted when ours is a BCP 47 tag. | | WebVTT in MP4 | ffprobe calls it `data`; accepted. | ### `packetset` `packetset FILE` lists, per track, each packet's byte position and size from ours and from ffprobe and compares them as sets, so a difference in how packets are counted (edit-list repeats, split or joined PCM packets) can be told from a difference in what bytes they cover. ### Structure notes `structure` ignores children of `stsd` entries. MP4Box prints size 0 for QuickTime-style sample entries and lists `moov` before `mdat` in some large files; mkvinfo prints declared sizes where our tree clamps at the end of a truncated file. These are reported as notes, not differences. ### Known ffprobe conventions - Matroska `pos` is the start of the Block element, not of the frame payload. ## Sweeps `tools/corpus/sweep.sh isobmff|matroska|all` builds the tool and runs `diff` in chunks of at most 30 minutes (`--budget 25` stops starting files; exit 75 means files remain). `SWEEP_WRAP` gives a command prefix for each chunk, for running through a job queue. Results are per file, so a rerun resumes. ## I/O measurement `OneMedia.Oracles io` measures the cost of probing each file, in bytes read and request or call counts: - **HTTP:** a local server (`RangeServer`, plain sockets, single `Range`, 16 KiB send buffer) counts requests, ranged requests and body bytes written. A client that closes early may have received up to about one socket buffer less than counted. ffprobe runs `-show_format -show_streams`; mediainfo gets the URL; our reader goes through `RangedMediaSource` and `ObservedMediaSource` over `HttpClient`. A tool without URL support is reported as failed. - **Local files:** `strace -f -e trace=openat,close,read,pread64,readv,preadv,mmap` on the tool; bytes are the sum of returns of read-class calls on descriptors opened on the file, calls their count; mapped length is noted separately. Used for ffprobe, mediainfo and ExifTool, and for our reader through `ObservedMediaSource` over `FileMediaSource`. Page cache does not affect these counts. Timing is not measured here. ### Baseline of the reference tools `docs/io-oracle-baseline.tsv` holds the measured cost of ffprobe, MediaInfo and ExifTool on the whole corpus: bytes and request or read-call counts per file. Our own reader's columns join it as the readers land (`io` already measures them through the same sources). ffprobe and MediaInfo were run with the pinned builds listed in the installers. ExifTool timed out (five minutes) on one Matroska audio file with nested tags and is marked `n/a`. # Speed rows `bench/OneMedia.Bench` measures OneMedia against managed comparators and prints the ratio, allocations and memory for each row. It is a console app, not packed, and the only project that references the comparator packages. ## What is measured | Row | OneMedia | Comparator (the bar) | Reference tools (information) | |---|---|---|---| | `mp4-demux` | read every packet of every track | SharpMP4 | `ffmpeg -c copy -f null` | | `mp4-audio` | read the audio track only, other tracks switched off | OneAudio `Mp4PacketSource` | – | | `mp4-probe` | open, track list, duration | MetadataExtractor, TagLib#, ATL.NET (fastest of the three counts) | ffprobe, MediaInfo, ExifTool, `MP4Box -info` | | `mkv-demux` | read every packet | the Matroska package (full deserialise, then walk the blocks) | `ffmpeg -c copy -f null`, `mkvextract tracks` (first track) | | `mkv-probe` | open, track list, duration | the package's element reader: Info and Tracks read, clusters skipped | ffprobe, MediaInfo, ExifTool, `mkvmerge -J`, `mkvinfo` | The Matroska package counts a laced block as one packet, where OneMedia delivers each frame of a lace; the table flags the different counts. The comparator therefore does less work per file than OneMedia on laced audio, which favours the comparator. Files per row are listed by `list`: a small generated file, open-movie files of different shapes (1080p, fragmented, Matroska/WebM), the published Matroska test files, and the over-4 GiB files with the index at the end (MP4) and a plain over-4 GiB Matroska. A file missing from the corpus is skipped and said so. **The work is the same for everyone.** A packet read counts the packet, adds its size and XORs its first and last byte, so every payload is delivered to the caller and not skipped. The counts are printed next to the times; a comparator whose packet count differs from OneMedia's is flagged in the table (Matroska laced blocks, files the package cannot read). The probe rows read what each library offers: tracks and duration, plus the title and first artist where the library has them; OneMedia's native tags join its probe when its tag layer lands. **Calls.** Each comparator is called as its published README shows, with a 64 KiB file buffer. Where the README does not cover a row the obvious public call is used: SharpMP4 reads track by track with `ReadSample`; MetadataExtractor uses `ImageMetadataReader.ReadMetadata(stream)` and reads every tag's description; TagLib# uses `File.Create(path)`; ATL.NET uses `new Track(path)`. ATL.NET is audio-oriented and may refuse video files; a refusal is shown as `fail`. ## Method - One fresh process per (row, implementation, file), so peak memory belongs to that work alone. The parent reads each file once before its rows so all implementations start with it in the page cache; the tables say so. - In the process: a cold first run (JIT and type loading included), then a warm-up of at least 30 runs and 1.5 s for every implementation alike, so that the runtime has promoted the hot methods to optimised code before any sample (tier-0, then the instrumented stage, then optimised, each behind a 100 ms quiet timer). Then one sizing run and 5 samples, each the mean of enough back-to-back runs to take about 50 ms. The median is reported, and the range is kept in `results.tsv`. Probe rows (`*-probe`) are sampled this way on every file, since probe work does not grow with the file. Demux-style rows on files over 1 GiB get the cold run and then 3 samples of one run each. The cold first run is shown apart in the note column, for the comparators as well; it is a published, non-gating number (the first open in a process: what a one-off `info` call or a per-file open pays). - Allocation is `GC.GetAllocatedBytesForCurrentThread` per run, and per packet. Peak memory is the process's peak working set; the working set before any work is printed beside it. - Reference tools run as processes under `/usr/bin/time`, three times (once over 1 GiB): wall time measured by the parent, peak resident set from `time`. Their start-up cost is part of the figure. - Ratio = OneMedia's median ÷ the fastest managed comparator's median for that file. The bar is a geometric mean ≤ 1.00× per row and no file above 1.25×. Rows with no managed comparator pass. - Every run records CPU, logical cores, ISA flags, storage of the corpus, runtime, restored package versions, tool versions and the one-minute load average before, during (mean and max of 5 s samples) and after. ## Running it ``` dotnet run -c Release --project bench/OneMedia.Bench -- run --rows 0.1 --out results/x64 [--corpus DIR] ``` `--rows` takes a stage (`0.1`) or row ids. `--files a,b` keeps files whose path contains one of the strings, `--no-tools` leaves out the reference tools, `--reps` and `--tool-reps` set the repetitions, `--budget MIN` stops starting new cells after that many minutes (exit code 75) and `--resume` continues from `results.tsv`. `report` rebuilds `report.md`; `prepare` derives the fragmented MP4 (done by `run`); `list` shows rows and files. The corpus is `$ONEMEDIA_HOME/corpus` (default `~/onemedia/corpus`). Timings are only comparable between runs on a quiet machine. The published run is one command per machine under the exclusive bench queue. ## JIT tiers: steady state and first open Each cell runs in a fresh process. The runtime starts every method in unoptimised tier-0 code and promotes hot ones only after a 100 ms quiet period and a background compile, with a second instrumented stage under dynamic PGO. Measuring before that climb finishes measures tier-0 code, which punishes libraries that do more of their own work per call. The samples are therefore taken after a warm-up long enough for every implementation to have tiered up (see Method), and the library carries no tiering overrides. The first call in a process is a separate matter: it is JIT dominated, it is what a one-off open pays, and it is reported as "cold first run" for OneMedia and for every comparator. It is published but does not enter the ratio. To see the unwarmed regime for a cell, run `OneMedia.Bench child ROW IMPL REL CORPUS REPS` under `DOTNET_TieredCompilation=0` (every method fully optimised) or `DOTNET_TieredPGO=0`. ## Provenance of the comparator calls Every comparator call was chosen from the package's published README, its package page and its public type and member signatures. No comparator's source, decompiled code or unpacked package contents was read, and no reference tool's source was read; the tools run as released binaries. ## Looking at a cell - `OneMedia.Bench once ROW IMPL REL [CORPUS [TIMES]]` runs one cell, optionally in a loop, and prints the mean time of a run. - `bench/iocount.sh DLL ROW IMPL REL` counts the read calls and bytes a cell makes on its input file (strace). - `bench/profile.sh DLL ROW IMPL REL TIMES` samples a looped cell with `dotnet-trace` and prints self and inclusive time by method. It needs `dotnet-trace` in `~/onemedia/u5-bench/tools` (`dotnet tool install dotnet-trace --tool-path ...`). # OneMedia speed rows ## Run 2026-11-01 00:49Z - Host: x64 build worker; AMD Ryzen 9 7950X3D 16-Core Processor; 32 logical cores; Linux Mint 22.3 X64 - ISA: sse4.2 avx2 bmi2 avx512f avx512bw - Storage of the corpus: /dev/nvme0n1p4; every file is read once before its rows, so it is in the page cache for all of them - .NET: 10.0.13 (.NET 10.0.13) - Load average (1 min): before 9.62; during mean 5.12, max 9.62 over 32 samples at 5 s; after 2.83 - Elapsed: 2.7 min; repetitions 5 (managed), 3 (tools); files over 1 GiB get one warm-up run and 3 samples of one run each, and tools once - Packages as restored: SharpMP4 0.3.2; RecursiveAI.OneAudio 1.4.0; MetadataExtractor 2.9.3; TagLibSharp 2.3.0.0; z440.atl.core 7.19; Matroska 0.0.12 - Tools: ffmpeg version 7.0.2-static https://johnvansickle.com/ffmpeg/ Copyrig; MP4Box - GPAC version 26.07-rev0-ga07cbfff-master; mkvmerge v103.0 ('The Darkness') 64-bit; MediaInfo MediaInfo Command line,; ExifTool 13.60 Ratio is OneMedia's median over the fastest managed comparator's median on the same file (below 1.00× is faster). Peak is the process's peak resident set; Baseline is its resident set before any work, so Peak − Baseline is what the work added. ## mp4-demux: MP4/MOV: read every packet of every track | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia | 33.7 µs | 0.02× | 23.6 KiB | 78.2 B | 309 | 0.2 | - | 88.3 | 27.8 | cold first run 19.88 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | SharpMP4 | 1.45 ms | 1.00× | 1.4 MiB | 4620.0 B | 309 | 0.2 | - | 103.8 | 27.8 | cold first run 52.81 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia | 2.73 ms | 0.12× | 57.6 KiB | 3.2 B | 18671 | 26.5 | - | 65.1 | 27.8 | cold first run 26.18 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | SharpMP4 | 23.25 ms | 1.00× | 42.6 MiB | 2390.1 B | 18671 | 26.5 | - | 103.3 | 27.8 | cold first run 109.41 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia | 74.86 ms | 0.20× | 2.1 MiB | 52.7 B | 42276 | 691.1 | - | 42.5 | 27.8 | cold first run 110.10 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | SharpMP4 | 374.22 ms | 1.00× | 801.0 MiB | 19866.2 B | 42276 | 691.1 | - | 105.5 | 27.8 | cold first run 743.98 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia | 52.92 ms | - | 2.1 MiB | 51.7 B | 42276 | 691.1 | - | 40.0 | 27.8 | cold first run 85.39 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | SharpMP4 | fail | - | - | - | - | - | - | 103.9 | 27.9 | IndexOutOfRangeException: Index was outside the bounds of the array. | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | OneMedia | 267.01 ms | 0.21× | 1.9 MiB | 30.6 B | 64576 | 4172.2 | - | 36.9 | 27.8 | cold first run 298.65 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | SharpMP4 | 1.30 s | 1.00× | 4334.8 MiB | 70388.5 B | 64576 | 4172.2 | - | 112.6 | 27.9 | cold first run 1.50 s | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/big_buck_bunny_1080p_h264.mov | ffmpeg -c copy -f null | 194.60 ms | 193.15 ms – 194.94 ms | 24.3 | | | generated/bench/frag-bbb-1080p.mp4 | ffmpeg -c copy -f null | 195.05 ms | 191.19 ms – 197.23 ms | 38.4 | | | generated/large/over4gb-moov-at-end.mp4 | ffmpeg -c copy -f null | 609.22 ms | 609.22 ms – 609.22 ms | 25.2 | | | generated/mp4/h264-aac.mp4 | ffmpeg -c copy -f null | 6.30 ms | 6.23 ms – 7.07 ms | 17.1 | | | samples/MPEG-4/video.mp4 | ffmpeg -c copy -f null | 72.00 ms | 55.21 ms – 81.73 ms | 14.9 | | ## mp4-audio: MP4/MOV: read the audio track's packets | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia | 30.7 µs | 0.82× | 23.6 KiB | 127.8 B | 189 | 0.0 | - | 88.3 | 27.8 | cold first run 19.87 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneAudio Mp4PacketSource | 37.6 µs | 1.00× | 71.6 KiB | 388.2 B | 189 | 0.0 | - | 89.5 | 27.7 | cold first run 18.08 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia, length asked of the file each time | 32.8 µs | 0.87× | 23.7 KiB | 128.3 B | 189 | 0.0 | - | 92.5 | 27.7 | cold first run 20.30 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia | 392.4 µs | 0.63× | 57.6 KiB | 8.2 B | 7200 | 1.9 | - | 87.7 | 27.8 | cold first run 21.16 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneAudio Mp4PacketSource | 627.6 µs | 1.00× | 97.6 KiB | 13.9 B | 7200 | 1.9 | - | 86.1 | 27.9 | cold first run 9.88 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia, length asked of the file each time | 397.3 µs | 0.63× | 57.7 KiB | 8.2 B | 7200 | 1.9 | - | 88.2 | 27.9 | cold first run 20.18 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia | 5.39 ms | - | 62.4 KiB | 2.3 B | 27960 | 31.1 | - | 53.8 | 27.8 | cold first run 32.98 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneAudio Mp4PacketSource | fail | - | - | - | - | - | - | 31.7 | 27.8 | UnsupportedAudioFormatException: Audio track 0 uses a QuickTime sound description version this library does not read. | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia, length asked of the file each time | 5.43 ms | - | 62.5 KiB | 2.3 B | 27960 | 31.1 | - | 52.6 | 27.8 | cold first run 31.44 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia | 4.78 ms | - | 21.6 KiB | 0.8 B | 27960 | 31.1 | - | 44.5 | 27.9 | cold first run 36.04 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneAudio Mp4PacketSource | fail | - | - | - | - | - | - | 31.8 | 27.9 | UnsupportedAudioFormatException: Fragmented MP4 files (mvex boxes) are not supported. | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia, length asked of the file each time | 5.13 ms | - | 21.7 KiB | 0.8 B | 27960 | 31.1 | - | 44.3 | 27.8 | cold first run 36.80 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | OneMedia | 37.46 ms | 0.29× | 46.9 KiB | 1.2 B | 39376 | 9.7 | - | 33.3 | 27.7 | cold first run 60.58 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | OneAudio Mp4PacketSource | 128.43 ms | 1.00× | 1020.5 KiB | 26.5 B | 39376 | 9.7 | - | 34.0 | 27.9 | cold first run 135.50 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | OneMedia, length asked of the file each time | 37.18 ms | 0.29× | 47.0 KiB | 1.2 B | 39376 | 9.7 | - | 33.4 | 27.8 | cold first run 59.76 ms | ## mp4-probe: MP4/MOV: open, track list, duration, tags | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia | 11.8 µs | 0.16× | 23.6 KiB | - | - | - | 2 | 90.5 | 27.9 | cold first run 18.41 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | ATL.NET | 122.8 µs | 1.66× | 146.6 KiB | - | - | - | 0 | 94.0 | 27.8 | cold first run 44.89 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | MetadataExtractor | 83.6 µs | 1.13× | 77.7 KiB | - | - | - | 48 | 87.5 | 27.8 | cold first run 20.84 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | TagLib# | 73.8 µs | 1.00× | 36.7 KiB | - | - | - | 2 | 84.1 | 28.0 | fastest comparator; cold first run 20.11 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia | 17.6 µs | 0.12× | 57.6 KiB | - | - | - | 6 | 91.7 | 27.8 | cold first run 16.43 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | ATL.NET | 423.0 µs | 2.82× | 151.7 KiB | - | - | - | 0 | 91.4 | 27.9 | cold first run 41.22 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | MetadataExtractor | 159.5 µs | 1.06× | 91.8 KiB | - | - | - | 96 | 85.6 | 27.8 | cold first run 11.21 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | TagLib# | 150.0 µs | 1.00× | 78.1 KiB | - | - | - | 2 | 84.6 | 27.7 | fastest comparator; cold first run 11.07 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia | 38.4 µs | 0.24× | 62.4 KiB | - | - | - | 3 | 95.4 | 27.8 | cold first run 19.34 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | ATL.NET | 1.02 ms | 6.36× | 229.2 KiB | - | - | - | 0 | 91.4 | 27.8 | cold first run 45.04 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | MetadataExtractor | 159.9 µs | 1.00× | 87.7 KiB | - | - | - | 61 | 89.1 | 27.8 | fastest comparator; cold first run 12.87 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | TagLib# | fail | - | - | - | - | - | - | 31.7 | 27.9 | UnsupportedFormatException: blender/big_buck_bunny_1080p_h264.mov (taglib/mov) | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia | 10.3 µs | 0.01× | 20.9 KiB | - | - | - | 3 | 91.0 | 27.9 | cold first run 16.04 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | ATL.NET | 6.17 ms | 3.64× | 483.7 KiB | - | - | - | 0 | 94.8 | 27.8 | cold first run 54.53 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | MetadataExtractor | 5.04 ms | 2.97× | 183.6 KiB | - | - | - | 60 | 84.8 | 27.9 | cold first run 15.63 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | TagLib# | 1.70 ms | 1.00× | 521.2 KiB | - | - | - | 2 | 89.2 | 27.8 | fastest comparator; cold first run 12.88 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | OneMedia | 29.5 µs | 0.40× | 46.4 KiB | - | - | - | 2 | 95.1 | 27.8 | cold first run 17.84 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | ATL.NET | 1.07 ms | 14.61× | 146.5 KiB | - | - | - | 0 | 92.5 | 27.9 | cold first run 41.99 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | MetadataExtractor | 86.6 µs | 1.19× | 77.6 KiB | - | - | - | 48 | 85.5 | 27.8 | cold first run 10.57 ms | | generated/large/over4gb-moov-at-end.mp4 | 4.08 GiB | TagLib# | 73.0 µs | 1.00× | 36.0 KiB | - | - | - | 2 | 84.1 | 27.9 | fastest comparator; cold first run 10.52 ms | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/big_buck_bunny_1080p_h264.mov | ExifTool | 68.20 ms | 65.56 ms – 72.36 ms | 23.2 | | | blender/big_buck_bunny_1080p_h264.mov | MP4Box -info | 34.84 ms | 34.34 ms – 37.39 ms | 50.8 | | | blender/big_buck_bunny_1080p_h264.mov | MediaInfo | 19.06 ms | 18.95 ms – 20.21 ms | 14.2 | | | blender/big_buck_bunny_1080p_h264.mov | ffprobe -show_format -show_streams | 10.05 ms | 10.02 ms – 11.79 ms | 23.4 | | | generated/bench/frag-bbb-1080p.mp4 | ExifTool | 139.76 ms | 138.08 ms – 141.02 ms | 23.0 | | | generated/bench/frag-bbb-1080p.mp4 | MP4Box -info | 38.38 ms | 37.43 ms – 40.29 ms | 53.2 | | | generated/bench/frag-bbb-1080p.mp4 | MediaInfo | 26.46 ms | 25.69 ms – 27.79 ms | 14.1 | | | generated/bench/frag-bbb-1080p.mp4 | ffprobe -show_format -show_streams | 39.43 ms | 39.09 ms – 40.19 ms | 37.3 | | | generated/large/over4gb-moov-at-end.mp4 | ExifTool | 68.26 ms | 68.26 ms – 68.26 ms | 26.7 | | | generated/large/over4gb-moov-at-end.mp4 | MP4Box -info | 38.68 ms | 38.68 ms – 38.68 ms | 50.1 | | | generated/large/over4gb-moov-at-end.mp4 | MediaInfo | 22.26 ms | 22.26 ms – 22.26 ms | 17.5 | | | generated/large/over4gb-moov-at-end.mp4 | ffprobe -show_format -show_streams | 19.04 ms | 19.04 ms – 19.04 ms | 20.5 | | | generated/mp4/h264-aac.mp4 | ExifTool | 61.72 ms | 60.31 ms – 64.83 ms | 22.2 | | | generated/mp4/h264-aac.mp4 | MP4Box -info | 34.86 ms | 33.64 ms – 36.08 ms | 50.4 | | | generated/mp4/h264-aac.mp4 | MediaInfo | 9.64 ms | 9.49 ms – 9.68 ms | 11.2 | | | generated/mp4/h264-aac.mp4 | ffprobe -show_format -show_streams | 5.71 ms | 5.52 ms – 6.15 ms | 16.2 | | | samples/MPEG-4/video.mp4 | ExifTool | 97.51 ms | 96.71 ms – 97.58 ms | 32.9 | | | samples/MPEG-4/video.mp4 | MP4Box -info | 33.17 ms | 32.87 ms – 34.88 ms | 50.2 | | | samples/MPEG-4/video.mp4 | MediaInfo | 10.76 ms | 10.14 ms – 11.73 ms | 11.4 | | | samples/MPEG-4/video.mp4 | ffprobe -show_format -show_streams | 4.67 ms | 4.48 ms – 4.81 ms | 13.8 | | ## mkv-demux: Matroska/WebM: read every packet | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mkv/h264-aac.mkv | 0.2 MiB | OneMedia | 86.9 µs | 0.21× | 1.0 MiB | 3513.4 B | 309 | 0.2 | - | 44.5 | 27.8 | cold first run 22.11 ms | | generated/mkv/h264-aac.mkv | 0.2 MiB | OneMedia, length asked of the file each time | 258.1 µs | 0.62× | 1.0 MiB | 3513.7 B | 309 | 0.2 | - | 44.7 | 27.9 | cold first run 22.77 ms | | generated/mkv/h264-aac.mkv | 0.2 MiB | StefH Matroska | 418.7 µs | 1.00× | 671.5 KiB | 2225.3 B | 309 | 0.2 | - | 88.8 | 27.8 | cold first run 27.48 ms | | matroska-test-files/test1.mkv | 22.3 MiB | OneMedia | 2.26 ms | 0.29× | 1.0 MiB | 189.4 B | 5735 | 22.2 | - | 45.2 | 27.9 | cold first run 27.78 ms | | matroska-test-files/test1.mkv | 22.3 MiB | OneMedia, length asked of the file each time | 3.69 ms | 0.47× | 1.0 MiB | 189.4 B | 5735 | 22.2 | - | 45.4 | 27.8 | cold first run 29.24 ms | | matroska-test-files/test1.mkv | 22.3 MiB | StefH Matroska | 7.78 ms | 1.00× | 45.3 MiB | 18416.7 B | 2582 | 22.2 | - | 110.7 | 27.8 | packet count differs from OneMedia's 5735; cold first run 44.44 ms | | matroska-test-files/test5.mkv | 30.3 MiB | OneMedia | 2.51 ms | 0.23× | 1.0 MiB | 254.5 B | 4298 | 30.3 | - | 45.1 | 27.9 | cold first run 26.77 ms | | matroska-test-files/test5.mkv | 30.3 MiB | OneMedia, length asked of the file each time | 3.43 ms | 0.31× | 1.0 MiB | 254.5 B | 4298 | 30.3 | - | 44.5 | 27.7 | cold first run 27.75 ms | | matroska-test-files/test5.mkv | 30.3 MiB | StefH Matroska | 10.91 ms | 1.00× | 61.6 MiB | 41194.0 B | 1568 | 30.3 | - | 116.0 | 27.9 | packet count differs from OneMedia's 4298; cold first run 49.00 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | OneMedia | 124.37 ms | 0.31× | 1.0 MiB | 22.2 B | 49323 | 1125.2 | - | 39.3 | 27.9 | cold first run 143.64 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | OneMedia, length asked of the file each time | 134.24 ms | 0.34× | 1.0 MiB | 22.2 B | 49323 | 1125.2 | - | 40.7 | 27.8 | cold first run 157.67 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | StefH Matroska | 398.11 ms | 1.00× | 2265.5 MiB | 93603.5 B | 25379 | 1125.2 | - | 1597.1 | 27.9 | packet count differs from OneMedia's 49323; cold first run 828.64 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | OneMedia | 50.31 ms | 0.25× | 1.0 MiB | 13.7 B | 80000 | 544.6 | - | 40.9 | 27.9 | cold first run 97.10 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | OneMedia, length asked of the file each time | 63.90 ms | 0.32× | 1.0 MiB | 13.7 B | 80000 | 544.6 | - | 41.3 | 27.8 | cold first run 114.03 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | StefH Matroska | 199.94 ms | 1.00× | 1097.5 MiB | 44893.5 B | 25634 | 544.7 | - | 1197.3 | 27.9 | packet count differs from OneMedia's 80000; cold first run 460.46 ms | | generated/large/over4gb.mkv | 4.14 GiB | OneMedia | 269.85 ms | 0.28× | 1.0 MiB | 16.8 B | 64596 | 4239.4 | - | 39.6 | 27.9 | cold first run 343.81 ms | | generated/large/over4gb.mkv | 4.14 GiB | OneMedia, length asked of the file each time | 304.02 ms | 0.31× | 1.0 MiB | 16.8 B | 64596 | 4239.4 | - | 39.8 | 27.9 | cold first run 387.46 ms | | generated/large/over4gb.mkv | 4.14 GiB | StefH Matroska | 975.70 ms | 1.00× | 8516.7 MiB | 138250.7 B | 64596 | 4239.4 | - | 6391.6 | 27.9 | cold first run 2.71 s | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/Sintel.2010.1080p.mkv | ffmpeg -c copy -f null | 273.25 ms | 273.25 ms – 273.25 ms | 20.1 | | | blender/Sintel.2010.1080p.mkv | mkvextract tracks (first track) | 264.63 ms | 264.63 ms – 264.63 ms | 23.9 | | | blender/tears_of_steel_1080p.webm | ffmpeg -c copy -f null | 220.75 ms | 215.10 ms – 233.47 ms | 13.8 | | | blender/tears_of_steel_1080p.webm | mkvextract tracks (first track) | 108.88 ms | 108.01 ms – 111.14 ms | 23.9 | | | generated/large/over4gb.mkv | ffmpeg -c copy -f null | 590.66 ms | 590.66 ms – 590.66 ms | 24.0 | | | generated/large/over4gb.mkv | mkvextract tracks (first track) | 1.28 s | 1.28 s – 1.28 s | 27.2 | | | generated/mkv/h264-aac.mkv | ffmpeg -c copy -f null | 6.56 ms | 6.04 ms – 7.00 ms | 17.1 | | | generated/mkv/h264-aac.mkv | mkvextract tracks (first track) | 21.16 ms | 20.87 ms – 22.57 ms | 17.4 | | | matroska-test-files/test1.mkv | ffmpeg -c copy -f null | 23.51 ms | 22.84 ms – 30.06 ms | 13.9 | | | matroska-test-files/test1.mkv | mkvextract tracks (first track) | 28.20 ms | 27.52 ms – 28.77 ms | 20.6 | | | matroska-test-files/test5.mkv | ffmpeg -c copy -f null | 36.58 ms | 36.32 ms – 36.99 ms | 23.1 | | | matroska-test-files/test5.mkv | mkvextract tracks (first track) | 29.56 ms | 28.97 ms – 30.31 ms | 23.2 | | ## mkv-probe: Matroska/WebM: open, track list, duration | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mkv/h264-aac.mkv | 0.2 MiB | OneMedia | 8.3 µs | 0.64× | 36.1 KiB | - | - | - | 2 | 94.0 | 27.8 | cold first run 13.41 ms | | generated/mkv/h264-aac.mkv | 0.2 MiB | NEbml element reader | 13.0 µs | 1.00× | 68.7 KiB | - | - | - | 2 | 86.5 | 27.8 | cold first run 2.68 ms | | matroska-test-files/test1.mkv | 22.3 MiB | OneMedia | 8.1 µs | 0.66× | 36.5 KiB | - | - | - | 2 | 93.9 | 27.8 | cold first run 13.21 ms | | matroska-test-files/test1.mkv | 22.3 MiB | NEbml element reader | 12.3 µs | 1.00× | 68.3 KiB | - | - | - | 2 | 86.5 | 27.8 | cold first run 2.72 ms | | matroska-test-files/test5.mkv | 30.3 MiB | OneMedia | 11.5 µs | 0.67× | 44.0 KiB | - | - | - | 11 | 94.0 | 27.8 | cold first run 13.96 ms | | matroska-test-files/test5.mkv | 30.3 MiB | NEbml element reader | 17.1 µs | 1.00× | 72.8 KiB | - | - | - | 11 | 90.9 | 27.8 | cold first run 2.72 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | OneMedia | 11.7 µs | 0.65× | 44.7 KiB | - | - | - | 12 | 98.0 | 27.9 | cold first run 13.70 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | NEbml element reader | 18.0 µs | 1.00× | 73.3 KiB | - | - | - | 12 | 90.6 | 27.8 | cold first run 2.84 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | OneMedia | 10.4 µs | 0.83× | 43.9 KiB | - | - | - | 2 | 94.1 | 27.9 | cold first run 13.94 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | NEbml element reader | 12.6 µs | 1.00× | 68.2 KiB | - | - | - | 2 | 86.6 | 27.8 | cold first run 2.68 ms | | generated/large/over4gb.mkv | 4.14 GiB | OneMedia | 8.4 µs | 0.67× | 36.1 KiB | - | - | - | 2 | 94.1 | 27.8 | cold first run 12.75 ms | | generated/large/over4gb.mkv | 4.14 GiB | NEbml element reader | 12.6 µs | 1.00× | 68.6 KiB | - | - | - | 2 | 86.6 | 27.9 | cold first run 2.84 ms | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/Sintel.2010.1080p.mkv | ExifTool | 51.78 ms | 51.78 ms – 51.78 ms | 18.4 | | | blender/Sintel.2010.1080p.mkv | MediaInfo | 99.02 ms | 99.02 ms – 99.02 ms | 11.0 | | | blender/Sintel.2010.1080p.mkv | ffprobe -show_format -show_streams | 7.20 ms | 7.20 ms – 7.20 ms | 19.0 | | | blender/Sintel.2010.1080p.mkv | mkvinfo | 21.20 ms | 21.20 ms – 21.20 ms | 16.2 | | | blender/Sintel.2010.1080p.mkv | mkvmerge -J | 31.88 ms | 31.88 ms – 31.88 ms | 21.2 | | | blender/tears_of_steel_1080p.webm | ExifTool | 51.61 ms | 50.69 ms – 52.69 ms | 18.4 | | | blender/tears_of_steel_1080p.webm | MediaInfo | 20.97 ms | 19.63 ms – 21.53 ms | 11.1 | | | blender/tears_of_steel_1080p.webm | ffprobe -show_format -show_streams | 3.17 ms | 2.74 ms – 3.53 ms | 12.2 | | | blender/tears_of_steel_1080p.webm | mkvinfo | 20.61 ms | 20.16 ms – 20.80 ms | 16.0 | | | blender/tears_of_steel_1080p.webm | mkvmerge -J | 28.79 ms | 28.00 ms – 29.40 ms | 21.1 | | | generated/large/over4gb.mkv | ExifTool | 49.74 ms | 49.74 ms – 49.74 ms | 17.9 | | | generated/large/over4gb.mkv | MediaInfo | 138.35 ms | 138.35 ms – 138.35 ms | 11.7 | | | generated/large/over4gb.mkv | ffprobe -show_format -show_streams | 65.86 ms | 65.86 ms – 65.86 ms | 23.5 | | | generated/large/over4gb.mkv | mkvinfo | 25.07 ms | 25.07 ms – 25.07 ms | 20.5 | | | generated/large/over4gb.mkv | mkvmerge -J | 53.30 ms | 53.30 ms – 53.30 ms | 34.7 | | | generated/mkv/h264-aac.mkv | ExifTool | 49.84 ms | 48.78 ms – 52.98 ms | 18.5 | | | generated/mkv/h264-aac.mkv | MediaInfo | 9.42 ms | 8.99 ms – 9.89 ms | 10.5 | | | generated/mkv/h264-aac.mkv | ffprobe -show_format -show_streams | 5.71 ms | 5.55 ms – 5.76 ms | 16.0 | | | generated/mkv/h264-aac.mkv | mkvinfo | 20.96 ms | 19.48 ms – 28.38 ms | 15.7 | | | generated/mkv/h264-aac.mkv | mkvmerge -J | 22.78 ms | 21.83 ms – 22.91 ms | 19.6 | | | matroska-test-files/test1.mkv | ExifTool | 49.46 ms | 49.04 ms – 49.63 ms | 18.4 | | | matroska-test-files/test1.mkv | MediaInfo | 9.85 ms | 9.51 ms – 10.28 ms | 11.2 | | | matroska-test-files/test1.mkv | ffprobe -show_format -show_streams | 3.19 ms | 3.19 ms – 3.60 ms | 12.6 | | | matroska-test-files/test1.mkv | mkvinfo | 21.05 ms | 20.99 ms – 21.13 ms | 16.2 | | | matroska-test-files/test1.mkv | mkvmerge -J | 28.18 ms | 27.46 ms – 38.96 ms | 22.2 | | | matroska-test-files/test5.mkv | ExifTool | 84.47 ms | 83.92 ms – 85.41 ms | 29.9 | | | matroska-test-files/test5.mkv | MediaInfo | 19.42 ms | 19.04 ms – 19.61 ms | 10.9 | | | matroska-test-files/test5.mkv | ffprobe -show_format -show_streams | 20.03 ms | 19.76 ms – 21.76 ms | 21.8 | | | matroska-test-files/test5.mkv | mkvinfo | 20.35 ms | 20.27 ms – 20.52 ms | 17.8 | | | matroska-test-files/test5.mkv | mkvmerge -J | 30.09 ms | 29.54 ms – 30.81 ms | 25.1 | | ## Against the bar Bar: geometric mean of the ratios ≤ 1.00× and no file worse than 1.25×, against the fastest managed comparator per file. | Row | Files with a ratio | Geomean | Worst | Verdict | |---|---:|---:|---:|---| | mp4-demux | 4/5 | 0.10× | 0.21× | at the bar | | mp4-audio | 3/5 | 0.53× | 0.82× | at the bar | | mp4-probe | 5/5 | 0.10× | 0.40× | at the bar | | mkv-demux | 6/6 | 0.26× | 0.31× | at the bar | | mkv-probe | 6/6 | 0.69× | 0.83× | at the bar | # OneMedia speed rows ## Run 2026-11-01 00:16Z - Host: Mac mini; Apple M2; 8 logical cores; macOS 27.0.1 Arm64 - ISA: neon crc32 aes dotprod - Storage of the corpus: /dev/disk3s5; every file is read once before its rows, so it is in the page cache for all of them - .NET: 10.0.11 (.NET 10.0.11) - Load average (1 min): before 2.32; during mean 2.18, max 2.41 over 27 samples at 5 s; after 2.02 - Elapsed: 2.2 min; repetitions 5 (managed), 3 (tools); files over 1 GiB get one warm-up run and 3 samples of one run each, and tools once - Packages as restored: SharpMP4 0.3.2; RecursiveAI.OneAudio 1.4.0; MetadataExtractor 2.9.3; TagLibSharp 2.3.0.0; z440.atl.core 7.19; Matroska 0.0.12 - Tools: ffmpeg version 8.0 Copyright (c) 2000-2025 the FFmpeg developers; MP4Box - GPAC version 26.07-revrelease; mkvmerge v103.0 ('The Darkness') 64-bit; MediaInfo MediaInfo Command line,; ExifTool 13.60 Ratio is OneMedia's median over the fastest managed comparator's median on the same file (below 1.00× is faster). Peak is the process's peak resident set; Baseline is its resident set before any work, so Peak − Baseline is what the work added. ## mp4-demux: MP4/MOV: read every packet of every track | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia | 39.6 µs | 0.02× | 23.6 KiB | 78.2 B | 309 | 0.2 | - | - | 39.9 | cold first run 17.47 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | SharpMP4 | 1.68 ms | 1.00× | 1.4 MiB | 4744.3 B | 309 | 0.2 | - | - | 40.0 | cold first run 55.09 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia | 2.40 ms | 0.13× | 57.6 KiB | 3.2 B | 18671 | 26.5 | - | - | 40.0 | cold first run 24.10 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | SharpMP4 | 18.11 ms | 1.00× | 42.6 MiB | 2390.8 B | 18671 | 26.5 | - | - | 39.9 | cold first run 88.98 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia | 53.99 ms | 0.20× | 2.1 MiB | 52.7 B | 42276 | 691.1 | - | - | 39.9 | cold first run 81.90 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | SharpMP4 | 271.02 ms | 1.00× | 801.0 MiB | 19866.2 B | 42276 | 691.1 | - | - | 40.0 | cold first run 779.67 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia | 53.94 ms | - | 2.1 MiB | 51.7 B | 42276 | 691.1 | - | - | 39.9 | cold first run 94.61 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | SharpMP4 | fail | - | - | - | - | - | - | - | 40.0 | IndexOutOfRangeException: Index was outside the bounds of the array. | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/big_buck_bunny_1080p_h264.mov | ffmpeg -c copy -f null | 171.54 ms | 171.30 ms – 181.61 ms | 45.8 | | | generated/bench/frag-bbb-1080p.mp4 | ffmpeg -c copy -f null | 198.48 ms | 190.23 ms – 199.11 ms | 58.6 | | | generated/mp4/h264-aac.mp4 | ffmpeg -c copy -f null | 27.89 ms | 27.47 ms – 102.90 ms | 18.3 | | | samples/MPEG-4/video.mp4 | ffmpeg -c copy -f null | 51.35 ms | 50.31 ms – 53.70 ms | 17.1 | | ## mp4-audio: MP4/MOV: read the audio track's packets | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia | 31.2 µs | 0.40× | 23.6 KiB | 127.8 B | 189 | 0.0 | - | - | 40.0 | cold first run 18.15 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneAudio Mp4PacketSource | 78.8 µs | 1.00× | 71.6 KiB | 388.2 B | 189 | 0.0 | - | - | 40.0 | cold first run 13.41 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia, length asked of the file each time | 41.3 µs | 0.52× | 23.7 KiB | 128.3 B | 189 | 0.0 | - | - | 40.0 | cold first run 17.51 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia | 362.1 µs | 0.61× | 57.6 KiB | 8.2 B | 7200 | 1.9 | - | - | 40.0 | cold first run 53.84 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneAudio Mp4PacketSource | 595.8 µs | 1.00× | 97.6 KiB | 13.9 B | 7200 | 1.9 | - | - | 40.0 | cold first run 57.34 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia, length asked of the file each time | 354.9 µs | 0.60× | 57.7 KiB | 8.2 B | 7200 | 1.9 | - | - | 40.0 | cold first run 19.56 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia | 3.88 ms | - | 62.4 KiB | 2.3 B | 27960 | 31.1 | - | - | 40.0 | cold first run 33.10 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneAudio Mp4PacketSource | fail | - | - | - | - | - | - | - | 39.8 | UnsupportedAudioFormatException: Audio track 0 uses a QuickTime sound description version this library does not read. | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia, length asked of the file each time | 3.91 ms | - | 62.5 KiB | 2.3 B | 27960 | 31.1 | - | - | 39.9 | cold first run 28.07 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia | 4.31 ms | - | 21.6 KiB | 0.8 B | 27960 | 31.1 | - | - | 40.0 | cold first run 34.13 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneAudio Mp4PacketSource | fail | - | - | - | - | - | - | - | 39.9 | UnsupportedAudioFormatException: Fragmented MP4 files (mvex boxes) are not supported. | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia, length asked of the file each time | 4.53 ms | - | 21.7 KiB | 0.8 B | 27960 | 31.1 | - | - | 40.0 | cold first run 34.13 ms | ## mp4-probe: MP4/MOV: open, track list, duration, tags | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mp4/h264-aac.mp4 | 0.2 MiB | OneMedia | 16.0 µs | 0.28× | 23.6 KiB | - | - | - | 2 | - | 40.0 | cold first run 16.12 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | ATL.NET | 100.3 µs | 1.73× | 146.6 KiB | - | - | - | 0 | - | 40.0 | cold first run 37.90 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | MetadataExtractor | 73.2 µs | 1.27× | 77.7 KiB | - | - | - | 48 | - | 40.0 | cold first run 14.16 ms | | generated/mp4/h264-aac.mp4 | 0.2 MiB | TagLib# | 57.8 µs | 1.00× | 36.7 KiB | - | - | - | 2 | - | 40.0 | fastest comparator; cold first run 11.80 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | OneMedia | 23.1 µs | 0.21× | 57.6 KiB | - | - | - | 6 | - | 39.9 | cold first run 15.05 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | ATL.NET | 547.3 µs | 4.98× | 151.7 KiB | - | - | - | 0 | - | 40.0 | cold first run 36.07 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | MetadataExtractor | 142.3 µs | 1.30× | 91.8 KiB | - | - | - | 96 | - | 40.0 | cold first run 10.45 ms | | samples/MPEG-4/video.mp4 | 26.6 MiB | TagLib# | 109.9 µs | 1.00× | 78.2 KiB | - | - | - | 2 | - | 40.0 | fastest comparator; cold first run 10.11 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | OneMedia | 36.5 µs | 0.27× | 62.4 KiB | - | - | - | 3 | - | 40.0 | cold first run 16.61 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | ATL.NET | 1.01 ms | 7.55× | 229.2 KiB | - | - | - | 0 | - | 40.0 | cold first run 40.00 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | MetadataExtractor | 134.4 µs | 1.00× | 87.7 KiB | - | - | - | 61 | - | 40.0 | fastest comparator; cold first run 11.49 ms | | blender/big_buck_bunny_1080p_h264.mov | 691.5 MiB | TagLib# | fail | - | - | - | - | - | - | - | 40.0 | UnsupportedFormatException: blender/big_buck_bunny_1080p_h264.mov (taglib/mov) | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | OneMedia | 15.1 µs | 0.01× | 20.9 KiB | - | - | - | 3 | - | 40.0 | cold first run 14.94 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | ATL.NET | 4.65 ms | 3.84× | 483.7 KiB | - | - | - | 0 | - | 39.9 | cold first run 47.95 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | MetadataExtractor | 4.26 ms | 3.52× | 183.6 KiB | - | - | - | 60 | - | 40.0 | cold first run 19.25 ms | | generated/bench/frag-bbb-1080p.mp4 | 691.5 MiB | TagLib# | 1.21 ms | 1.00× | 521.2 KiB | - | - | - | 2 | - | 40.0 | fastest comparator; cold first run 10.71 ms | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/big_buck_bunny_1080p_h264.mov | ExifTool | 76.99 ms | 76.74 ms – 77.93 ms | 27.2 | | | blender/big_buck_bunny_1080p_h264.mov | MP4Box -info | 17.13 ms | 16.63 ms – 21.45 ms | 9.2 | | | blender/big_buck_bunny_1080p_h264.mov | MediaInfo | 26.29 ms | 26.25 ms – 31.26 ms | 14.6 | | | blender/big_buck_bunny_1080p_h264.mov | ffprobe -show_format -show_streams | 27.07 ms | 26.94 ms – 29.07 ms | 26.0 | | | generated/bench/frag-bbb-1080p.mp4 | ExifTool | 158.79 ms | 158.42 ms – 160.27 ms | 27.4 | | | generated/bench/frag-bbb-1080p.mp4 | MP4Box -info | 19.15 ms | 18.83 ms – 22.34 ms | 11.6 | | | generated/bench/frag-bbb-1080p.mp4 | MediaInfo | 41.34 ms | 41.26 ms – 46.29 ms | 14.4 | | | generated/bench/frag-bbb-1080p.mp4 | ffprobe -show_format -show_streams | 52.89 ms | 52.88 ms – 58.02 ms | 40.4 | | | generated/mp4/h264-aac.mp4 | ExifTool | 73.28 ms | 71.87 ms – 86.89 ms | 25.7 | | | generated/mp4/h264-aac.mp4 | MP4Box -info | 16.23 ms | 15.98 ms – 32.58 ms | 8.6 | | | generated/mp4/h264-aac.mp4 | MediaInfo | 19.17 ms | 19.06 ms – 34.76 ms | 11.0 | | | generated/mp4/h264-aac.mp4 | ffprobe -show_format -show_streams | 24.83 ms | 24.55 ms – 85.62 ms | 18.0 | | | samples/MPEG-4/video.mp4 | ExifTool | 111.30 ms | 106.67 ms – 118.17 ms | 40.0 | | | samples/MPEG-4/video.mp4 | MP4Box -info | 16.81 ms | 16.65 ms – 19.07 ms | 8.6 | | | samples/MPEG-4/video.mp4 | MediaInfo | 20.92 ms | 20.09 ms – 21.28 ms | 11.5 | | | samples/MPEG-4/video.mp4 | ffprobe -show_format -show_streams | 25.16 ms | 25.07 ms – 31.24 ms | 16.8 | | ## mkv-demux: Matroska/WebM: read every packet | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mkv/h264-aac.mkv | 0.2 MiB | OneMedia | 86.8 µs | 0.21× | 1.0 MiB | 3513.4 B | 309 | 0.2 | - | - | 40.0 | cold first run 19.41 ms | | generated/mkv/h264-aac.mkv | 0.2 MiB | OneMedia, length asked of the file each time | 197.3 µs | 0.48× | 1.0 MiB | 3513.7 B | 309 | 0.2 | - | - | 39.9 | cold first run 19.39 ms | | generated/mkv/h264-aac.mkv | 0.2 MiB | StefH Matroska | 411.1 µs | 1.00× | 672.2 KiB | 2227.5 B | 309 | 0.2 | - | - | 40.0 | cold first run 17.27 ms | | matroska-test-files/test1.mkv | 22.3 MiB | OneMedia | 2.13 ms | 0.20× | 1.0 MiB | 189.4 B | 5735 | 22.2 | - | - | 39.9 | cold first run 25.59 ms | | matroska-test-files/test1.mkv | 22.3 MiB | OneMedia, length asked of the file each time | 2.98 ms | 0.28× | 1.0 MiB | 189.4 B | 5735 | 22.2 | - | - | 40.0 | cold first run 25.77 ms | | matroska-test-files/test1.mkv | 22.3 MiB | StefH Matroska | 10.46 ms | 1.00× | 46.0 MiB | 18698.1 B | 2582 | 22.2 | - | - | 39.9 | packet count differs from OneMedia's 5735; cold first run 25.52 ms | | matroska-test-files/test5.mkv | 30.3 MiB | OneMedia | 2.58 ms | 0.20× | 1.0 MiB | 254.5 B | 4298 | 30.3 | - | - | 39.8 | cold first run 24.02 ms | | matroska-test-files/test5.mkv | 30.3 MiB | OneMedia, length asked of the file each time | 3.10 ms | 0.25× | 1.0 MiB | 254.5 B | 4298 | 30.3 | - | - | 40.0 | cold first run 27.48 ms | | matroska-test-files/test5.mkv | 30.3 MiB | StefH Matroska | 12.62 ms | 1.00× | 61.6 MiB | 41197.8 B | 1568 | 30.3 | - | - | 40.0 | packet count differs from OneMedia's 4298; cold first run 26.69 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | OneMedia | 132.06 ms | 0.41× | 1.0 MiB | 22.2 B | 49323 | 1125.2 | - | - | 40.0 | cold first run 142.56 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | OneMedia, length asked of the file each time | 140.40 ms | 0.44× | 1.0 MiB | 22.2 B | 49323 | 1125.2 | - | - | 40.0 | cold first run 155.42 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | StefH Matroska | 318.36 ms | 1.00× | 2265.6 MiB | 93606.8 B | 25379 | 1125.2 | - | - | 40.0 | packet count differs from OneMedia's 49323; cold first run 2.03 s | | blender/tears_of_steel_1080p.webm | 544.9 MiB | OneMedia | 49.40 ms | 0.29× | 1.0 MiB | 13.7 B | 80000 | 544.6 | - | - | 40.0 | cold first run 111.38 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | OneMedia, length asked of the file each time | 60.19 ms | 0.35× | 1.0 MiB | 13.7 B | 80000 | 544.6 | - | - | 39.9 | cold first run 106.66 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | StefH Matroska | 171.47 ms | 1.00× | 1097.5 MiB | 44895.5 B | 25634 | 544.7 | - | - | 40.0 | packet count differs from OneMedia's 80000; cold first run 255.32 ms | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/Sintel.2010.1080p.mkv | ffmpeg -c copy -f null | 544.24 ms | 544.24 ms – 544.24 ms | 34.4 | | | blender/Sintel.2010.1080p.mkv | mkvextract tracks (first track) | 210.74 ms | 210.74 ms – 210.74 ms | 26.9 | | | blender/tears_of_steel_1080p.webm | ffmpeg -c copy -f null | 193.79 ms | 192.65 ms – 228.55 ms | 19.3 | | | blender/tears_of_steel_1080p.webm | mkvextract tracks (first track) | 118.15 ms | 117.91 ms – 130.19 ms | 29.5 | | | generated/mkv/h264-aac.mkv | ffmpeg -c copy -f null | 25.29 ms | 24.36 ms – 68.96 ms | 18.0 | | | generated/mkv/h264-aac.mkv | mkvextract tracks (first track) | 40.30 ms | 39.79 ms – 91.29 ms | 15.5 | | | matroska-test-files/test1.mkv | ffmpeg -c copy -f null | 35.57 ms | 35.54 ms – 38.49 ms | 15.6 | | | matroska-test-files/test1.mkv | mkvextract tracks (first track) | 45.96 ms | 45.83 ms – 46.13 ms | 24.4 | | | matroska-test-files/test5.mkv | ffmpeg -c copy -f null | 49.96 ms | 49.79 ms – 53.56 ms | 25.8 | | | matroska-test-files/test5.mkv | mkvextract tracks (first track) | 47.09 ms | 47.09 ms – 50.03 ms | 24.1 | | ## mkv-probe: Matroska/WebM: open, track list, duration | File | Size | Implementation | Median | Ratio | Alloc / run | Alloc / packet | Packets | Payload MiB | Tracks or tags | Peak MiB | Baseline MiB | Note | |---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---| | generated/mkv/h264-aac.mkv | 0.2 MiB | OneMedia | 12.9 µs | 0.62× | 36.1 KiB | - | - | - | 2 | - | 39.9 | cold first run 11.99 ms | | generated/mkv/h264-aac.mkv | 0.2 MiB | NEbml element reader | 20.6 µs | 1.00× | 68.7 KiB | - | - | - | 2 | - | 40.0 | cold first run 2.82 ms | | matroska-test-files/test1.mkv | 22.3 MiB | OneMedia | 12.3 µs | 0.64× | 36.5 KiB | - | - | - | 2 | - | 40.0 | cold first run 12.55 ms | | matroska-test-files/test1.mkv | 22.3 MiB | NEbml element reader | 19.4 µs | 1.00× | 68.3 KiB | - | - | - | 2 | - | 39.9 | cold first run 2.78 ms | | matroska-test-files/test5.mkv | 30.3 MiB | OneMedia | 16.8 µs | 0.53× | 44.0 KiB | - | - | - | 11 | - | 40.0 | cold first run 12.70 ms | | matroska-test-files/test5.mkv | 30.3 MiB | NEbml element reader | 31.7 µs | 1.00× | 72.8 KiB | - | - | - | 11 | - | 40.0 | cold first run 2.93 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | OneMedia | 16.0 µs | 0.48× | 44.7 KiB | - | - | - | 12 | - | 40.0 | cold first run 12.10 ms | | blender/Sintel.2010.1080p.mkv | 1.10 GiB | NEbml element reader | 33.2 µs | 1.00× | 73.3 KiB | - | - | - | 12 | - | 39.9 | cold first run 2.57 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | OneMedia | 14.4 µs | 0.76× | 43.9 KiB | - | - | - | 2 | - | 40.0 | cold first run 12.00 ms | | blender/tears_of_steel_1080p.webm | 544.9 MiB | NEbml element reader | 19.1 µs | 1.00× | 68.2 KiB | - | - | - | 2 | - | 40.0 | cold first run 2.54 ms | Reference tools, run as processes (wall time including start-up, peak resident set); information only: | File | Tool | Wall median | Wall min – max | Peak MiB | Note | |---|---|---:|---:|---:|---| | blender/Sintel.2010.1080p.mkv | ExifTool | 64.67 ms | 64.67 ms – 64.67 ms | 21.1 | | | blender/Sintel.2010.1080p.mkv | MediaInfo | 165.47 ms | 165.47 ms – 165.47 ms | 12.3 | | | blender/Sintel.2010.1080p.mkv | ffprobe -show_format -show_streams | 28.24 ms | 28.24 ms – 28.24 ms | 20.8 | | | blender/Sintel.2010.1080p.mkv | mkvinfo | 40.73 ms | 40.73 ms – 40.73 ms | 14.7 | | | blender/Sintel.2010.1080p.mkv | mkvmerge -J | 43.50 ms | 43.50 ms – 43.50 ms | 20.0 | | | blender/tears_of_steel_1080p.webm | ExifTool | 60.80 ms | 59.89 ms – 61.23 ms | 21.2 | | | blender/tears_of_steel_1080p.webm | MediaInfo | 47.45 ms | 47.40 ms – 50.59 ms | 11.6 | | | blender/tears_of_steel_1080p.webm | ffprobe -show_format -show_streams | 24.40 ms | 24.17 ms – 27.92 ms | 14.8 | | | blender/tears_of_steel_1080p.webm | mkvinfo | 39.60 ms | 39.04 ms – 39.66 ms | 14.6 | | | blender/tears_of_steel_1080p.webm | mkvmerge -J | 41.31 ms | 41.28 ms – 43.38 ms | 19.0 | | | generated/mkv/h264-aac.mkv | ExifTool | 61.09 ms | 60.86 ms – 85.20 ms | 21.3 | | | generated/mkv/h264-aac.mkv | MediaInfo | 17.86 ms | 17.67 ms – 33.79 ms | 10.5 | | | generated/mkv/h264-aac.mkv | ffprobe -show_format -show_streams | 25.18 ms | 24.11 ms – 69.71 ms | 17.8 | | | generated/mkv/h264-aac.mkv | mkvinfo | 40.31 ms | 39.44 ms – 133.52 ms | 14.6 | | | generated/mkv/h264-aac.mkv | mkvmerge -J | 40.25 ms | 40.06 ms – 61.57 ms | 16.7 | | | matroska-test-files/test1.mkv | ExifTool | 60.60 ms | 59.94 ms – 60.73 ms | 21.3 | | | matroska-test-files/test1.mkv | MediaInfo | 19.78 ms | 19.26 ms – 42.38 ms | 10.9 | | | matroska-test-files/test1.mkv | ffprobe -show_format -show_streams | 24.31 ms | 24.22 ms – 27.92 ms | 15.0 | | | matroska-test-files/test1.mkv | mkvinfo | 43.41 ms | 43.34 ms – 43.85 ms | 14.9 | | | matroska-test-files/test1.mkv | mkvmerge -J | 44.42 ms | 42.15 ms – 45.14 ms | 20.1 | | | matroska-test-files/test5.mkv | ExifTool | 93.33 ms | 93.17 ms – 95.16 ms | 36.6 | | | matroska-test-files/test5.mkv | MediaInfo | 34.18 ms | 34.02 ms – 118.83 ms | 11.4 | | | matroska-test-files/test5.mkv | ffprobe -show_format -show_streams | 38.85 ms | 38.65 ms – 41.45 ms | 24.5 | | | matroska-test-files/test5.mkv | mkvinfo | 42.51 ms | 41.88 ms – 43.24 ms | 16.2 | | | matroska-test-files/test5.mkv | mkvmerge -J | 44.94 ms | 44.30 ms – 45.91 ms | 23.1 | | ## Against the bar Bar: geometric mean of the ratios ≤ 1.00× and no file worse than 1.25×, against the fastest managed comparator per file. | Row | Files with a ratio | Geomean | Worst | Verdict | |---|---:|---:|---:|---| | mp4-demux | 3/4 | 0.09× | 0.20× | at the bar | | mp4-audio | 2/4 | 0.49× | 0.61× | at the bar | | mp4-probe | 4/4 | 0.12× | 0.28× | at the bar | | mkv-demux | 5/5 | 0.25× | 0.41× | at the bar | | mkv-probe | 5/5 | 0.60× | 0.76× | at the bar | # Changelog All notable changes are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] ## [0.1.0-preview.1] - 2026-11-01 First preview: reading MP4/MOV/3GP (including fragmented MP4) and Matroska/WebM, with native tags, a common tag view and basic capture facts. No writing or tag editing yet. ### Added - `MediaFile.Tags`, `CaptureTime`, `Location`, `Device`, `Rotation`, `Native[...]` and `ReadMetadataAsync`, and tags and facts in `onemedia info` (text and `--json`). - `MediaFile.ContainerDuration`, `TracksDuration` and `ExactDuration`; `Duration` is the container's figure unless a track without an edit list is longer. - Matroska: zlib-compressed frames are inflated on read; `OutputSamplingFrequency` is the audio rate; `V_MS/VFW/FOURCC` tracks are named by their FourCC. - ISO: QuickTime packetised audio (IMA 4:1 and similar) is joined per chunk; the QuickTime sound layout is read from the data; AMR is mono at its codec rate; QuickTime `text` entries are timed text; `tapt`, `gmhd`, `hinf` and `hnti` are containers; a video size of 0 is taken from the sequence parameter set. - Repository skeleton: core package `RecursiveAI.OneMedia`, audio adapter `RecursiveAI.OneMedia.Audio`, the `onemedia` tool, tests, NativeAOT and browser-WebAssembly smoke apps. - Byte sources: memory, file, seekable stream, caller-supplied ranged reads and forward-only streams, plus an observer hook for counting reads. - `MediaTime` (exact rational time with explicit rounding), `MediaLimits`, typed defects and exceptions. - Content-based format sniffing and an explicit format registry; sniffers for ISO base media files, EBML (Matroska/WebM) and the other container and stream signatures. - The track and packet model, demuxer interfaces with an allocation-free pull path, and the structure-node contract. - `Media.Open` / `MediaFile` happy path, the `onemedia info` command, and the audio adapter: tracks as OneAudio packet sources, OneAudio container registrations for MP4 and Matroska, and `Decode()`. # Get video duration, resolution and codec in C# `Media.Open` reads a video file’s structure and gives you its duration, each track’s codec, the frame size and frame rate, and the audio sample rate, in managed code with no FFmpeg or ffprobe. It reads MP4, MOV, 3GP and fragmented MP4, and MKV and WebM. The two inputs here are the same five-second clip from MDN’s CC0 sample videos, as H.264/AAC in MP4 and VP8/Vorbis in WebM. ```shell dotnet new console -o VideoInfo && cd VideoInfo dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 m=https://interactive-examples.mdn.mozilla.net/media/cc0-videos curl -L -o flower.mp4 $m/flower.mp4 curl -L -o flower.webm $m/flower.webm ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Codecs; foreach (string path in new[] { "flower.mp4", "flower.webm" }) { using MediaFile media = Media.Open(path); Console.WriteLine($"{path}: {media.Format}, {media.Duration:mm\\:ss\\.fff}"); if (media.Video is { Video: { } v } video) { double fps = (double)v.FrameRateNumerator / v.FrameRateDenominator; Console.WriteLine($" video {video.Codec} {v.Width}x{v.Height} {fps:0.###} fps" + $" {CodecString(video)}"); } if (media.Audio is { Audio: { } a } audio) Console.WriteLine($" audio {audio.Codec} {a.SampleRate} Hz {a.Channels} ch" + $" {CodecString(audio)}"); } // Parses the codec configuration (avcC, esds, CodecPrivate...) for the details. static string CodecString(MediaTrack track) => CodecParameterReader.TryDescribe(track.Codec, track.Configuration, out CodecParameters? p) && p is not null ? $"[{p.Rfc6381}{(p.ProfileName is { Length: > 0 } n ? $", {n}" : "")}]" : ""; ``` ```text flower.mp4: isobmff, 00:05.055 video h264 960x540 29.97 fps [avc1.64001F, High] audio aac 48000 Hz 2 ch [mp4a.40.2, LC] flower.webm: matroska, 00:05.059 video vp8 960x540 29.97 fps audio vorbis 44100 Hz 2 ch [vorbis] ``` `media.Duration` is a `TimeSpan?`; it is null only when the file doesn’t state one. `media.Video` and `media.Audio` are the first video and audio tracks; `media.Tracks` lists them all (see [List the tracks of a video](https://recursiveai.org/onemedia/cookbook/list-video-tracks/)). The frame size is the coded size; `RotationDegrees` on the same record says whether a player turns it (see [Get video rotation](https://recursiveai.org/onemedia/cookbook/get-video-rotation/)). The RFC 6381 string is what an HTML `` or a DASH/HLS manifest wants. The same `CodecParameters` record carries the profile and level, bit depth, chroma format, color description, pixel aspect ratio and, for audio, the sample rate and channel count from the codec header itself. ## Check it with ffprobe If you have FFmpeg installed, ffprobe reports the same figures: ```shell ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate,sample_rate -of compact flower.mp4 ``` ```text stream|codec_name=h264|width=960|height=540|r_frame_rate=30000/1001 stream|codec_name=aac|sample_rate=48000|r_frame_rate=0/0 format|duration=5.055000 ``` ## Limits - **No decoding.** OneMedia reads what the container and codec headers state. It can’t grab a frame, make a thumbnail or measure anything that needs decoded pixels or samples. - **Formats.** MP4, MOV, 3GP/3G2, M4A/M4V, fragmented MP4, MKV, MKA, MK3D and WebM only in 0.1. AVI, MPEG-TS, Ogg, FLV and the rest throw `UnsupportedMediaException`. - **Codec strings.** `Rfc6381` comes from the codec header parsers, so a track whose codec has none (VP8 here) gets no string. The parsers cover H.264, HEVC, H.266, AV1, VP8/VP9, AAC, MP3, Opus, Vorbis, FLAC, AC-3 and others: [Codec headers](https://recursiveai.org/onemedia/docs/codecs/). - **Frame rate.** It is the nominal rate the container states: the sample durations in MP4, `DefaultDuration` in Matroska (which gives fractions such as 500000000/16683333). Both are 0 when the file doesn’t say, so check before dividing. - **Fragmented MP4 duration** is structural: it comes from the headers, and the fragments are not scanned. See [MP4, MOV and 3GP](https://recursiveai.org/onemedia/docs/isobmff/). For tags such as title and artist, see [Read MP4 metadata](https://recursiveai.org/onemedia/cookbook/read-mp4-metadata/). # Read MKV and WebM file info in C# `Media.Open` reads Matroska (`.mkv`, `.mka`, `.mk3d`) and WebM the same way as MP4: duration, tracks and codecs, a common tag view, and every Matroska tag with its target. The inputs are a WebM clip from MDN’s CC0 sample videos and `test5.mkv` from the Matroska project’s test files, 32 MB, cut from Elephants Dream (CC BY: © copyright 2006, Blender Foundation / Netherlands Media Art Institute / www.elephantsdream.org). ```shell dotnet new console -o MkvInfo && cd MkvInfo dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 curl -L -o flower.webm https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm curl -L -o test5.mkv https://raw.githubusercontent.com/ietf-wg-cellar/matroska-test-files/master/test_files/test5.mkv ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Matroska; using RecursiveAI.OneMedia.Tags; foreach (string path in new[] { "flower.webm", "test5.mkv" }) { using MediaFile media = Media.Open(path); // Both are "matroska"; the EBML header's DocType tells WebM apart. string docType = (media.Demuxer as MatroskaDemuxer)?.Header.DocType ?? "?"; Console.WriteLine($"{path}: {docType}, {media.Duration}, {media.Tracks.Count} tracks"); foreach (MediaTrack t in media.Tracks.Take(3)) Console.WriteLine($" track {t.Index}: {t.Kind} {t.Codec} [{t.CodecTag}]"); // Common fields, then each native tag: Segment Info (mkv-info) and Tags (mkv). Console.WriteLine($" title: {media.Tags.Title}"); foreach (NativeTag tag in media.Metadata.NativeTags) { string target = tag.TargetTypeValue is long level ? $" (target {level})" : ""; Console.WriteLine($" {tag.System}:{tag.Key}{target} = {tag.Text}"); } } ``` ```text flower.webm: webm, 00:00:05.0590000, 2 tracks track 0: Video vp8 [V_VP8] track 1: Audio vorbis [A_VORBIS] title: mkv-info:MuxingApp = Lavf57.71.100 mkv-info:WritingApp = Lavf57.71.100 test5.mkv: matroska, 00:00:46.6650000, 11 tracks track 0: Video h264 [V_MPEG4/ISO/AVC] track 1: Audio aac [A_AAC] track 2: Subtitle subrip [S_TEXT/UTF8] title: Big Buck Bunny - test 8 mkv-info:MuxingApp = libebml v1.0.0 + libmatroska v1.0.0 mkv-info:WritingApp = mkvmerge v4.0.0 ('The Stars were mine') built on Jun 6 2010 16:18:42 mkv-info:DateUTC = 2010-08-21T18:06:43Z mkv:TITLE (target 50) = Big Buck Bunny - test 8 mkv:DATE_RELEASED (target 50) = 2010 mkv:COMMENT (target 50) = Matroska Validation File 8, secondary audio commentary track, misc subtitle tracks ``` The tags are what each file stores: `test5.mkv` is titled “Big Buck Bunny - test 8” although its pictures come from Elephants Dream, and `flower.webm` has no title at all. `mkv-info` holds the Segment Info element: the title, the muxing library, the writing application and `DateUTC`, when the file was muxed. `mkv` holds the `Tags` element: each `SimpleTag` with its target level (50 is the whole movie or album, 30 a track or chapter, per RFC 9559) and the track it targets, if any (`tag.TrackId`). Nested tags are keyed `PARENT/CHILD`. The common view (`media.Tags`) takes only tags that target the whole file, plus a single-track file’s track tags, and puts the lowest level of 30 or above first. Everything else stays on the native list. Attachments, such as cover art and fonts, are listed as native tags of kind `Image` or `Binary`, and their bytes are read only when you call `tag.ReadValue()`. ## Streams and remote files `Media.Open(stream)` takes any readable, seekable `Stream` and throws `ArgumentException` for one that can’t seek. The Matroska reader also has a live, forward-only mode for streams that can’t seek, through the lower-level `StreamForwardSource`; it is not behind `Media.Open` in 0.1. For a file on a web server, `Media.OpenAsync` reads byte ranges (see [Read video metadata from a URL](https://recursiveai.org/onemedia/cookbook/read-video-metadata-from-url/)). ## Limits - **Reading only.** No writing, remuxing or tag editing in 0.1. - **Subtitles** are listed as tracks, with their packets, but not converted or parsed: subtitles are not in this release. - **Track durations** are unknown for Matroska: the container states the Segment’s duration only, so `track.Duration` is unknown and `media.Duration` is the Segment’s. - **Chapters** in Matroska `Chapters` elements are not read yet. - **Known differences** from ffprobe, MKVToolNix and MediaInfo (attachments are not streams, RealAudio is not de-interleaved, RealVideo timestamps are as stored) are listed in [Matroska and WebM](https://recursiveai.org/onemedia/docs/matroska-differential/). To list every track with its language and flags, see [List the tracks of a video](https://recursiveai.org/onemedia/cookbook/list-video-tracks/). # Read MP4 metadata tags in C# `media.Tags` gives the common fields of an MP4, M4A or MOV file (title, artist, album, comment, description, copyright, date, encoder) whichever tag system the file uses, and `media.Metadata.NativeTags` lists every tag exactly as stored. The input is the Sintel trailer, 4.4 MB, from the Blender Foundation (CC BY 3.0). ```shell dotnet new console -o Mp4Tags && cd Mp4Tags dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 curl -L -o sintel.mp4 https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4 ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Tags; using MediaFile media = Media.Open("sintel.mp4"); // The common view: the same fields for iTunes, QuickTime, 3GPP, XMP and Matroska tags. CommonTags tags = media.Tags; Console.WriteLine($"Title: {tags.Title}"); Console.WriteLine($"Artist: {tags.Artist}"); Console.WriteLine($"Description: {tags.Description}"); Console.WriteLine($"Copyright: {tags.Copyright}"); Console.WriteLine($"Encoder: {tags.Encoder}"); // Every native tag: its system, its key and its value as stored. Console.WriteLine(); foreach (NativeTag tag in media.Metadata.NativeTags) Console.WriteLine($"{tag.System,-5} {tag.Key,-5} {tag.Text}"); // One native tag by system and key. Console.WriteLine(); Console.WriteLine(media.Native["ilst", "©cpy"]?.Text); ``` ```text Title: Sintel Trailer Artist: Durian Open Movie Team Description: Trailer for the Sintel open movie project Copyright: Encoder: Lavf52.62.0 ilst ©nam Sintel Trailer ilst ©ART Durian Open Movie Team ilst ©too Lavf52.62.0 ilst ©cpy (c) copyright Blender Foundation | durian.blender.org ilst desc Trailer for the Sintel open movie project (c) copyright Blender Foundation | durian.blender.org ``` `Copyright` is empty although the file has a copyright: this file stores it as `©cpy` in the iTunes list, and the 0.1 mapping takes copyright from `cprt` there (`©cpy` only from QuickTime user data). The native lookup at the end reads it whatever the mapping says. The common view is computed from the native tags each time you ask, through a fixed [mapping table](https://recursiveai.org/onemedia/docs/tag-mapping/): `©nam` in an iTunes list, `com.apple.quicktime.title` in QuickTime keys, `titl` in a 3GPP file and `TITLE` in Matroska all become `Title`. When several native tags map to one field, the property is the first and `tags.Values(CommonField.Title)` lists them all. The native systems are `ilst` (iTunes items, what ffmpeg and most tools write to MP4), `udta` (QuickTime user data), `mdta` (QuickTime keys, what phones write), `3gpp`, `xmp`, and `mkv` and `mkv-info` for Matroska. Text values are decoded when the file is opened; pictures and long binary values are read only when you ask, with `tag.ReadValue()`. ## Check it with ffprobe ```shell ffprobe -v error -show_entries format_tags -of default=nw=1 sintel.mp4 ``` ```text TAG:major_brand=isom TAG:minor_version=512 TAG:compatible_brands=isomiso2avc1mp41 TAG:creation_time=1970-01-01T00:00:00.000000Z TAG:title=Sintel Trailer TAG:artist=Durian Open Movie Team TAG:encoder=Lavf52.62.0 TAG:copyright=(c) copyright Blender Foundation | durian.blender.org TAG:description=Trailer for the Sintel open movie project ``` ffprobe lists the file type brands and the movie header’s creation time with the tags. In OneMedia the creation time is a candidate of `media.CaptureTime` (here the 1970 date the muxer wrote; see [When and where a video was recorded](https://recursiveai.org/onemedia/cookbook/video-recording-date-location/)). ## Limits - **Reading only.** OneMedia 0.1 doesn’t write or edit tags. Tag editing is not in this release. - **Chapters.** Nero `chpl` chapter lists are read (`media.Metadata.Chapters`); QuickTime text chapter tracks and Matroska `Chapters` are not read yet. - **Mapping gaps.** A tag the [mapping table](https://recursiveai.org/onemedia/docs/tag-mapping/) doesn’t list, such as `©cpy` in an iTunes list, stays on the native list only. - **Values that disagree.** Where a file has the same field in several systems, OneMedia picks by a fixed order and keeps the rest. ExifTool and MediaInfo sometimes pick a different one; [Tags and capture facts](https://recursiveai.org/onemedia/docs/tags/) lists every known difference. - **Bad text.** Invalid UTF-8 or UTF-16 is replaced with U+FFFD and reported in `media.Metadata.Defects`, not thrown. The same calls work on MKV and WebM: [Read MKV and WebM file info](https://recursiveai.org/onemedia/cookbook/read-mkv-webm-info/). # Read video metadata from a URL without downloading it in C# `Media.OpenAsync(length, readRange)` reads a file through any function that returns a byte range, such as an HTTP range request, cloud blob storage or a browser `Blob`. Probing a file reads only its structure, so you get the duration, tracks, codecs and tags of a remote video for a small part of its size. The file here is the Sintel trailer on Blender’s server, 4.4 MB (CC BY 3.0, Blender Foundation). ```shell dotnet new console -o RemoteProbe && cd RemoteProbe dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using System.Net.Http.Headers; using RecursiveAI.OneMedia; string url = "https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4"; using var http = new HttpClient(); // The length, from a HEAD request. using var head = await http.SendAsync(new HttpRequestMessage(HttpMethod.Head, url)); long length = head.Content.Headers.ContentLength ?? throw new InvalidOperationException("No Content-Length"); long fetched = 0; int requests = 0; async ValueTask ReadRange(long offset, Memory buffer, CancellationToken ct) { using var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Range = new RangeHeaderValue(offset, offset + buffer.Length - 1); using var response = await http.SendAsync(request, ct); response.EnsureSuccessStatusCode(); byte[] bytes = await response.Content.ReadAsByteArrayAsync(ct); bytes.AsSpan(0, Math.Min(bytes.Length, buffer.Length)).CopyTo(buffer.Span); fetched += bytes.Length; requests++; return Math.Min(bytes.Length, buffer.Length); } await using MediaFile media = await Media.OpenAsync(length, ReadRange); var metadata = await media.ReadMetadataAsync(); Console.WriteLine($"Duration: {media.Duration}"); foreach (MediaTrack t in media.Tracks) Console.WriteLine($"Track {t.Index}: {t.Kind} {t.Codec}" + (t.Video is { } v ? $" {v.Width}x{v.Height}" : "")); Console.WriteLine($"Title: {metadata.Tags.Title}"); Console.WriteLine($"Fetched {fetched:N0} of {length:N0} bytes in {requests} requests"); ``` ```text Duration: 00:00:52.2090000 Track 0: Video h264 854x480 Track 1: Audio aac Title: Sintel Trailer Fetched 43,704 of 4,372,373 bytes in 7 requests ``` Over a ranged source, call `ReadMetadataAsync()` before reading `media.Tags` or the facts: the synchronous properties need a source they can read on demand. This file’s movie index is at its end, after 4.3 MB of media data (its first video packet starts at byte 48). OneMedia steps over the media data box by its header to reach the index, without reading the media in between. The project measured a 4.38 GB MP4 with the index at the end (local reads): 165,681 B in 34 reads, against 1,741,689 B in 32 reads for ffprobe and 23,628,223 B in 138 reads for MediaInfo. Packets work over the same source with `media.PacketsAsync(track)`. Each one is a request here, so for a whole track, read bigger ranges and cache them in your `readRange`. ## Limits - **The server must honor ranges.** `ReadRange` assumes a `206 Partial Content` answer. A server that ignores `Range` sends the whole file each time; check `response.StatusCode` if you don’t control it. - **No decoding.** Only container structure and tags are read; nothing is decoded, and no thumbnail can be made. - **Fragmented MP4** durations come from the headers, not from scanning the fragments, which keeps a remote probe cheap but can differ from a full read. See [MP4, MOV and 3GP](https://recursiveai.org/onemedia/docs/isobmff/). - **Matroska over HTTP** reads the elements up to the first Cluster. `Tags` and `Attachments` stored after the Clusters are found through the SeekHead and cost a read each, never a scan; each is fetched whole, up to `MediaLimits.MaxAttachmentBytes`. The project’s byte and request counts for 213 files, over HTTP range requests and local reads, are in [docs/io-results.txt](https://github.com/recursive-ai-oss/onemedia-csharp/blob/v0.1.0-preview.1/docs/io-results.txt). # Get when and where a video was recorded in C# `media.CaptureTime`, `media.Location` and `media.Device` give the recording date, the GPS position and the camera or phone that made a video. Each is a list of candidates, best first, with where it was read from, because a file often holds several dates that disagree. The two inputs are small test files from the OneMedia repository (MIT OR Apache-2.0): one carries Apple-style QuickTime keys (`com.apple.quicktime.*`), the other Android-style keys (`com.android.*`). Their pictures are synthetic and their tags are invented: Contoso, Northwind and the San Francisco position are not real devices or recordings. ```shell dotnet new console -o WhenWhere && cd WhenWhere dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 t=https://raw.githubusercontent.com/recursive-ai-oss/onemedia-csharp/v0.1.0-preview.1/testdata/metadata curl -L -o iphone-style.mov $t/tags_keys.mov curl -L -o android-style.mp4 $t/tags_keys_android.mp4 ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Metadata; foreach (string path in new[] { "iphone-style.mov", "android-style.mp4" }) { using MediaFile media = Media.Open(path); Console.WriteLine(path); // The value is the first candidate; every candidate says where it came from. foreach (FactCandidate c in media.CaptureTime.Candidates) Console.WriteLine($" recorded {c.Value} ({c.Certainty}) from {c.Source}"); // Test Best, not Value: Value is a struct and reads 0, 0 when there is no location. if (media.Location.Best is { } loc) Console.WriteLine($" location {loc.Value.Latitude}, {loc.Value.Longitude}" + $" altitude {loc.Value.Altitude?.ToString() ?? "-"} m" + $" from {loc.Source}"); DeviceFacts device = media.Device; Console.WriteLine($" device {device.Make.Value} {device.Model.Value}," + $" software {device.Software.Value}"); // As a DateTimeOffset, when the file states the zone. MediaDateTime when = media.CaptureTime.Value; if (media.CaptureTime.HasValue && when.OffsetMinutes is int offset) Console.WriteLine(" as DateTimeOffset " + new DateTimeOffset(when.Value, TimeSpan.FromMinutes(offset)).ToString("o")); } ``` ```text iphone-style.mov recorded 2023-04-05T10:11:12+02:00 (Offset) from mdta:com.apple.quicktime.creationdate location 37.7749, -122.4194 altitude 12.5 m from mdta:com.apple.quicktime.location.ISO6709 device Contoso Camera_X1, software 17.1 as DateTimeOffset 2023-04-05T10:11:12.0000000+02:00 android-style.mp4 recorded 2023-06-01T09:30:00Z (Utc) from mdta:creation_time recorded 2023-06-01T09:30:00Z (Utc) from mvhd device Northwind Droid_7, software 14 ``` The iPhone-style file has `com.apple.quicktime.creationdate`, the local time with its UTC offset, so the zone is known (`Offset`). The Android-style file has a UTC `creation_time` key and the same instant in the movie header (`mvhd`), so both candidates appear, the key first. Location comes from the ISO 6709 text (`+37.7749-122.4194+012.500/`) as decimal degrees and meters. ## Which candidate wins The order is fixed and documented in [Tags and capture facts](https://recursiveai.org/onemedia/docs/tags/#fact-precedence). For capture time: the Apple `creationdate` key first, then XMP dates with a zone, the `mdta` `creation_time`, `©day`, XMP dates without a zone, Matroska `DATE_RECORDED`, Matroska `DateUTC`, and the movie header last. For location: QuickTime keys, then `©xyz` user data, then the 3GPP `loci` box, then XMP GPS. The movie header is a weak source. Some cameras store local time in it as if it were UTC, and some muxers write a placeholder: the [Sintel trailer](https://recursiveai.org/onemedia/cookbook/read-mp4-metadata/)’s says 1970-01-01T00:00:00Z, which OneMedia reports as it is, as the only candidate. `c.Interpretation` says what each source means, and `c.RawValue` is the text or number as stored. ## Limits - **Dates as stored.** OneMedia doesn’t guess a zone or correct a wrong clock. `Certainty` says what the file states: `Utc`, `Offset`, `Local` or `Unknown`. - **Sources read.** QuickTime keys and user data, iTunes `©day`, 3GPP boxes, XMP (`uuid` box and `XMP_` atom), the movie header, and Matroska tags and `DateUTC`. A GPS log stored as its own timed metadata track, as some action cameras and dashcams write, is not among them. - **Matroska dates.** `DateUTC` is when the file was muxed, which for a converted or remuxed file is not when it was recorded. - **No value.** Many converters and upload sites drop these tags. When a file has none, `HasValue` is false, `Best` is null and `Value` is the type’s default: for `Location` that is 0, 0, a real place, so test `Best` or `HasValue` first. The same facts include rotation: [Get video rotation](https://recursiveai.org/onemedia/cookbook/get-video-rotation/). # Get video rotation in C# A phone records portrait video as landscape frames plus a rotation that tells the player to turn them. `media.Rotation` reads that rotation from the MP4 or MOV track header as 0, 90, 180 or 270 degrees clockwise, and the frame-size fact gives the width and height as displayed. To have a rotated file you can share, the setup line below makes one from MDN’s CC0 flower clip with FFmpeg (6.0 or later), the way a phone held upright marks its video: the same frames, a 90° display matrix. ```shell dotnet new console -o Rotation && cd Rotation dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 curl -L -o flower.mp4 https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4 ffmpeg -v error -display_rotation -90 -i flower.mp4 -c copy portrait.mp4 ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Metadata; foreach (string path in new[] { "flower.mp4", "portrait.mp4" }) { using MediaFile media = Media.Open(path); // Clockwise degrees to turn the picture for display, and a mirror flag. VideoOrientation turn = media.Rotation.Value; FrameSize size = media.Metadata.Facts.FrameSize.Value; string shape = size.DisplayHeight > size.DisplayWidth ? "portrait" : "landscape"; Console.WriteLine($"{path}: rotate {turn.RotationDegrees}° clockwise" + $"{(turn.MirrorHorizontal ? ", mirrored" : "")}"); Console.WriteLine($" stored {size.CodedWidth}x{size.CodedHeight}," + $" shown {size.DisplayWidth}x{size.DisplayHeight} ({shape})"); Console.WriteLine($" from {media.Rotation.Best?.Source}" + $" {media.Rotation.Best?.RawValue}"); } ``` ```text flower.mp4: rotate 0° clockwise stored 960x540, shown 960x540 (landscape) from tkhd.matrix [00010000 00000000 00000000 00010000] portrait.mp4: rotate 90° clockwise stored 960x540, shown 540x960 (portrait) from tkhd.matrix [00000000 00010000 FFFF0000 00000000] ``` The rotation comes from the matrix in the first video track’s header (`tkhd`): its four unit entries are a rotation by a multiple of 90°, possibly mirrored. `RawValue` shows them as stored. The same number is on the track as `media.Video.Video.RotationDegrees`, and the `onemedia info` tool prints it as `rotate 90`. ## Clockwise or counterclockwise OneMedia counts clockwise. FFmpeg counts counterclockwise: `-display_rotation -90` above asks for 90° clockwise, and ffprobe reports the file as `-90`: ```shell ffprobe -v error -select_streams v -show_entries stream_side_data=rotation -of csv=p=0 portrait.mp4 ``` ```text -90 ``` So a file ffprobe calls `90` is 270 here. Both describe the same matrix. ## Limits - **No decoding, no rotating.** OneMedia reads the rotation; it doesn’t turn the frames or rewrite the file. - **MP4 family only.** The rotation is read from the MP4, MOV and 3GP track matrix. 0.1 reads none from Matroska or WebM: there `media.Rotation` has no candidate, and `Value` is the default, 0. - **Quarter turns.** Only the eight matrices of 0, 90, 180 or 270°, mirrored or not, give a candidate; a scaled or skewed matrix gives none. A vertical flip reads as 180° and mirrored. For the other basic facts of a video, see [Get video duration, resolution and codec](https://recursiveai.org/onemedia/cookbook/get-video-duration-resolution-codec/). # List the audio, video and subtitle tracks of a video in C# `media.Tracks` lists every track of an MP4, MOV, MKV or WebM file with its kind, codec, language, name and flags, in file order. The input is `test5.mkv` from the Matroska project’s test files: one video track, two audio tracks and eight subtitle tracks, cut from Elephants Dream (CC BY: © copyright 2006, Blender Foundation / Netherlands Media Art Institute / www.elephantsdream.org). ```shell dotnet new console -o ListTracks && cd ListTracks dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 curl -L -o test5.mkv https://raw.githubusercontent.com/ietf-wg-cellar/matroska-test-files/master/test_files/test5.mkv ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using MediaFile media = Media.Open("test5.mkv"); Console.WriteLine("# kind codec lang name flags"); foreach (MediaTrack t in media.Tracks) { string detail = t.Video is { } v ? $" {v.Width}x{v.Height}" : t.Audio is { } a ? $" {a.SampleRate} Hz {a.Channels} ch" : ""; Console.WriteLine($"{t.Index,-3} {t.Kind,-9} {t.Codec,-7} {t.Language,-5} " + $"{t.Name ?? "",-11} {t.Disposition}{detail}"); } // Pick tracks the way a player would. MediaTrack? english = media.Tracks.FirstOrDefault(t => t.Kind == TrackKind.Subtitle && t.Language == "eng"); MediaTrack? secondAudio = media.Tracks.Where(t => t.Kind == TrackKind.Audio) .Skip(1).FirstOrDefault(); Console.WriteLine($"English subtitles: track {english?.Index}"); Console.WriteLine($"Second audio: track {secondAudio?.Index}, \"{secondAudio?.Name}\""); ``` ```text # kind codec lang name flags 0 Video h264 und Default 1024x576 1 Audio aac und Default 48000 Hz 2 ch 2 Subtitle subrip eng Default 3 Subtitle subrip hun None 4 Subtitle subrip ger None 5 Subtitle subrip fre None 6 Subtitle subrip spa None 7 Subtitle subrip ita None 8 Audio aac eng Commentary None 44100 Hz 1 ch 9 Subtitle subrip jpn None 10 Subtitle subrip und None English subtitles: track 2 Second audio: track 8, "Commentary" ``` `Index` is the track’s position in `media.Tracks` and the number every other call uses. `Id` is the number the container gives it (the Matroska `TrackNumber` or the MP4 `track_ID`), which need not be in order: here track 10 has `Id` 7. `Language` is the code as stored: Matroska and MP4 use ISO 639-2 (`eng`, `ger`, `fre`), and `und` means undetermined. `Disposition` holds the default, forced, commentary, hearing-impaired, original and similar flags the file sets. ## Check it with ffprobe ffprobe lists the same tracks in the same order: ```shell ffprobe -v error -show_entries stream=index,codec_type,codec_name:stream_tags=language,title -of compact test5.mkv ``` ```text stream|index=0|codec_name=h264|codec_type=video stream|index=1|codec_name=aac|codec_type=audio stream|index=2|codec_name=subrip|codec_type=subtitle|tag:language=eng stream|index=3|codec_name=subrip|codec_type=subtitle|tag:language=hun stream|index=4|codec_name=subrip|codec_type=subtitle|tag:language=ger stream|index=5|codec_name=subrip|codec_type=subtitle|tag:language=fre stream|index=6|codec_name=subrip|codec_type=subtitle|tag:language=spa stream|index=7|codec_name=subrip|codec_type=subtitle|tag:language=ita stream|index=8|codec_name=aac|codec_type=audio|tag:language=eng|tag:title=Commentary stream|index=9|codec_name=subrip|codec_type=subtitle|tag:language=jpn stream|index=10|codec_name=subrip|codec_type=subtitle ``` ## Limits - **Listing, not extracting subtitles.** Subtitle tracks are listed and their packets can be read with `media.Packets(track)`, but OneMedia 0.1 doesn’t parse or convert subtitles, and it can’t write a track out to a new file: remuxing is not in this release. - **Attachments are not tracks.** Fonts and cover art in a Matroska file are native tags, not entries in `media.Tracks` (ffprobe lists them as streams). - **Protected tracks.** An encrypted track is listed with `IsProtected` set; its packets are delivered as stored. - **Language codes** are as stored, not translated: a Matroska track’s `LanguageBCP47` when it has one (`de`), else its ISO 639-2 code (`ger` or `deu`). To decode one of the audio tracks, see [Extract the audio from a video](https://recursiveai.org/onemedia/cookbook/extract-audio-from-video/). # Get video keyframe timestamps in C# `media.Packets(track)` reads a track’s packets in file order, each with its timestamps, keyframe flag, size and byte position in the file, without decoding anything. Keyframes are where a player can start decoding, so they are where a cut or a seek can land without re-encoding. The input is the Sintel trailer, 4.4 MB, from the Blender Foundation (CC BY 3.0). ```shell dotnet new console -o Keyframes && cd Keyframes dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 curl -L -o sintel.mp4 https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4 ``` Put this in `Program.cs` and run `dotnet run`: ```csharp using RecursiveAI.OneMedia; using MediaFile media = Media.Open("sintel.mp4"); MediaTrack video = media.Video!; // Packet times are on the track's own media timeline. Shift them by the edit list // to get the time a player shows. TrackEditInfo edit = video.Edit; int packets = 0, keyframes = 0; foreach (OwnedPacket packet in media.Packets(video)) { using (packet) { packets++; if (!packet.Info.IsKeyframe) continue; keyframes++; MediaTime shown = packet.Info.Pts - edit.Priming + edit.StartTime; Console.WriteLine($"{shown.ToTimeSpan():mm\\:ss\\.fff} byte {packet.Info.SourceRange.Offset,8}" + $" {packet.Info.Size,6} bytes"); } } Console.WriteLine($"{keyframes} keyframes in {packets} video packets"); ``` ```text 00:00.000 byte 48 766 bytes 00:09.333 byte 767856 1942 bytes 00:13.916 byte 1064100 42465 bytes 00:16.500 byte 1213115 14998 bytes 00:21.916 byte 1793351 6636 bytes 00:23.791 byte 2103099 28047 bytes 00:25.875 byte 2454669 11924 bytes 00:27.416 byte 2683094 10664 bytes 00:32.166 byte 3365431 5389 bytes 00:42.583 byte 4107389 4951 bytes 00:47.500 byte 4235471 1648 bytes 11 keyframes in 1253 video packets ``` `Pts` and `Dts` are exact rational times (`MediaTime`) in the track’s timescale, so nothing is rounded until you call `ToTimeSpan()`. `Packets(track)` switches the other tracks off while it runs, so their bytes are not read. Each packet’s `Data` is its bytes, rented from a pool: dispose the packet when you are done with it. `SourceRange` is where those bytes sit in the file. ## Why the edit list matters OneMedia doesn’t apply edit lists to packet times: they stay on the track’s media timeline, as the sample tables state them. This file’s edit list starts the picture 2/24 s into the media (to make room for B-frame reordering), so the raw first `Pts` is 00:00.083 and the shifted one is 00:00.000. ffprobe applies the edit list, and the shifted times match its keyframe times (the C# format string above cuts to milliseconds, so 13.916667 prints as 13.916): ```shell ffprobe -v error -select_streams v -skip_frame nokey -show_entries frame=pts_time -of csv=p=0 sintel.mp4 ``` ```text 0.000000 9.333333 13.916667 16.500000 21.916667 23.791667 25.875000 27.416667 32.166667 42.583333 47.500000 ``` `Pts - Priming + StartTime` is right for a track with one edit, which is what nearly every file has. A track with several edits maps pieces of media onto the timeline separately; `edit.Edits` lists them as stored. See [MP4, MOV and 3GP](https://recursiveai.org/onemedia/docs/isobmff/#time). ## Limits - **No frames.** A keyframe here is a packet the container flags as a random-access point. OneMedia can’t decode it into a picture or make a thumbnail. - **No cutting.** Trimming or splitting a video at keyframes needs a writer; writing and remuxing are not in 0.1. - **Matroska** packets come from Blocks and SimpleBlocks; a keyframe is a SimpleBlock with the keyframe bit or a Block without references. Matroska has no edit lists, so `Pts` is already the shown time (a track’s `CodecDelay` is already subtracted). - **Whole file read.** Reading every packet reads the track’s bytes. To read only the index, open the file and look at `media.Tracks`; the packet count of an MP4 track is `track.PacketCount`. To read packets of a file on a web server without downloading it all, see [Read video metadata from a URL](https://recursiveai.org/onemedia/cookbook/read-video-metadata-from-url/). # Extract the audio from a video to WAV in C# OneMedia reads the audio track’s packets out of the video file, and the `RecursiveAI.OneMedia.Audio` adapter hands them to [OneAudio](https://recursiveai.org/oneaudio/), which decodes them. `media.Decode()` gives you OneAudio’s decoder for the file’s audio, and OneAudio’s `WavEncoder` writes the samples to a WAV file. OneMedia itself decodes nothing. The input is MDN’s CC0 flower clip, H.264 video with AAC audio. ```shell dotnet new console -o VideoToWav && cd VideoToWav dotnet add package RecursiveAI.OneMedia.Audio --version 0.1.0-preview.1 curl -L -o flower.mp4 https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4 ``` `RecursiveAI.OneMedia.Audio` brings in `RecursiveAI.OneMedia` and `RecursiveAI.OneAudio` 1.4.0. Put this in `Program.cs` and run `dotnet run`: ```csharp using OneAudio; using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Audio; using MediaFile media = Media.Open("flower.mp4"); Console.WriteLine($"Audio track: {media.Audio?.Codec}, {media.Audio?.Audio?.SampleRate} Hz"); // The default audio track as a OneAudio decoder (AAC here), float samples out. using IAudioDecoder pcm = media.Decode(); AudioFormat format = pcm.Format; Console.WriteLine($"Decoder: {pcm.GetType().Name}, {format.SampleRate} Hz," + $" {format.Channels} ch, {pcm.LengthFrames} frames"); // Write 16-bit PCM WAV block by block, so memory stays flat for long files. using (FileStream file = File.Create("flower.wav")) { var wav = new WavEncoder(file, format, new WavEncoderSettings(), leaveOpen: true); float[] block = new float[4096 * format.Channels]; long frames = 0; int n; while ((n = pcm.Read(block)) > 0) { wav.Write(block.AsSpan(0, n * format.Channels)); frames += n; } wav.Finish(); Console.WriteLine($"flower.wav: {frames} frames," + $" {frames / (double)format.SampleRate:0.000} s, {file.Length:N0} bytes"); } ``` ```text Audio track: aac, 48000 Hz Decoder: AacDecoder, 48000 Hz, 2 ch, 242640 frames flower.wav: 242640 frames, 5.055 s, 970,640 bytes ``` The decoder starts and stops where the file says the audio does: the MP4 edit list cuts priming at the start (none in this file) and padding at the end (48 frames here), so the WAV is 242,640 frames, the track’s edited length. Decoded with FFmpeg 8.0.1 to 16-bit PCM, the same track gives the same samples over those 242,640 frames; FFmpeg’s output is 48 frames longer because it keeps the end the edit list cuts. ## Another track `media.Decode(track)` decodes a track you choose, for example the second audio track of a Matroska file (see [List the tracks of a video](https://recursiveai.org/onemedia/cookbook/list-video-tracks/)): ```csharp using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Audio; using MediaFile media = Media.Open("movie.mkv"); MediaTrack commentary = media.Tracks.First(t => t.Name == "Commentary"); using OneAudio.IAudioDecoder pcm = media.Decode(commentary); ``` Without a track, `Decode()` takes the first audio track marked default, else the first one OneAudio has a decoder for. ## Limits - **Codecs.** OneAudio decodes AAC-LC, Opus, Vorbis, FLAC, MP3 and AMR. - **HE-AAC** decodes as its AAC-LC core, at half the rate, without an error: the commentary track of `test5.mkv`, 44,100 Hz in the container, comes out at 22,050 Hz. The adapter docs list HE-AAC with SBR and PS as a gap. AC-3, E-AC-3, PCM, ALAC and protected tracks throw `UnsupportedAudioFormatException` naming the codec. See [OneAudio adapter](https://recursiveai.org/onemedia/docs/audio-adapter/). - **No video.** The video track is not decoded; there is no way to get frames from OneMedia. - **Matroska lengths.** For Matroska, `LengthFrames` is -1 (unknown) because the container states only a rounded duration; decode to the end to count. - **End padding.** Opus end trimming is applied; for Vorbis in Matroska the adapter can return up to 16 more frames than an Ogg copy, and some discard padding can’t be passed to OneAudio (`AudioSeamLimitations` on the packet source reports it). For audio files rather than videos, use OneAudio directly: [Decode AAC and M4A](https://recursiveai.org/oneaudio/cookbook/decode-aac/). # Inspect a video file from the command line `onemedia info ` prints a video file’s format, duration, tracks with their codec strings, tags and capture facts. Add `--json` for the same as JSON, with every candidate for each fact. It is a .NET tool, so it needs the .NET 10 SDK or runtime. The files are MDN’s CC0 flower clip and the iPhone-style test file from the OneMedia repository (synthetic picture, invented tags). ```shell dotnet tool install --global RecursiveAI.OneMedia.Tool --version 0.1.0-preview.1 curl -L -o flower.mp4 https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4 curl -L -o iphone-style.mov https://raw.githubusercontent.com/recursive-ai-oss/onemedia-csharp/v0.1.0-preview.1/testdata/metadata/tags_keys.mov onemedia --version ``` ```text 0.1.0-preview.1 ``` ## Text ```shell onemedia info flower.mp4 ``` ```text format: isobmff duration: 00:00:05.0550000 track 0: video h264 (avc1.64001F) [avc1] 960x540 30000/1001 fps lang=und disposition=Default track 1: audio aac (mp4a.40.2) [mp4a] 48000 Hz 2 ch lang=und disposition=Default captureTime: 2018-03-07T15:21:21Z (mvhd, 1 candidate(s)) rotation: 0 (tkhd.matrix, 1 candidate(s)) ``` Each track line is: index, kind, codec, the RFC 6381 string in parentheses, the container’s codec tag in brackets, then size, rotation (`rotate 90` when there is one) and frame rate for video, or rate and channels for audio, then language and flags. A file with tags and device facts prints more lines: ```shell onemedia info iphone-style.mov ``` ```text format: isobmff duration: 00:00:01 track 0: video h264 (avc1.64000B) [avc1] 160x120 25/1 fps lang=und disposition=Default track 1: audio aac (mp4a.40.2) [mp4a] 44100 Hz 1 ch lang=und disposition=Default tag Title: Keyed_Title tag Author: Pat_Doe tag Comment: Keyed_comment tag Encoder: Lavf61.1.100 captureTime: 2023-04-05T10:11:12+02:00 (mdta:com.apple.quicktime.creationdate, 1 candidate(s)) location: 37.7749,-122.4194,12.5 (mdta:com.apple.quicktime.location.ISO6709, 1 candidate(s)) make: Contoso (mdta:com.apple.quicktime.make, 1 candidate(s)) model: Camera_X1 (mdta:com.apple.quicktime.model, 1 candidate(s)) software: 17.1 (mdta:com.apple.quicktime.software, 1 candidate(s)) rotation: 0 (tkhd.matrix, 1 candidate(s)) ``` Each fact line shows the value, where it came from, and how many candidates the file holds. Damage the reader found prints as `defect:` lines. ## JSON ```shell onemedia info iphone-style.mov --json ``` The object has `format`, the durations as exact ticks and timescales, `tracks` (with `rfc6381`, size, rotation, rate and channels), `tags`, `facts` and `defects`. Each fact lists every candidate: ```text "location": { "value": "37.7749,-122.4194,12.5", "candidates": [ { "value": "37.7749,-122.4194,12.5", "source": "mdta:com.apple.quicktime.location.ISO6709", "raw": "\u002B37.7749-122.4194\u002B012.500/", "interpretation": "ISO 6709 latitude, longitude and optional altitude", "timezone": "Unknown" } ] } ``` The JSON escapes `+` as `\u002B`; any JSON parser reads it back as `+`. ## Exit codes `0` when the file was read, `1` when it couldn’t be (not found, or not a format OneMedia reads: `unsupported: The data is not in any registered format.` on standard error), and `2` with a usage message for anything but `info [--json]` or `--version`. ## Limits - **One command.** 0.1 has `info` and `--version` only. It doesn’t convert, extract, trim or edit anything. - **Formats.** MP4, MOV, 3GP, M4A/M4V, fragmented MP4, MKV and WebM, as for the library. - **Text output** shows the best candidate of each fact and the number of candidates; use `--json` to see them all. It doesn’t print track names; `--json` doesn’t either, so use the library for those (see [List the tracks of a video](https://recursiveai.org/onemedia/cookbook/list-video-tracks/)). - **Matroska frame rates** print as the container’s fraction of nanoseconds, such as `200000000/8333333 fps` for 24 fps. The same information from C#: [Get video duration, resolution and codec](https://recursiveai.org/onemedia/cookbook/get-video-duration-resolution-codec/).