craft-media: MXF demuxer and writer from FilmCraft (feature mxf) (#877)

Moves filmcraft-mxf (storytold/filmcraft@4bec22b) into craft_media::mxf.
ByteSource is the crate's trait, Codec::codec_id() and
EssenceTrack::codec_params() map tracks to CodecId, and the writer names
craft-media as company and product unless the caller sets them.

Fixes two panics: EssenceTrack::sync_before on a track without picture
samples, and Timecode::format for a drop-frame start near i64::MAX.
Adds Container::Mxf, an ffprobe/ffmpeg oracle test for the reader and the
writer, and hostile-input tests.
This commit is contained in:
Dylan M. Taylor
2026-11-11 16:41:32 -05:00
committed by GitHub
parent e21d7e85f8
commit d3bc790aba
18 changed files with 5736 additions and 10 deletions
Generated
+1
View File
@@ -1067,6 +1067,7 @@ dependencies = [
name = "craft-media"
version = "0.1.0"
dependencies = [
"chrono",
"craft-media-testkit",
"proptest",
"serde",
+1 -1
View File
@@ -118,7 +118,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
AC-3/E-AC-3, MP1/MP2), streaming and seeking, and packet decoding for any demuxer. See its
README for formats and features.
- [`craft-media`](crates/media/README.md): bit-level I/O (`bitstream`), clean-room MP4/MOV
(`isobmff`) and Matroska/WebM (`matroska`) demuxers and muxers, Ogg (`ogg`) and AVI (`avi`) demuxers, from
(`isobmff`), Matroska/WebM (`matroska`) and MXF (`mxf`) demuxers and muxers, Ogg (`ogg`) and AVI (`avi`) demuxers, from
FilmCraft, and an ISO BMFF box reader. Containers only; codecs are in `craft-audio`, and
video codecs will go in a separate crate.
- [`craft-lottie`](crates/lottie/README.md): Lottie (Bodymovin) JSON, the wire layer: the document
+8 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "craft-media"
description = "Bit-level I/O, clean-room ISO BMFF (MP4), QuickTime (MOV) and Matroska (MKV, WebM) demuxers and muxers, and Ogg and AVI demuxers."
description = "Bit-level I/O, clean-room ISO BMFF (MP4), QuickTime (MOV) and Matroska (MKV, WebM) and MXF demuxers and muxers, and Ogg and AVI demuxers."
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -20,6 +20,8 @@ isobmff = ["bitstream", "dep:thiserror"]
matroska = []
# AVI (RIFF) and OpenDML demuxer (`craft_media::avi`).
avi = []
# MXF demuxer and writer (`craft_media::mxf`); the writer formats dates with chrono.
mxf = ["dep:chrono"]
# Ogg demuxer (Ogg Opus and Vorbis mapping, `craft_media::ogg`).
ogg = []
# Serialize and Deserialize for `Container`, `CodecId`, `CodecParams` and `PcmFormat`.
@@ -28,6 +30,7 @@ serde = ["dep:serde"]
[dependencies]
thiserror = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
chrono = { workspace = true, optional = true }
[dev-dependencies]
craft-media-testkit = { workspace = true }
@@ -58,6 +61,10 @@ required-features = ["matroska"]
name = "oracle_mux"
required-features = ["isobmff"]
[[test]]
name = "oracle_mxf"
required-features = ["mxf"]
[[test]]
name = "oracle_ogg"
required-features = ["ogg"]
+175 -7
View File
@@ -1,9 +1,10 @@
# craft-media
Media containers and bit-level I/O for the Craft apps: an MSB-first bit reader and writer,
clean-room ISO BMFF (MP4), QuickTime (MOV) and Matroska (MKV, WebM) demuxers and muxers, and Ogg and
AVI demuxers, written from the public specifications. No `unsafe`, no panics on malformed input, builds
for `wasm32-unknown-unknown`. Dependencies: `thiserror` (for `isobmff`), `serde` (optional).
clean-room ISO BMFF (MP4), QuickTime (MOV) and Matroska (MKV, WebM) and MXF demuxers and muxers, and
Ogg and AVI demuxers, written from the public specifications. No `unsafe`, no panics on malformed
input, builds for `wasm32-unknown-unknown`. Dependencies: `thiserror` (for `isobmff`), `chrono`
(for `mxf`), `serde` (optional).
Codecs are in separate crates that depend on this one: [`craft-audio`](../audio/README.md) for
audio; `craft-video` for video. What both need lives here: the containers, the
@@ -19,12 +20,13 @@ that codec crates use to describe a file and configure a decoder.
| `matroska` | `craft_media::matroska` | none |
| `ogg` | `craft_media::ogg` | none |
| `avi` | `craft_media::avi` | none |
| `mxf` | `craft_media::mxf` | `chrono` |
| (none) | `Container`, `CodecId`, `CodecParams`, `PcmFormat`, `xiph_lace`, `ByteSource`, `ReadSeekSource` | none |
| `serde` | `Serialize`/`Deserialize` on `Container`, `CodecId`, `CodecParams` and `PcmFormat` | `serde` |
`default = ["isobmff"]`, so `bitstream` and `isobmff` build. `matroska` is opt-in: it is about as
much code as `isobmff`, and most users of MP4/MOV (LightCraft, FigCraft, SoundCraft, EffectCraft)
do not read Matroska. `ogg` and `avi` are opt-in as well. The features are independent.
do not read Matroska. `ogg`, `avi` and `mxf` are opt-in as well. The features are independent.
```toml
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "<commit>", features = ["matroska"] }
@@ -113,7 +115,7 @@ Other differences:
- `isobmff::ByteSource`, `matroska::ByteSource` and `ogg::ByteSource` are one trait,
`craft_media::ByteSource` (with `ReadSeekSource`, which was Matroska's). A type FilmCraft
implements both traits for keeps one of its two impls.
- `isobmff::Track`, `matroska::Track`, `ogg::Stream` and `avi::Stream` have `codec_params()`, all returning a
- `isobmff::Track`, `matroska::Track`, `ogg::Stream`, `avi::Stream` and `mxf::EssenceTrack` have `codec_params()`, all returning a
`CodecParams` (`CodecId::Unknown` for a codec without an identifier). The MP4 one describes
the first sample's sample description; `codec_params_for(description_index)` gives any other,
and `has_mixed_descriptions()` says whether the samples use more than one.
@@ -491,12 +493,178 @@ multi-byte damage (never a panic). `tests/oracle_avi.rs` checks ffmpeg-made file
H.264 with B-frames, BGR24, YUY2; PCM and MP3) against ffprobe; the ignored `opendml_file_past_1_gb`
checks a 1.16 GB OpenDML file with `AVIX` extensions.
## `mxf`
A clean-room MXF demuxer and writer from FilmCraft's `filmcraft-mxf` (storytold/filmcraft@4bec22b):
OP1a and OP-Atom, index tables, AVC / VC-3 / ProRes / MPEG-2 pictures and PCM sound. No GPL/LGPL
code (FFmpeg, libMXF, bmx, MXFLib) was consulted; FFmpeg is a fixture generator and test oracle
only. One dependency: `chrono`, for the calendar dates the writer stores.
### Specifications
Implemented from the SMPTE documents (editions used):
| Document | Edition | Used for |
|---|---|---|
| SMPTE ST 336 | 2017 | KLV coding: 16-byte UL keys, BER lengths |
| SMPTE ST 377-1 | 2011 (+ Amd 1:2012) | partitions, primer pack, header metadata sets and local tags, index table segments, random index pack, run-in |
| SMPTE ST 378 | 2004 | OP1a |
| SMPTE ST 390 | 2011 | OP-Atom |
| SMPTE ST 379-1 / 379-2 | 2009 / 2010 | generic container: content packages, element keys, frame / clip wrapping |
| SMPTE ST 381-1 | 2005 | MPEG video mapping (MPEG-2 identified, MPEG video descriptor) |
| SMPTE ST 381-3 | 2013 | AVC byte-stream mapping and AVC sub-descriptor |
| SMPTE ST 382 | 2007 | AES3 and Broadcast Wave audio mapping (wave / AES3 descriptors) |
| SMPTE ST 331 | 2011 | element data of 8-channel AES3 sound (D-10) |
| SMPTE ST 386 | 2004 | D-10 (IMX) mapping: container label, sound element type |
| SMPTE ST 2019-4 | 2009 | VC-3 (DNxHD / DNxHR) mapping |
| SMPTE RDD 44 | 2017 | Apple ProRes mapping |
| SMPTE RP 224 / RP 210 | registers | labels: data definitions, picture coding, essence containers, operational patterns; property labels of the primer pack (writer) |
| SMPTE ST 330 | 2011 | basic UMIDs of the written packages |
### Reading a file
```rust
use craft_media::mxf::{TrackKind, open};
let mxf = open(&file)?; // any ByteSource
println!("{} {:?}", mxf.operational_pattern.name(), mxf.timecode.map(|t| t.format()));
let v = mxf.track_of_kind(TrackKind::Picture).unwrap();
let t = &mxf.tracks[v];
let key = t.sync_before(t.sample_at(42).unwrap()); // stored index of the random-access picture
let bytes = mxf.read_sample(&file, v, key)?; // one edit unit's essence
let a = mxf.track_of_kind(TrackKind::Sound).unwrap();
let pcm = mxf.read_pcm(&file, a, 48_000, 1920)?; // planar f32, sample-exact
```
- **Open.** The header partition pack is found in the run-in (≤ 64 KiB). Every KLV packet is then
walked (keys and lengths only; essence values are skipped, never read): partition packs, primer
packs, header metadata local sets, index table segments, fill, and generic-container elements
(system items are ignored). A lost KLV sync resynchronises on the next partition pack; a
truncated file keeps everything before the cut (a truncated frame-wrapped picture is dropped,
clip-wrapped essence keeps its whole edit units).
- **Metadata.** The header metadata of the most complete partition is used (closed complete >
open complete > closed > open; later partitions win ties, so the footer's final durations are
preferred). Dynamic local tags resolve through the primer pack (e.g. `SubDescriptors`).
- **Tracks.** Material package picture/sound tracks → source clip → file package track (track
number, edit rate, origin) → descriptor (a multiple descriptor's sub-descriptor by
`LinkedTrackID`). Without a usable material package the file packages' tracks are used directly.
Elements are matched by track number, or — when the number is 0 or does not match (some OP-Atom
writers) — the only unclaimed element stream of the right kind.
- **Pictures.** One `Sample` per edit unit in stored order: frame wrapping (one element each) or
clip wrapping (one element split by the index: constant `EditUnitByteCount` or VBR entries).
Random access from index flag bit 7 (intra-only codings: every picture); without an index the
essence is scanned (AVC IDR, MPEG-2 sequence header). Presentation order from the index temporal
offsets (display position *n* is stored at *n* + `TemporalOffset[n]`). When the index marks B
pictures but has no temporal offsets (FFmpeg-written AVC), `needs_reorder` is set: the caller
orders by the bitstream (the AVC picture order counts).
- **Codecs.** From the picture essence coding label, then the essence container label, then the
essence bytes: AVC / AVC-Intra, VC-3, ProRes (profile from the label), MPEG-2,
MPEG-4 visual, JPEG 2000, DV, uncompressed.
- **Sound.** PCM chunks (frame- or clip-wrapped) with sample counts; `read_pcm` decodes
little-endian 8/16/24/32-bit PCM (wave / AES3 descriptors) and ST 331 AES3 elements (D-10).
- **Timecode.** The material package timecode component (start frame count, rounded base,
drop frame), falling back to the file package's; `Timecode::format` gives `HH:MM:SS:FF` /
`HH:MM:SS;FF`.
Not supported: OP1b/OP2x+ edit lists beyond the first source clip, external essence (OP-Atom
material packages whose other tracks live in other files are opened track by track), partial or
encrypted essence (ST 429-6), and descriptive metadata (parsed sets are ignored).
`Codec::codec_id()` and `EssenceTrack::codec_params()` give the crate-wide `CodecId` /
`CodecParams`: AVC is `H264`, VC-3 is `Dnxhd`, PCM and AES3 element data are `Pcm`; JPEG 2000, DV
and uncompressed pictures have no identifier. The size is the displayed frame, sound gets its
sample rate, channels and (for plain PCM) the little-endian `PcmFormat`. MXF essence carries its
configuration in band, so `extradata` is empty.
Limits, each with a test: a run-in of at most 65 535 bytes before the header partition pack, a
metadata or index value of at most 64 MiB (larger is refused before it is read), 16 MiB scanned
for the next partition pack after a lost KLV sync, an index position that cannot make a sparse
table, at most 256 sound channels (8 in an AES3 element), a PCM block alignment of at most 4096
bytes, and a sound request of at most 2²⁴ samples past the stored essence.
### Writing a file
```rust
use craft_media::mxf::*;
let ids = PackageIds::from_seed("unique seed (path + time)", "Clip name"); // material + file UMIDs
let mut cfg = WriterConfig::new(Pattern::Op1a, Rational::new(25, 1), ids);
cfg.picture = Some(PictureDesc::new(PictureCoding::Vc3 { cid: 1272 }, 1920, 1080));
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: 2, bits: 24 }];
cfg.timecode = Some(StartTimecode { frames: 90_000, rate: Rational::new(25, 1), drop_frame: false });
cfg.company = "My company".into(); // `craft-media` unless set, like `product`
let mut w = MxfWriter::new(std::io::BufWriter::new(std::fs::File::create("out.mxf")?), cfg)?;
w.push_picture(&frame_bytes, FrameInfo::intra())?; // stored order; AVC: key / P / B + display position
w.push_sound(0, &interleaved_le_pcm)?; // any chunking: content packages are cut per edit unit
w.finish()?;
```
- **Streaming.** Any `Write + Seek` sink. `new` writes the header partition (open, incomplete,
durations unknown) and a body partition; essence follows as it is pushed; `finish` writes the
footer partition (closed, complete header metadata with the final durations and the index table
segments), the random index pack, then rewrites the header partition pack and header metadata in
place (the metadata's size does not depend on the durations; a reserved fill item absorbs any
difference) and the body partition pack's footer offset. Only the index entries (11 bytes per
edit unit, plus 4 per sound slice) are kept in memory.
- **OP1a** (ST 378): one content package per edit unit, frame wrapping (ST 379-1): the picture
element then one element per sound track. Sound is buffered and cut into edit units exactly
(`floor((n+1)·rate·den/num) − floor(n·rate·den/num)` samples: 1601/1602 at 29.97). Pictures
without their sound yet wait; `finish` pads the last units' sound with silence. A multiple
descriptor with one sub-descriptor per track (linked track ids), the "multiple wrappings"
container label.
- **OP-Atom** (ST 390, Avid style): exactly one essence track per file, clip-wrapped (one element
with a 9-byte BER length patched by `finish`). Several atom files of one clip can share the
material package UMID with distinct track ids (`WriterConfig::first_track_id`). Sound files may
use the sample rate as the edit rate (a constant-bytes index) or the frame rate (VBR entries).
- **Index tables** (ST 377-1 §11): VBR entries per edit unit — stream offset (from the first
essence byte; clip-wrapped: from the element value), key-frame offset, flags (`0x80` random
access, `0xC0` AVC IDR with its parameter sets, `0x22` P, `0x33` B) and temporal offsets from the
display positions (`TO[d] = stored(d) − d`); a delta entry and a slice per sound element. Split
into segments of at most ~60 KB (the entry array is a 2-byte-length local set item).
- **Metadata.** Preface, identification (company / product / version / modification date),
content storage, essence container data (BodySID 1, IndexSID 2), material and file packages
(`PackageIds`: UMIDs and names) with a timecode track (start, rounded base, drop frame) and one
timeline track per essence (source clip → file package track; track numbers = element keys),
CDCI picture descriptors (size, aspect ratio, depth, subsampling, black / white / range, BT.709 /
BT.2020 PQ / HLG colour labels, coding label) and wave audio descriptors.
- **Mappings.** VC-3 (ST 2019-4: coding label `…71 cc` from the compression id, container `02 11`,
element types 0x0C / 0x0D), ProRes (RDD 44: coding label profile byte, container `02 1C`),
AVC byte stream (ST 381-3: container `02 10 60`, coding label from `profile_idc`; Annex B access
units with in-band SPS / PPS — the AVC sub-descriptor is not written), Broadcast Wave PCM
(ST 382: container `02 06 01/02`, element types 0x01 / 0x02).
- `write_opatom_pcm(&OpAtomPcm, &[i32])` builds a complete in-memory OP-Atom PCM file (AAF media
consolidation); `encode_pcm` interleaves planar f32 as little-endian PCM.
### Differences from `filmcraft-mxf`
- `ByteSource` is `craft_media::ByteSource`.
- `WriterConfig::new` sets `company` and `product` to `craft-media` (FilmCraft: `FilmCraft`), and
the identification set's product UID is derived from `craft-media MXF writer`. Apps set their
own names; `write_opatom_pcm` always writes the defaults.
- `EssenceTrack::sync_before` returns 0 for a track without picture samples (a sound track); it
indexed an empty table and panicked.
- `Timecode::format` saturates for a drop-frame start near `i64::MAX`; it overflowed (a panic in
a debug build).
- `Error` is `#[non_exhaustive]`; `Codec::codec_id()` and `EssenceTrack::codec_params()` are new.
### Tests
Synthetic OP1a files (temporal offsets, key flags, PCM, timecode), writer output read back (OP1a
with two sound tracks, 29.97 sound per edit unit, long-GOP AVC index, OP-Atom picture and PCM,
multi-segment index), truncation and mutation, and `hostile_tests.rs`: every metadata item, index
item and partition pack field set to extreme values, KLV lengths that lie, and the parsers on
arbitrary bytes. `tests/oracle_mxf.rs`: eight ffmpeg-written files (OP1a H.264 with and without B
pictures, DNxHR, ProRes, MPEG-2; OP-Atom DNxHR and PCM; D-10) compared with ffprobe's packets and
ffmpeg's decoded sound sample for sample; and files written by `MxfWriter` from the same essence,
which ffprobe must describe like their source and ffmpeg must decode to the same pictures and
sound.
## Containers, codecs and codec parameters
Always built, with no dependencies, so codec crates share them:
- `Container`: the file format (`Wav`, `Aiff`, `Flac`, `Mp4`, `QuickTime`, `Matroska`, `WebM`,
`Ogg`, `Caf`, `Adts`, `MpegAudio`, `Ac3`, `Avi`), with `name()`.
`Ogg`, `Caf`, `Adts`, `MpegAudio`, `Ac3`, `Avi`, `Mxf`), with `name()`.
- `CodecId`: audio and video codecs (`Aac`, `HeAac`, `Opus`, `Eac3`, `Speex`, …, `H264`, `Hevc`,
`Av1`, `ProRes`, `Dnxhd`, `Apv`, `Theora`, …, `Unknown`), with `name()`, `is_audio()`,
`is_video()`.
@@ -518,7 +686,7 @@ All are `#[non_exhaustive]`: new formats and fields are minor changes.
`craft_media::ByteSource` (`len`, `read_at(offset, buf)`) is what every demuxer reads from. It is
always built, so a crate that uses only `bitstream` can name it too. `isobmff::ByteSource`,
`matroska::ByteSource`, `ogg::ByteSource` and `avi::ByteSource` are the same trait. It is implemented for `[u8]`,
`matroska::ByteSource`, `ogg::ByteSource`, `avi::ByteSource` and `mxf::ByteSource` are the same trait. It is implemented for `[u8]`,
`Vec<u8>`, `&T`, `Arc<T>`, `Box<T>` and `std::fs::File` (not on wasm). `ReadSeekSource` adapts any
`Read + Seek`; it is `Send + Sync` when the reader is `Send`. Implement the trait once for a
custom source (a web `Blob`, a network range reader) to use every container with it.
+3
View File
@@ -38,6 +38,8 @@ pub enum Container {
Ac3,
/// AVI (RIFF), including OpenDML.
Avi,
/// MXF (SMPTE ST 377-1): OP1a, OP-Atom.
Mxf,
}
impl Container {
@@ -57,6 +59,7 @@ impl Container {
Container::MpegAudio => "MPEG audio",
Container::Ac3 => "AC-3",
Container::Avi => "AVI",
Container::Mxf => "MXF",
}
}
}
+4 -1
View File
@@ -9,10 +9,11 @@
//! - `matroska` (feature `matroska`): a clean-room Matroska (MKV) and WebM demuxer and muxer.
//! - `ogg` (feature `ogg`): a clean-room Ogg demuxer with Ogg Opus and Ogg Vorbis stream mapping.
//! - `avi` (feature `avi`): a clean-room AVI (RIFF) and OpenDML demuxer.
//! - `mxf` (feature `mxf`, enables `chrono`): a clean-room MXF demuxer and writer (OP1a, OP-Atom).
//! - [`Container`] and [`CodecId`] (no feature): the identifiers the codec crates report a file's
//! container and codec with.
//!
//! `bitstream` and `isobmff` are on by default; `matroska`, `ogg` and `avi` are opt-in. A crate that needs
//! `bitstream` and `isobmff` are on by default; `matroska`, `ogg`, `avi` and `mxf` are opt-in. A crate that needs
//! only bit I/O uses `default-features = false, features = ["bitstream"]`.
#[cfg(feature = "avi")]
@@ -24,6 +25,8 @@ mod id;
pub mod isobmff;
#[cfg(feature = "matroska")]
pub mod matroska;
#[cfg(feature = "mxf")]
pub mod mxf;
#[cfg(feature = "ogg")]
pub mod ogg;
pub mod source;
+410
View File
@@ -0,0 +1,410 @@
//! Essence identification and PCM sound decoding.
//!
//! Picture codecs are identified from the descriptor's picture essence coding label, then the
//! essence container label (ST 379-2 mapping numbers), then the first bytes of the essence.
use crate::mxf::klv::Ul;
use crate::mxf::{Rational, Result};
/// The essence codec of a track.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Codec {
/// H.264 / AVC byte stream (ST 381-3). `intra`: AVC-Intra / an intra-only profile.
Avc {
intra: bool,
},
/// SMPTE VC-3 (DNxHD / DNxHR, ST 2019-4).
Vc3,
/// Apple ProRes (RDD 44). `profile`: byte 14 of the coding label (1 Proxy … 6 4444 XQ).
ProRes {
profile: Option<u8>,
},
/// MPEG-2 video (ST 381-1, or D-10 / IMX).
Mpeg2,
/// MPEG-4 part 2 visual.
Mpeg4Visual,
Jpeg2000,
Dv,
/// Uncompressed pictures (ST 384).
Uncompressed,
/// Linear PCM, little-endian (Broadcast Wave and AES3 descriptors, ST 382).
Pcm,
/// 8-channel AES3 element data (ST 331, D-10 sound).
Aes3Element,
Unknown,
}
impl Codec {
pub fn name(&self) -> &'static str {
match self {
Codec::Avc { intra: true } => "AVC-Intra",
Codec::Avc { intra: false } => "H.264",
Codec::Vc3 => "VC-3",
Codec::ProRes { .. } => "Apple ProRes",
Codec::Mpeg2 => "MPEG-2 Video",
Codec::Mpeg4Visual => "MPEG-4 Visual",
Codec::Jpeg2000 => "JPEG 2000",
Codec::Dv => "DV",
Codec::Uncompressed => "Uncompressed",
Codec::Pcm => "PCM",
Codec::Aes3Element => "AES3 PCM",
Codec::Unknown => "unknown",
}
}
/// The crate-wide identifier: VC-3 is [`CodecId::Dnxhd`], PCM and AES3 element data are
/// [`CodecId::Pcm`]; JPEG 2000, DV and uncompressed pictures have no identifier.
///
/// [`CodecId::Dnxhd`]: crate::CodecId::Dnxhd
/// [`CodecId::Pcm`]: crate::CodecId::Pcm
pub fn codec_id(&self) -> crate::CodecId {
use crate::CodecId as Id;
match self {
Codec::Avc { .. } => Id::H264,
Codec::Vc3 => Id::Dnxhd,
Codec::ProRes { .. } => Id::ProRes,
Codec::Mpeg2 => Id::Mpeg2Video,
Codec::Mpeg4Visual => Id::Mpeg4Video,
Codec::Pcm | Codec::Aes3Element => Id::Pcm,
Codec::Jpeg2000 | Codec::Dv | Codec::Uncompressed | Codec::Unknown => Id::Unknown,
}
}
/// Every picture is coded independently.
pub fn intra_only(&self) -> bool {
matches!(self, Codec::Avc { intra: true } | Codec::Vc3 | Codec::ProRes { .. } | Codec::Jpeg2000 | Codec::Dv | Codec::Uncompressed)
}
}
/// Identify a picture codec from the picture essence coding label and the essence container label.
pub fn identify_picture(coding: Option<Ul>, container: Option<Ul>, mpeg_descriptor: bool) -> Codec {
if let Some(c) = coding.filter(Ul::is_smpte) {
let b = &c.0;
// 04 01 02 02 .. : compressed picture coding
if b[8..12] == [0x04, 0x01, 0x02, 0x02] {
match (b[12], b[13]) {
// 01: MPEG compression (ISO/IEC 13818-2 profiles, 14496-2, 14496-10)
(0x01, 0x01..=0x1F) => return Codec::Mpeg2,
(0x01, 0x20..=0x2F) => return Codec::Mpeg4Visual,
(0x01, 0x32) => return Codec::Avc { intra: true },
(0x01, 0x30..=0x3F) => return Codec::Avc { intra: false },
(0x02, _) => return Codec::Dv,
(0x03, 0x01) => return Codec::Jpeg2000,
(0x03, 0x02) => return Codec::Vc3,
(0x03, 0x06) => return Codec::ProRes { profile: Some(b[14]) },
// Avid-registered VC-3 coding labels
(0x71, _) => return Codec::Vc3,
_ => {}
}
}
if b[8..11] == [0x04, 0x01, 0x02] && b[11] == 0x01 {
return Codec::Uncompressed;
}
}
if let Some(c) = container.filter(Ul::is_smpte)
&& c.item_starts_with(&[0x0D, 0x01, 0x03, 0x01, 0x02])
{
match c.0[13] {
0x01 => return Codec::Mpeg2,
0x02 => return Codec::Dv,
0x04 if mpeg_descriptor => return Codec::Mpeg2,
0x05 => return Codec::Uncompressed,
0x0C => return Codec::Jpeg2000,
0x0F | 0x10 => return Codec::Avc { intra: false },
0x11 => return Codec::Vc3,
0x1C => return Codec::ProRes { profile: None },
_ => {}
}
}
if mpeg_descriptor { Codec::Mpeg2 } else { Codec::Unknown }
}
/// Identify a picture codec from the first bytes of an essence element.
pub fn sniff_picture(b: &[u8]) -> Option<Codec> {
if b.len() >= 8 && &b[4..8] == b"icpf" {
return Some(Codec::ProRes { profile: None });
}
if b.len() >= 5 && b[..4] == [0x00, 0x00, 0x02, 0x80] {
return Some(Codec::Vc3);
}
if b.len() >= 4 && b[..4] == [0x00, 0x00, 0x01, 0xB3] {
return Some(Codec::Mpeg2);
}
let sc4 = b.len() >= 5 && b[..4] == [0, 0, 0, 1];
let sc3 = b.len() >= 4 && b[..3] == [0, 0, 1];
if sc4 || sc3 {
let nal = b[if sc4 { 4 } else { 3 }];
if nal & 0x80 == 0 && matches!(nal & 0x1F, 1 | 5 | 6 | 7 | 8 | 9) {
return Some(Codec::Avc { intra: false });
}
}
None
}
/// Picture descriptor properties (ST 377-1 generic picture / CDCI / RGBA descriptors).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PictureInfo {
pub stored_width: u32,
pub stored_height: u32,
pub display_width: u32,
pub display_height: u32,
/// 0 full frame, 1 separate fields, 2 single field, 3 mixed fields, 4 segmented frame.
pub frame_layout: u8,
/// Display aspect ratio of the whole image.
pub aspect_ratio: Rational,
pub component_depth: u32,
pub horizontal_subsampling: u32,
pub vertical_subsampling: u32,
pub black_ref_level: Option<u32>,
pub white_ref_level: Option<u32>,
pub alpha_depth: u32,
/// RGBA descriptor (pixel layout present).
pub rgba: bool,
pub picture_coding: Option<Ul>,
pub transfer_characteristic: Option<Ul>,
pub coding_equations: Option<Ul>,
pub color_primaries: Option<Ul>,
}
impl PictureInfo {
/// Frame height: field-based layouts code the height of one field.
pub fn frame_height(&self) -> u32 {
let h = if self.display_height > 0 { self.display_height } else { self.stored_height };
if matches!(self.frame_layout, 1 | 3) { h.saturating_mul(2) } else { h }
}
pub fn frame_width(&self) -> u32 {
if self.display_width > 0 { self.display_width } else { self.stored_width }
}
/// Full-range coding (black at 0 and white at the maximum code value).
pub fn full_range(&self) -> bool {
let d = self.component_depth;
d > 0 && self.black_ref_level == Some(0) && self.white_ref_level == Some((1u32 << d.min(31)) - 1)
}
}
/// PCM layout of a sound track.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SoundFormat {
/// Interleaved little-endian samples (`bits` per sample, `block_align` bytes per frame).
Pcm,
/// ST 331 8-channel AES3 element data (4-byte header, 8 × 32-bit sub-frames per sample).
Aes3Element,
/// A coding we do not decode.
Other,
}
/// Sound descriptor properties (ST 377-1 generic sound descriptor, ST 382 wave / AES3).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SoundInfo {
pub sample_rate: Rational,
pub channels: u32,
pub bits: u32,
pub block_align: u32,
pub locked: Option<bool>,
pub sound_coding: Option<Ul>,
}
impl SoundInfo {
/// Bytes per sample frame of plain PCM.
pub fn frame_bytes(&self) -> usize {
if self.block_align > 0 {
self.block_align as usize
} else {
(self.channels.max(1) as usize).saturating_mul((self.bits as usize).div_ceil(8).max(1))
}
}
}
/// Number of sample frames in a ST 331 AES3 element (from its header; 0 if too short).
pub fn aes3_element_samples(v: &[u8]) -> usize {
if v.len() < 4 {
return 0;
}
let declared = u16::from_le_bytes([v[1], v[2]]) as usize;
declared.min((v.len() - 4) / 32)
}
/// De-interleave `bps`-byte little-endian samples (`B` = `bps`), one pass per channel.
fn decode_planes<const B: usize>(v: &[u8], frame_bytes: usize, out: &mut [Vec<f32>], scale: f32) {
for (c, o) in out.iter_mut().enumerate() {
let off = c * B;
o.extend(v.chunks_exact(frame_bytes).map(|f| {
let mut s = [0u8; B];
s.copy_from_slice(&f[off..off + B]);
let x = match B {
1 => (s[0] as i32 - 128) << 24,
2 => i32::from_le_bytes([0, 0, s[0], s[1]]),
3 => i32::from_le_bytes([0, s[0], s[1], s[2]]),
_ => i32::from_le_bytes([s[0], s[1], s[2], s[3]]),
};
(x >> (32 - 8 * B)) as f32 * scale
}));
}
}
/// Decode ST 331 sample frames (without the element header; 32 bytes each, the trailing partial
/// frame is ignored) into `channels` planar channels, at most eight.
pub(crate) fn decode_aes3_frames(frames: &[u8], channels: usize) -> Vec<Vec<f32>> {
let mut out: Vec<Vec<f32>> = (0..channels).map(|_| Vec::with_capacity(frames.len() / 32)).collect();
for f in frames.as_chunks::<32>().0 {
for (c, o) in out.iter_mut().enumerate().take(8) {
let w = u32::from_le_bytes([f[c * 4], f[c * 4 + 1], f[c * 4 + 2], f[c * 4 + 3]]);
let s = (((w >> 4) & 0xFF_FFFF) << 8) as i32 >> 8;
o.push(s as f32 / 8_388_608.0);
}
}
out
}
/// Decode PCM chunk bytes into planar f32 (`channels` channels).
///
/// `Pcm`: interleaved little-endian signed integers of `bits` (8-bit is unsigned, per WAVE),
/// `frame_bytes` per sample frame. `Aes3Element`: the 24-bit sample of each ST 331 sub-frame
/// (bits 4-27 of the little-endian 32-bit word), first `channels` of the 8 channels.
pub fn decode_pcm(v: &[u8], format: SoundFormat, channels: usize, bits: u32, frame_bytes: usize) -> Result<Vec<Vec<f32>>> {
let ch = channels.max(1);
if ch > 256 {
return Err(crate::mxf::Error::Invalid("PCM channel count exceeds 256".into()));
}
match format {
SoundFormat::Pcm => {
let bps = (bits as usize).div_ceil(8);
if bps == 0 || bps > 4 || frame_bytes < bps * ch {
return Err(crate::mxf::Error::Unsupported(format!("{bits}-bit PCM with {frame_bytes}-byte frames")));
}
// `frame_bytes >= bps * ch`, so the output is bounded by the bytes actually supplied.
let n = v.len() / frame_bytes;
let scale = 1.0 / (1u64 << (8 * bps - 1)) as f32;
// `vec![Vec::with_capacity(n); ch]` would clone empty vectors without the capacity.
let mut out: Vec<Vec<f32>> = (0..ch).map(|_| Vec::with_capacity(n)).collect();
match bps {
1 => decode_planes::<1>(v, frame_bytes, &mut out, scale),
2 => decode_planes::<2>(v, frame_bytes, &mut out, scale),
3 => decode_planes::<3>(v, frame_bytes, &mut out, scale),
_ => decode_planes::<4>(v, frame_bytes, &mut out, scale),
}
Ok(out)
}
SoundFormat::Aes3Element => {
// SMPTE 331M: an AES3 element carries at most eight channels (and an output buffer per
// declared channel would multiply a hostile header's allocation).
if ch > 8 {
return Err(crate::mxf::Error::Invalid("an AES3 element has at most eight channels".into()));
}
let n = aes3_element_samples(v);
Ok(decode_aes3_frames(&v[4.min(v.len())..][..n * 32], ch))
}
SoundFormat::Other => Err(crate::mxf::Error::Unsupported("sound coding".into())),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ul(item: &[u8]) -> Ul {
let mut b = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0A, 0, 0, 0, 0, 0, 0, 0, 0];
b[8..8 + item.len()].copy_from_slice(item);
Ul(b)
}
#[test]
fn interlaced_picture_height_does_not_overflow() {
let picture = PictureInfo { stored_height: u32::MAX, frame_layout: 1, ..Default::default() };
assert_eq!(picture.frame_height(), u32::MAX);
let picture = PictureInfo { display_height: 240, stored_height: u32::MAX, frame_layout: 3, ..Default::default() };
assert_eq!(picture.frame_height(), 480);
}
#[test]
fn identifies_picture_codings() {
assert_eq!(identify_picture(Some(ul(&[4, 1, 2, 2, 1, 0x31, 0x40, 1])), None, false), Codec::Avc { intra: false });
assert_eq!(identify_picture(Some(ul(&[4, 1, 2, 2, 1, 0x32, 0x21, 1])), None, false), Codec::Avc { intra: true });
assert_eq!(identify_picture(Some(ul(&[4, 1, 2, 2, 0x71, 0x11])), None, false), Codec::Vc3);
assert_eq!(identify_picture(Some(ul(&[4, 1, 2, 2, 3, 6, 4])), None, false), Codec::ProRes { profile: Some(4) });
assert_eq!(identify_picture(Some(ul(&[4, 1, 2, 2, 1, 1, 0x11])), None, true), Codec::Mpeg2);
// container fallback
assert_eq!(identify_picture(None, Some(ul(&[0x0D, 1, 3, 1, 2, 0x11, 1])), false), Codec::Vc3);
assert_eq!(identify_picture(None, Some(ul(&[0x0D, 1, 3, 1, 2, 0x04, 0x60, 1])), true), Codec::Mpeg2);
assert_eq!(identify_picture(None, None, false), Codec::Unknown);
}
#[test]
fn sniffs_essence() {
assert_eq!(sniff_picture(&[0, 0, 0, 1, 0x09, 0x10]), Some(Codec::Avc { intra: false }));
assert_eq!(sniff_picture(&[0, 0, 1, 0xB3, 0]), Some(Codec::Mpeg2));
assert_eq!(sniff_picture(&[0, 0, 2, 0x80, 1]), Some(Codec::Vc3));
assert_eq!(sniff_picture(&[0, 0, 0, 9, b'i', b'c', b'p', b'f']), Some(Codec::ProRes { profile: None }));
assert_eq!(sniff_picture(&[1, 2, 3]), None);
}
#[test]
fn decodes_pcm_and_aes3() {
let v = [0x00, 0x80, 0xFF, 0x7F, 0x01, 0x00, 0x00, 0x00];
let p = decode_pcm(&v, SoundFormat::Pcm, 2, 16, 4).unwrap();
assert_eq!(p[0], vec![-1.0, 1.0 / 32768.0]);
assert_eq!(p[1], vec![32767.0 / 32768.0, 0.0]);
let v24 = [0x00, 0x00, 0x80, 0xFF, 0xFF, 0x7F];
let p = decode_pcm(&v24, SoundFormat::Pcm, 1, 24, 3).unwrap();
assert_eq!(p[0], vec![-1.0, 8_388_607.0 / 8_388_608.0]);
// one AES3 element: 1 sample, channel 0 = -1, channel 1 = 0.5
let mut e = vec![0x00, 1, 0, 0xFF];
for c in 0..8u32 {
let s: i32 = match c {
0 => -8_388_608,
1 => 4_194_304,
_ => 0,
};
let w = (((s as u32) & 0xFF_FFFF) << 4) | c;
e.extend_from_slice(&w.to_le_bytes());
}
assert_eq!(aes3_element_samples(&e), 1);
let p = decode_pcm(&e, SoundFormat::Aes3Element, 2, 24, 0).unwrap();
assert_eq!(p, vec![vec![-1.0], vec![0.5]]);
// truncated: no panic, no samples
assert!(decode_pcm(&e[..10], SoundFormat::Aes3Element, 2, 24, 0).unwrap()[0].is_empty());
}
/// The per-sample loop `decode_pcm` used before the per-width passes.
fn decode_pcm_reference(v: &[u8], ch: usize, bits: u32, frame_bytes: usize) -> Vec<Vec<f32>> {
let bps = (bits as usize).div_ceil(8);
let n = v.len() / frame_bytes;
let mut out = vec![Vec::with_capacity(n); ch];
let scale = 1.0 / (1u64 << (8 * bps - 1)) as f32;
for i in 0..n {
let f = &v[i * frame_bytes..];
for (c, o) in out.iter_mut().enumerate() {
let s = &f[c * bps..c * bps + bps];
let x = match bps {
1 => (s[0] as i32 - 128) << 24,
2 => i32::from_le_bytes([0, 0, s[0], s[1]]),
3 => i32::from_le_bytes([0, s[0], s[1], s[2]]),
_ => i32::from_le_bytes([s[0], s[1], s[2], s[3]]),
};
o.push((x >> (32 - 8 * bps)) as f32 * scale);
}
}
out
}
#[test]
fn decode_pcm_matches_the_per_sample_reference() {
let mut seed = 0x1234_5678_u32;
let v: Vec<u8> = (0..4099)
.map(|_| {
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(seed >> 24) as u8
})
.collect();
for (bits, ch, pad) in [(8, 1, 0), (8, 2, 2), (16, 1, 0), (16, 2, 0), (16, 6, 3), (24, 1, 0), (24, 2, 0), (24, 3, 5), (32, 2, 0), (32, 1, 4)]
{
let bps = (bits as usize).div_ceil(8);
let fb = bps * ch + pad;
let got = decode_pcm(&v, SoundFormat::Pcm, ch, bits, fb).unwrap();
let want = decode_pcm_reference(&v, ch, bits, fb);
assert_eq!(got.len(), want.len());
for (g, w) in got.iter().zip(&want) {
assert!(g.iter().map(|x| x.to_bits()).eq(w.iter().map(|x| x.to_bits())), "{bits}-bit, {ch} channels, {pad} pad");
assert_eq!(g.capacity(), v.len() / fb);
}
}
}
}
File diff suppressed because it is too large Load Diff
+358
View File
@@ -0,0 +1,358 @@
//! Hostile input: written files with single fields of their metadata, index tables and partition
//! packs set to extreme values, KLV lengths that lie, and the public parsers on arbitrary bytes.
//! Errors, warnings and empty tables are fine; a panic (an index out of range, an overflow or a
//! giant allocation in a debug build) is not.
use std::io::Cursor;
use super::write::{
CodedKind, FrameInfo, MxfWriter, OpAtomPcm, PackageIds, Pattern, PictureCoding, PictureDesc, SoundDesc, StartTimecode, WriterConfig,
write_opatom_pcm,
};
use super::{Error, IndexSegment, Rational, SoundFormat, aes3_element_samples, decode_pcm, klv, open, sniff};
/// Deterministic xorshift.
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n.max(1) as u64) as usize
}
}
/// OP1a: long-GOP AVC (temporal offsets, key and B flags) with stereo 16-bit and mono 24-bit sound.
fn op1a_avc() -> Vec<u8> {
let rate = Rational::new(30_000, 1001);
let mut cfg = WriterConfig::new(Pattern::Op1a, rate, PackageIds::from_seed("hostile", "Hostile"));
cfg.picture = Some(PictureDesc::new(PictureCoding::Avc { profile_idc: 100, intra: false }, 64, 36));
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: 2, bits: 16 }, SoundDesc { sample_rate: 48_000, channels: 1, bits: 24 }];
cfg.timecode = Some(StartTimecode { frames: 107_892, rate, drop_frame: true });
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
let order =
[(0, CodedKind::Intra, true), (3, CodedKind::Predicted, false), (1, CodedKind::Bidirectional, false), (2, CodedKind::Bidirectional, false)];
for gop in 0..3i64 {
for (i, (d, kind, key)) in order.iter().enumerate() {
let au = [&[0, 0, 0, 1, if *key { 0x65 } else { 0x41 }][..], &[i as u8; 40]].concat();
w.push_picture(&au, FrameInfo { key: *key, kind: *kind, display: Some(gop * 4 + d) }).unwrap();
w.push_sound(0, &vec![i as u8; 1602 * 4]).unwrap();
w.push_sound(1, &vec![gop as u8; 1602 * 3]).unwrap();
}
}
w.finish().unwrap().into_inner()
}
/// OP-Atom: a clip-wrapped VC-3 picture file.
fn atom_vc3() -> Vec<u8> {
let mut cfg = WriterConfig::new(Pattern::OpAtom, Rational::new(24, 1), PackageIds::from_seed("atom", "Atom"));
cfg.picture = Some(PictureDesc::new(PictureCoding::Vc3 { cid: 1272 }, 64, 36));
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
for i in 0..9u8 {
w.push_picture(&[&[0, 0, 2, 0x80, 1][..], &vec![i; 300 + 7 * i as usize]].concat(), FrameInfo::intra()).unwrap();
}
w.finish().unwrap().into_inner()
}
/// OP-Atom PCM, with the sample rate or a frame rate as the edit rate.
fn atom_pcm(edit_rate: Option<Rational>) -> Vec<u8> {
let opts = OpAtomPcm { sample_rate: 48_000, bits: 24, channels: 2, edit_rate, ids: PackageIds::from_seed("pcm", "PCM"), timecode: None };
write_opatom_pcm(&opts, &(0..6000).collect::<Vec<i32>>()).unwrap()
}
fn bases() -> Vec<Vec<u8>> {
vec![op1a_avc(), atom_vc3(), atom_pcm(None), atom_pcm(Some(Rational::new(25, 1)))]
}
/// Opens and uses everything the API offers. Returns whether the file opened.
fn exercise(data: &[u8]) -> bool {
let Ok(m) = open(data) else { return false };
let _ = (m.operational_pattern.name(), m.timecode.map(|t| t.format()));
for p in &m.partitions {
let _ = (p.closed(), p.complete());
}
for (ti, t) in m.tracks.iter().enumerate() {
let _ = (t.codec_params(), t.first_edit_unit(), t.stored_sample_frames(), t.samples_per_edit_unit(), t.codec.name());
let _ = t.picture.as_ref().map(|p| (p.frame_width(), p.frame_height(), p.full_range()));
let _ = t.sound.as_ref().map(|s| s.frame_bytes());
let n = t.samples.len();
for i in (0..n).step_by((n / 32).max(1)).chain(n.checked_sub(1)) {
let _ = m.read_sample(data, ti, i);
let _ = t.sync_before(i);
}
let _ = (t.sync_before(usize::MAX), t.sample_at(-1), t.sample_at(i64::MAX), t.sample_at(0));
// sound: the start, a request crossing chunks, one past the end, and absurd ones
for (start, frames) in
[(0u64, 100usize), (1500, 4000), (t.stored_sample_frames(), 16), (u64::MAX, 1), (0, usize::MAX), (u64::MAX - 3, 1 << 20)]
{
let _ = m.read_pcm(data, ti, start, frames);
}
}
let _ = (m.read_sample(data, m.tracks.len(), 0), m.read_pcm(data, m.tracks.len(), 0, 1));
true
}
/// The KLV packets of a written file: (key offset, value offset, value length).
fn klvs(f: &[u8]) -> Vec<(usize, usize, usize)> {
let mut out = Vec::new();
let mut pos = 0;
while pos + 17 <= f.len() {
let Some((len, n)) = klv::ber_length(&f[pos + 16..]) else { break };
let v = pos + 16 + n;
if v + len as usize > f.len() {
break;
}
out.push((pos, v, len as usize));
pos = v + len as usize;
}
out
}
/// The items of a local set value: (value offset, value length) of each 2-byte tag / 2-byte length.
fn items(f: &[u8], at: usize, len: usize) -> Vec<(usize, usize)> {
let mut out = Vec::new();
let mut p = at;
while p + 4 <= at + len {
let n = usize::from(u16::from_be_bytes([f[p + 2], f[p + 3]]));
if p + 4 + n > at + len {
break;
}
out.push((p + 4, n));
p += 4 + n;
}
out
}
/// Overwrite `f[at..at + len]` with one of the extremes of an integer, a rational, a batch header
/// or a reference.
fn extreme(f: &mut [u8], at: usize, len: usize, r: &mut Rng) {
let field = &mut f[at..at + len];
match r.below(8) {
0 => field.fill(0),
1 => field.fill(0xFF),
2 => {
field.fill(0xFF);
field[0] = 0x7F;
}
3 => {
field.fill(0);
field[0] = 0x80;
}
// a batch header that promises more items, or longer ones, than there are bytes
4 if len >= 8 => field[..8].copy_from_slice(&[0x7F, 0xFF, 0xFF, 0xFF, 0, 0, 0, 1]),
5 if len >= 8 => field[..8].copy_from_slice(&[0, 0, 0, 1, 0xFF, 0xFF, 0xFF, 0xF0]),
6 => {
let i = r.below(len);
field[i] ^= 1 << r.below(8);
}
_ => field.iter_mut().for_each(|b| *b = r.next() as u8),
}
}
/// Every metadata item, index table item and partition pack field of every kind of written file,
/// set to an extreme value, a few at a time.
#[test]
fn extreme_metadata_index_and_partition_fields_never_panic() {
let mut r = Rng(0x00C0_FFEE_1234_5678);
let mut opened = 0;
for base in bases() {
assert!(exercise(&base), "the unchanged file opens");
// local sets (header metadata, index table segments) and partition packs
let sets: Vec<(usize, usize, usize)> =
klvs(&base).into_iter().filter(|&(k, _, len)| len > 0 && base[k + 4] == 0x02 && matches!(base[k + 5], 0x53 | 0x05)).collect();
assert!(sets.len() > 10, "{} packets to damage", sets.len());
for round in 0..600 {
let mut f = base.clone();
for _ in 0..1 + round % 3 {
let (k, v, len) = sets[r.below(sets.len())];
if base[k + 5] == 0x53 {
let it = items(&base, v, len);
let (at, n) = it[r.below(it.len())];
if n > 0 {
extreme(&mut f, at, n, &mut r);
}
} else {
// a fixed pack (partition, primer): a whole 4- or 8-byte field
let n = [4, 8][r.below(2)].min(len);
extreme(&mut f, v + r.below(len - n + 1), n, &mut r);
}
}
opened += usize::from(exercise(&f));
}
}
assert!(opened > 1200, "most damaged files still open and are read: {opened}");
}
/// KLV lengths that lie (longer than the file, the largest BER lengths, zero) and keys that stop
/// being SMPTE labels.
#[test]
fn lying_klv_lengths_and_lost_keys_never_panic() {
let mut r = Rng(0xBAD0_BE12_3456_789A);
for base in bases() {
let all = klvs(&base);
for _ in 0..300 {
let mut f = base.clone();
for _ in 0..1 + r.below(2) {
let (k, v, _) = all[r.below(all.len())];
match r.below(5) {
0 => f[k + 16..v].fill(0xFF),
1 => f[k + 16..v].fill(0),
// an 8-byte BER length of 2^63 and more, written over the packet's start
2 if k + 25 <= f.len() => {
f[k + 16..k + 25].copy_from_slice(&[0x88, 0x80 | r.next() as u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
}
3 => f[k + r.below(16)] ^= 0xFF,
_ => f[v - 1] = f[v - 1].wrapping_add(1 + r.below(200) as u8),
}
}
if r.below(3) == 0 {
f.truncate(r.below(f.len()));
}
exercise(&f);
}
}
}
#[test]
fn parsers_never_panic_on_any_bytes() {
let mut r = Rng(0x5EED_0000_0000_0001);
for _ in 0..20_000 {
let n = r.below(96);
let b: Vec<u8> = (0..n).map(|_| [0u8, 0xFF, 0x80, 0x7F, r.next() as u8, r.next() as u8][r.below(6)]).collect();
let seg = IndexSegment::parse(&b);
for e in &seg.entries {
let _ = (e.random_access(), e.is_b());
}
let _ = (
sniff(&b),
aes3_element_samples(&b),
klv::ber_length(&b),
klv::batch_of(&b).len(),
klv::utf16_of(&b),
klv::int_of(&b, n.is_multiple_of(2)),
);
for (format, channels, bits, frame_bytes) in [
(SoundFormat::Pcm, r.below(300), r.below(70) as u32, r.below(40)),
(SoundFormat::Aes3Element, r.below(12), 24, 0),
(SoundFormat::Pcm, usize::MAX, u32::MAX, usize::MAX),
(SoundFormat::Other, 2, 16, 4),
] {
if let Ok(planes) = decode_pcm(&b, format, channels, bits, frame_bytes) {
assert!(planes.iter().all(|p| p.len() <= b.len()), "more samples than bytes");
}
}
}
}
/// A run-in may hold at most 65 535 bytes before the header partition pack.
#[test]
fn a_header_partition_behind_too_long_a_run_in_is_not_found() {
let file = atom_pcm(None);
let behind = |n: usize| [vec![0x55u8; n], file.clone()].concat();
for n in [1, 4096, 65_535] {
let f = behind(n);
assert!(sniff(&f[..(n + 64).min(f.len())]), "run-in of {n}");
let m = open(&f).unwrap();
assert_eq!(m.run_in, n as u64);
}
let f = behind(65_536 + 17);
assert!(!sniff(&f));
assert!(matches!(open(&f), Err(Error::NotMxf(_))));
}
/// A sound track has no picture samples: asking it for a random-access sample is not a panic.
#[test]
fn sync_before_on_a_track_without_samples_is_zero() {
let f = atom_pcm(None);
let m = open(&f).unwrap();
assert!(m.tracks[0].samples.is_empty());
assert_eq!((m.tracks[0].sync_before(0), m.tracks[0].sync_before(7)), (0, 0));
let f = op1a_avc();
let m = open(&f).unwrap();
let v = &m.tracks[0];
assert_eq!((v.sync_before(3), v.sync_before(4), v.sync_before(usize::MAX)), (0, 4, 8));
}
/// A start timecode at the end of the 64-bit range (a damaged timecode component) formats.
#[test]
fn a_drop_frame_timecode_at_the_end_of_the_range_formats() {
use super::Timecode;
for base in [30, 60] {
for start in [i64::MAX, i64::MAX - 1, i64::MAX / 2, i64::MIN] {
let text = Timecode { start, rounded_base: base, drop_frame: true }.format();
assert!(text.contains(';'), "{text}");
}
}
assert_eq!(Timecode { start: 107_892, rounded_base: 30, drop_frame: true }.format(), "01:00:00;00");
}
/// A source that is `.1` long and zero after its real bytes, and counts the bytes read.
struct Sparse(Vec<u8>, u64, std::cell::Cell<u64>);
impl super::ByteSource for Sparse {
fn len(&self) -> u64 {
self.1
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
self.2.set(self.2.get() + buf.len() as u64);
buf.fill(0);
let real = self.0.get(offset.min(self.0.len() as u64) as usize..).unwrap_or_default();
let n = real.len().min(buf.len());
buf[..n].copy_from_slice(&real[..n]);
Ok(())
}
}
/// The header of a written file up to and including its primer pack, then the key of its first
/// metadata set.
fn header_then_a_set_key() -> Vec<u8> {
let f = atom_pcm(None);
let all = klvs(&f);
let primer = all.iter().position(|&(k, _, _)| klv::classify(&klv::Ul(f[k..k + 16].try_into().unwrap())) == klv::KeyClass::Primer).unwrap();
let (next, _, _) = all[primer + 1];
assert_eq!(klv::classify(&klv::Ul(f[next..next + 16].try_into().unwrap())), klv::KeyClass::LocalSet);
f[..next + 16].to_vec()
}
/// A metadata value of more than 64 MiB is refused before it is read or allocated.
#[test]
fn a_metadata_value_past_the_limit_is_refused_unread() {
let limit = 64u32 << 20;
let with_len = |len: u32| {
let mut f = header_then_a_set_key();
f.push(0x84);
f.extend(len.to_be_bytes());
let total = f.len() as u64 + u64::from(len);
Sparse(f, total, Default::default())
};
let src = with_len(limit + 1);
match open(&src) {
Err(Error::Invalid(text)) => assert!(text.contains("metadata value"), "{text}"),
other => panic!("expected the value to be refused, got {:?}", other.map(|m| m.warnings)),
}
assert!(src.2.get() < 1 << 20, "{} bytes read", src.2.get());
// at the limit it is read
let src = with_len(limit);
let _ = open(&src);
assert!(src.2.get() >= u64::from(limit));
}
/// After a key that is no SMPTE label, the next partition pack is looked for in at most 16 MiB.
#[test]
fn lost_sync_scans_a_bounded_stretch_for_the_next_partition() {
// a written file cut after the key of its essence element, that key damaged
let f = atom_pcm(None);
let (essence, _, _) =
klvs(&f).into_iter().find(|&(k, _, _)| klv::classify(&klv::Ul(f[k..k + 16].try_into().unwrap())) == klv::KeyClass::Essence).unwrap();
let mut f = f[..essence + 16].to_vec();
f[essence..].fill(0x11);
let src = Sparse(f, 1 << 30, Default::default());
// nothing follows, so the file has no essence: what matters is how much was read to find out
assert!(matches!(open(&src), Err(Error::Unsupported(_))));
assert!(src.2.get() > 15 << 20, "the scan ran: {} bytes read", src.2.get());
assert!(src.2.get() < 18 << 20, "{} bytes read of a 1 GiB file", src.2.get());
}
+124
View File
@@ -0,0 +1,124 @@
//! Index table segments (ST 377-1 §11).
use crate::mxf::Rational;
use crate::mxf::klv::{Cur, int_of, rational_of};
/// One index entry (ST 377-1 §11.2.4), for one edit unit in stored order.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct IndexEntry {
/// Display-order position `n` is stored at `n + temporal_offset` (the offset belongs to the
/// entry at the display position).
pub temporal_offset: i8,
/// Stored-order offset from this edit unit to the key frame it depends on (≤ 0).
pub key_frame_offset: i8,
/// Bit 7 random access, bit 6 sequence header, bits 5-4 prediction (00 I, 10 P, 11 B).
pub flags: u8,
/// Byte offset of the edit unit in the essence container stream.
pub stream_offset: u64,
}
impl IndexEntry {
pub fn random_access(&self) -> bool {
self.flags & 0x80 != 0
}
/// Uses backward prediction (a B picture).
pub fn is_b(&self) -> bool {
self.flags & 0x30 == 0x30
}
}
/// An index table segment.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct IndexSegment {
pub edit_rate: Rational,
pub start_position: i64,
/// Edit units covered (0 with a constant byte count: the whole container).
pub duration: i64,
/// Constant bytes per edit unit (CBR); 0 when the entries give the offsets (VBR).
pub edit_unit_byte_count: u32,
pub index_sid: u32,
pub body_sid: u32,
pub slice_count: u8,
pub pos_table_count: u8,
/// (pos table index, slice, element delta) per element of a content package.
pub delta_entries: Vec<(i8, u8, u32)>,
pub entries: Vec<IndexEntry>,
}
impl IndexSegment {
pub fn parse(v: &[u8]) -> IndexSegment {
let mut s = IndexSegment::default();
let mut c = Cur::new(v);
let mut entry_array: Option<&[u8]> = None;
while c.remaining() >= 4 {
let (Some(tag), Some(len)) = (c.u16(), c.u16()) else { break };
let Some(val) = c.bytes(len as usize) else { break };
match tag {
0x3F0B => s.edit_rate = rational_of(val).unwrap_or_default(),
0x3F0C => s.start_position = int_of(val, true).unwrap_or(0),
0x3F0D => s.duration = int_of(val, true).unwrap_or(0),
0x3F05 => s.edit_unit_byte_count = int_of(val, false).unwrap_or(0) as u32,
0x3F06 => s.index_sid = int_of(val, false).unwrap_or(0) as u32,
0x3F07 => s.body_sid = int_of(val, false).unwrap_or(0) as u32,
0x3F08 => s.slice_count = val.first().copied().unwrap_or(0),
0x3F0E => s.pos_table_count = val.first().copied().unwrap_or(0),
0x3F09 => {
for d in crate::mxf::klv::batch_of(val) {
if d.len() >= 6 {
s.delta_entries.push((d[0] as i8, d[1], u32::from_be_bytes([d[2], d[3], d[4], d[5]])));
}
}
}
0x3F0A => entry_array = Some(val),
_ => {}
}
}
if let Some(val) = entry_array {
for e in crate::mxf::klv::batch_of(val) {
if e.len() >= 11 {
s.entries.push(IndexEntry {
temporal_offset: e[0] as i8,
key_frame_offset: e[1] as i8,
flags: e[2],
stream_offset: u64::from_be_bytes(e[3..11].try_into().unwrap_or([0; 8])),
});
}
}
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_vbr_segment() {
let mut v = Vec::new();
let mut prop = |tag: u16, val: &[u8]| {
v.extend_from_slice(&tag.to_be_bytes());
v.extend_from_slice(&(val.len() as u16).to_be_bytes());
v.extend_from_slice(val);
};
prop(0x3F0B, &[0, 0, 0, 25, 0, 0, 0, 1]);
prop(0x3F0C, &0i64.to_be_bytes());
prop(0x3F0D, &2i64.to_be_bytes());
prop(0x3F06, &2u32.to_be_bytes());
prop(0x3F07, &1u32.to_be_bytes());
let mut arr = vec![0, 0, 0, 2, 0, 0, 0, 11];
arr.extend_from_slice(&[0, 0, 0xC0]);
arr.extend_from_slice(&0u64.to_be_bytes());
arr.extend_from_slice(&[0xFF, 0xFF, 0x33]);
arr.extend_from_slice(&1000u64.to_be_bytes());
prop(0x3F0A, &arr);
let s = IndexSegment::parse(&v);
assert_eq!(s.edit_rate, Rational::new(25, 1));
assert_eq!(s.duration, 2);
assert_eq!(s.entries.len(), 2);
assert!(s.entries[0].random_access());
assert_eq!(s.entries[1].temporal_offset, -1);
assert!(s.entries[1].is_b());
assert_eq!(s.entries[1].stream_offset, 1000);
}
}
+250
View File
@@ -0,0 +1,250 @@
//! KLV coding (SMPTE ST 336): 16-byte universal label keys, BER lengths, and the key classes used
//! by ST 377-1.
use std::fmt;
/// A SMPTE universal label (16 bytes).
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Ul(pub [u8; 16]);
impl Ul {
/// Every SMPTE label starts with the object identifier 06 0E 2B 34.
pub fn is_smpte(&self) -> bool {
self.0[..4] == [0x06, 0x0E, 0x2B, 0x34]
}
/// Compare ignoring byte 7 (the registry version, which varies between writers).
pub fn matches(&self, other: &[u8; 16]) -> bool {
self.0.iter().zip(other).enumerate().all(|(i, (a, b))| i == 7 || a == b)
}
/// Bytes 8.. start with `prefix` (registry-independent comparison of the item designator).
pub fn item_starts_with(&self, prefix: &[u8]) -> bool {
self.0[8..].starts_with(prefix)
}
}
impl fmt::Debug for Ul {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, b) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(".")?;
}
write!(f, "{b:02x}")?;
}
Ok(())
}
}
impl fmt::Display for Ul {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
/// A rational number (edit rates, sample rates, aspect ratios).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Rational {
pub num: i32,
pub den: i32,
}
impl Rational {
pub fn new(num: i32, den: i32) -> Self {
Rational { num, den }
}
pub fn is_valid(&self) -> bool {
self.num > 0 && self.den > 0
}
pub fn as_f64(&self) -> f64 {
if self.den == 0 { 0.0 } else { self.num as f64 / self.den as f64 }
}
}
/// Partition pack keys: 06 0E 2B 34 02 05 01 01 0D 01 02 01 01 kk ss 00 (kk: 02 header, 03 body,
/// 04 footer; ss: status).
pub const PARTITION_PREFIX: [u8; 13] = [0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01];
/// The kinds of KLV packets the demuxer distinguishes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyClass {
/// Partition pack: kind byte (2 header, 3 body, 4 footer) and status byte.
Partition {
kind: u8,
status: u8,
},
Primer,
/// Header metadata local set (structural or descriptive).
LocalSet,
IndexSegment,
RandomIndexPack,
Fill,
/// Generic container content (system items and essence elements, ST 379-1).
Essence,
Other,
}
pub fn classify(k: &Ul) -> KeyClass {
let b = &k.0;
if !k.is_smpte() {
return KeyClass::Other;
}
if b[..13] == PARTITION_PREFIX && (2..=4).contains(&b[13]) && b[15] == 0 {
return KeyClass::Partition { kind: b[13], status: b[14] };
}
// 06 0E 2B 34 02 05 01 01 0D 01 02 01 01 05 01 00
if b[4] == 0x02 && b[5] == 0x05 && b[8..16] == [0x0D, 0x01, 0x02, 0x01, 0x01, 0x05, 0x01, 0x00] {
return KeyClass::Primer;
}
// 06 0E 2B 34 02 05 01 01 0D 01 02 01 01 11 01 00
if b[4] == 0x02 && b[5] == 0x05 && b[8..16] == [0x0D, 0x01, 0x02, 0x01, 0x01, 0x11, 0x01, 0x00] {
return KeyClass::RandomIndexPack;
}
// 06 0E 2B 34 02 53 01 01 0D 01 02 01 01 10 01 00 (also seen with 2-byte BER lengths: 02 13 / 02 33)
if b[4] == 0x02 && b[8..16] == [0x0D, 0x01, 0x02, 0x01, 0x01, 0x10, 0x01, 0x00] {
return KeyClass::IndexSegment;
}
// 06 0E 2B 34 01 01 01 0x 03 01 02 10 01 00 00 00 (KLV fill; the "01 02 10 01" legacy variant too)
if b[4] == 0x01 && b[5] == 0x01 && b[8..12] == [0x03, 0x01, 0x02, 0x10] {
return KeyClass::Fill;
}
// Generic container content: 06 0E 2B 34 xx xx xx xx 0D 01 03 01 ...
if b[8..12] == [0x0D, 0x01, 0x03, 0x01] {
return KeyClass::Essence;
}
// Header metadata sets: 06 0E 2B 34 02 53 ... (local sets with 2-byte tags and lengths)
if b[4] == 0x02 && b[5] == 0x53 {
return KeyClass::LocalSet;
}
KeyClass::Other
}
/// Decode a BER length at `b[0..]`: (length, bytes used). `None` if truncated or longer than 8 bytes.
pub fn ber_length(b: &[u8]) -> Option<(u64, usize)> {
let first = *b.first()?;
if first < 0x80 {
return Some((first as u64, 1));
}
let n = (first & 0x7F) as usize;
if n == 0 || n > 8 || b.len() < 1 + n {
return None;
}
Some((b[1..=n].iter().fold(0u64, |a, &x| (a << 8) | x as u64), 1 + n))
}
/// Offset of the header partition pack key in the run-in (ST 377-1: at most 65 535 bytes).
pub fn find_header_partition(head: &[u8]) -> Option<usize> {
let limit = head.len().min(65_536 + 16);
let mut i = 0;
while i + 16 <= limit {
if head[i..i + 13] == PARTITION_PREFIX && head[i + 13] == 0x02 {
return Some(i);
}
i += 1;
}
None
}
/// Big-endian cursor over a value.
pub struct Cur<'a> {
b: &'a [u8],
pub pos: usize,
}
impl<'a> Cur<'a> {
pub fn new(b: &'a [u8]) -> Self {
Cur { b, pos: 0 }
}
pub fn remaining(&self) -> usize {
self.b.len().saturating_sub(self.pos)
}
pub fn bytes(&mut self, n: usize) -> Option<&'a [u8]> {
let s = self.b.get(self.pos..self.pos.checked_add(n)?)?;
self.pos += n;
Some(s)
}
pub fn u16(&mut self) -> Option<u16> {
self.bytes(2).map(|b| u16::from_be_bytes([b[0], b[1]]))
}
pub fn u32(&mut self) -> Option<u32> {
self.bytes(4).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
pub fn u64(&mut self) -> Option<u64> {
self.bytes(8).map(|b| u64::from_be_bytes(b.try_into().unwrap_or([0; 8])))
}
pub fn ul(&mut self) -> Option<Ul> {
self.bytes(16).map(|b| Ul(b.try_into().unwrap_or([0; 16])))
}
}
/// Integer value of a property (1, 2, 4 or 8 bytes, big-endian, sign-extended when `signed`).
pub fn int_of(v: &[u8], signed: bool) -> Option<i64> {
if v.is_empty() || v.len() > 8 {
return None;
}
let u = v.iter().fold(0u64, |a, &x| (a << 8) | x as u64);
if signed && v.len() < 8 && v[0] & 0x80 != 0 {
let shift = 64 - 8 * v.len();
return Some(((u << shift) as i64) >> shift);
}
Some(u as i64)
}
/// A rational property (two big-endian i32).
pub fn rational_of(v: &[u8]) -> Option<Rational> {
if v.len() < 8 {
return None;
}
Some(Rational::new(i32::from_be_bytes([v[0], v[1], v[2], v[3]]), i32::from_be_bytes([v[4], v[5], v[6], v[7]])))
}
/// A UL property.
pub fn ul_of(v: &[u8]) -> Option<Ul> {
v.get(..16).map(|b| Ul(b.try_into().unwrap_or([0; 16])))
}
/// A batch / array property: count (u32), item size (u32), items.
pub fn batch_of(v: &[u8]) -> Vec<&[u8]> {
let mut c = Cur::new(v);
let (Some(n), Some(size)) = (c.u32(), c.u32()) else { return Vec::new() };
let size = size as usize;
if size == 0 {
return Vec::new();
}
let mut out = Vec::new();
for _ in 0..n {
match c.bytes(size) {
Some(b) => out.push(b),
None => break,
}
}
out
}
/// A UTF-16BE string property (trailing NULs removed).
pub fn utf16_of(v: &[u8]) -> String {
let units: Vec<u16> = v.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]])).take_while(|&u| u != 0).collect();
String::from_utf16_lossy(&units)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ber_lengths() {
assert_eq!(ber_length(&[0x05]), Some((5, 1)));
assert_eq!(ber_length(&[0x83, 0x01, 0x00, 0x00]), Some((65536, 4)));
assert_eq!(ber_length(&[0x88, 0, 0, 0, 0, 0, 0, 1, 2]), Some((258, 9)));
assert_eq!(ber_length(&[0x83, 0x01]), None);
assert_eq!(ber_length(&[0x80]), None);
assert_eq!(ber_length(&[0x89, 0, 0, 0, 0, 0, 0, 0, 0, 0]), None);
}
#[test]
fn integers_and_strings() {
assert_eq!(int_of(&[0xFF, 0xFE], true), Some(-2));
assert_eq!(int_of(&[0xFF, 0xFE], false), Some(0xFFFE));
assert_eq!(utf16_of(&[0, b'A', 0, b'b', 0, 0]), "Ab");
let b = [0, 0, 0, 2, 0, 0, 0, 1, 7, 9];
assert_eq!(batch_of(&b), vec![&[7u8][..], &[9u8][..]]);
}
}
+215
View File
@@ -0,0 +1,215 @@
//! Header metadata (ST 377-1 §9, Annex A/B): the primer pack and local sets.
//!
//! Local sets code each property as a 2-byte local tag and a 2-byte length. Tags below 0x8000 are
//! static (fixed by ST 377-1 and the mapping documents); dynamic tags (0x8000 and up) are mapped
//! to their property UL by the partition's primer pack.
use std::collections::HashMap;
use crate::mxf::klv::{Cur, Ul, batch_of, int_of, rational_of, ul_of, utf16_of};
/// Local tag → property UL (ST 377-1 §9.2).
#[derive(Clone, Debug, Default)]
pub struct Primer(pub HashMap<u16, Ul>);
impl Primer {
pub fn parse(v: &[u8]) -> Primer {
let mut map = HashMap::new();
for item in batch_of(v) {
if item.len() >= 18 {
map.insert(u16::from_be_bytes([item[0], item[1]]), Ul(item[2..18].try_into().unwrap_or([0; 16])));
}
}
Primer(map)
}
}
/// Header metadata set types: byte 14 of `06 0E 2B 34 02 53 01 01 0D 01 01 01 01 01 tt 00`.
#[allow(dead_code)]
pub mod set_type {
pub const PREFACE: u8 = 0x2F;
pub const IDENTIFICATION: u8 = 0x30;
pub const CONTENT_STORAGE: u8 = 0x18;
pub const ESSENCE_CONTAINER_DATA: u8 = 0x23;
pub const MATERIAL_PACKAGE: u8 = 0x36;
pub const SOURCE_PACKAGE: u8 = 0x37;
pub const TIMELINE_TRACK: u8 = 0x3B;
pub const EVENT_TRACK: u8 = 0x39;
pub const STATIC_TRACK: u8 = 0x3A;
pub const SEQUENCE: u8 = 0x0F;
pub const SOURCE_CLIP: u8 = 0x11;
pub const TIMECODE_COMPONENT: u8 = 0x14;
pub const FILLER: u8 = 0x09;
pub const MULTIPLE_DESCRIPTOR: u8 = 0x44;
pub const GENERIC_PICTURE_DESCRIPTOR: u8 = 0x27;
pub const CDCI_DESCRIPTOR: u8 = 0x28;
pub const RGBA_DESCRIPTOR: u8 = 0x29;
pub const GENERIC_SOUND_DESCRIPTOR: u8 = 0x42;
pub const GENERIC_DATA_DESCRIPTOR: u8 = 0x43;
pub const AES3_DESCRIPTOR: u8 = 0x47;
pub const WAVE_DESCRIPTOR: u8 = 0x48;
pub const MPEG2_VIDEO_DESCRIPTOR: u8 = 0x51;
pub const JPEG2000_SUBDESCRIPTOR: u8 = 0x5A;
pub const AVC_SUBDESCRIPTOR: u8 = 0x6E;
}
/// One header metadata local set.
#[derive(Clone, Debug, Default)]
pub struct Set {
pub key: Ul,
pub instance_uid: [u8; 16],
/// Static-tag properties.
pub props: HashMap<u16, Vec<u8>>,
/// Dynamic-tag properties, by property UL.
pub dynamic: HashMap<Ul, Vec<u8>>,
}
impl Set {
pub fn parse(key: Ul, v: &[u8], primer: &Primer) -> Set {
let mut s = Set { key, ..Default::default() };
let mut c = Cur::new(v);
while c.remaining() >= 4 {
let (Some(tag), Some(len)) = (c.u16(), c.u16()) else { break };
let Some(val) = c.bytes(len as usize) else { break };
if tag == 0x3C0A && val.len() >= 16 {
s.instance_uid.copy_from_slice(&val[..16]);
}
if tag >= 0x8000 {
if let Some(ul) = primer.0.get(&tag) {
s.dynamic.insert(*ul, val.to_vec());
}
} else {
s.props.insert(tag, val.to_vec());
}
}
s
}
/// The set type byte when this is a structural metadata set.
pub fn kind(&self) -> Option<u8> {
self.key.item_starts_with(&[0x0D, 0x01, 0x01, 0x01, 0x01, 0x01]).then_some(self.key.0[14])
}
pub fn get(&self, tag: u16) -> Option<&[u8]> {
self.props.get(&tag).map(Vec::as_slice)
}
pub fn u(&self, tag: u16) -> Option<i64> {
self.get(tag).and_then(|v| int_of(v, false))
}
pub fn i(&self, tag: u16) -> Option<i64> {
self.get(tag).and_then(|v| int_of(v, true))
}
pub fn rational(&self, tag: u16) -> Option<crate::mxf::Rational> {
self.get(tag).and_then(rational_of)
}
pub fn ul(&self, tag: u16) -> Option<Ul> {
self.get(tag).and_then(ul_of)
}
pub fn string(&self, tag: u16) -> Option<String> {
self.get(tag).map(utf16_of).filter(|s| !s.is_empty())
}
/// A strong / weak reference (16-byte UUID).
pub fn reference(&self, tag: u16) -> Option<[u8; 16]> {
self.get(tag).and_then(|v| v.get(..16)).map(|b| b.try_into().unwrap_or([0; 16]))
}
/// A batch of references.
pub fn references(&self, tag: u16) -> Vec<[u8; 16]> {
self.get(tag)
.map(|v| batch_of(v).into_iter().filter_map(|b| b.get(..16).map(|x| x.try_into().unwrap_or([0; 16]))).collect())
.unwrap_or_default()
}
/// A dynamic property whose UL's item designator (bytes 8..) equals `item`.
pub fn dynamic_item(&self, item: &[u8]) -> Option<&[u8]> {
self.dynamic.iter().find(|(k, _)| k.item_starts_with(item)).map(|(_, v)| v.as_slice())
}
}
/// All sets of one partition's header metadata, by instance UID.
#[derive(Clone, Debug, Default)]
pub struct Metadata {
pub sets: Vec<Set>,
by_uid: HashMap<[u8; 16], usize>,
}
impl Metadata {
pub fn push(&mut self, s: Set) {
if s.instance_uid != [0; 16] {
self.by_uid.insert(s.instance_uid, self.sets.len());
}
self.sets.push(s);
}
pub fn get(&self, uid: &[u8; 16]) -> Option<&Set> {
self.by_uid.get(uid).map(|&i| &self.sets[i])
}
pub fn of_kind(&self, kind: u8) -> impl Iterator<Item = &Set> {
self.sets.iter().filter(move |s| s.kind() == Some(kind))
}
}
/// Static local tags (ST 377-1 Annex B and the mapping documents).
#[allow(dead_code)]
pub mod tag {
// Generic package
pub const PACKAGE_UID: u16 = 0x4401;
pub const PACKAGE_NAME: u16 = 0x4402;
pub const PACKAGE_TRACKS: u16 = 0x4403;
pub const SOURCE_PACKAGE_DESCRIPTOR: u16 = 0x4701;
// Track
pub const TRACK_ID: u16 = 0x4801;
pub const TRACK_NUMBER: u16 = 0x4804;
pub const TRACK_NAME: u16 = 0x4802;
pub const TRACK_SEQUENCE: u16 = 0x4803;
pub const EDIT_RATE: u16 = 0x4B01;
pub const ORIGIN: u16 = 0x4B02;
// Structural components
pub const DATA_DEFINITION: u16 = 0x0201;
pub const DURATION: u16 = 0x0202;
pub const STRUCTURAL_COMPONENTS: u16 = 0x1001;
pub const START_POSITION: u16 = 0x1201;
pub const SOURCE_PACKAGE_ID: u16 = 0x1101;
pub const SOURCE_TRACK_ID: u16 = 0x1102;
pub const START_TIMECODE: u16 = 0x1501;
pub const ROUNDED_TIMECODE_BASE: u16 = 0x1502;
pub const DROP_FRAME: u16 = 0x1503;
// Preface / identification / content storage
pub const OPERATIONAL_PATTERN: u16 = 0x3B09;
pub const PRODUCT_NAME: u16 = 0x3C02;
pub const COMPANY_NAME: u16 = 0x3C01;
pub const ESSENCE_CONTAINER_DATA_LINKED_PACKAGE: u16 = 0x2701;
pub const INDEX_SID: u16 = 0x3F06;
pub const BODY_SID: u16 = 0x3F07;
// Descriptors
pub const SUB_DESCRIPTORS: u16 = 0x3F01;
pub const LINKED_TRACK_ID: u16 = 0x3006;
pub const SAMPLE_RATE: u16 = 0x3001;
pub const CONTAINER_DURATION: u16 = 0x3002;
pub const ESSENCE_CONTAINER: u16 = 0x3004;
pub const CODEC: u16 = 0x3005;
pub const FRAME_LAYOUT: u16 = 0x320C;
pub const STORED_WIDTH: u16 = 0x3203;
pub const STORED_HEIGHT: u16 = 0x3202;
pub const SAMPLED_WIDTH: u16 = 0x3205;
pub const SAMPLED_HEIGHT: u16 = 0x3204;
pub const DISPLAY_WIDTH: u16 = 0x3209;
pub const DISPLAY_HEIGHT: u16 = 0x3208;
pub const ASPECT_RATIO: u16 = 0x320E;
pub const TRANSFER_CHARACTERISTIC: u16 = 0x3210;
pub const PICTURE_ESSENCE_CODING: u16 = 0x3201;
pub const CODING_EQUATIONS: u16 = 0x321A;
pub const COLOR_PRIMARIES: u16 = 0x3219;
pub const COMPONENT_DEPTH: u16 = 0x3301;
pub const HORIZONTAL_SUBSAMPLING: u16 = 0x3302;
pub const VERTICAL_SUBSAMPLING: u16 = 0x3308;
pub const BLACK_REF_LEVEL: u16 = 0x3304;
pub const WHITE_REF_LEVEL: u16 = 0x3305;
pub const ALPHA_SAMPLE_DEPTH: u16 = 0x3309;
pub const PIXEL_LAYOUT: u16 = 0x3401;
pub const AUDIO_SAMPLING_RATE: u16 = 0x3D03;
pub const LOCKED: u16 = 0x3D02;
pub const CHANNEL_COUNT: u16 = 0x3D07;
pub const QUANTIZATION_BITS: u16 = 0x3D01;
pub const SOUND_ESSENCE_CODING: u16 = 0x3D06;
pub const BLOCK_ALIGN: u16 = 0x3D0A;
}
/// The `SubDescriptors` property (ST 377-1:2009, dynamic tag): 06 0E 2B 34 01 01 01 09 06 01 01 04 06 10 00 00.
pub const SUB_DESCRIPTORS_ITEM: [u8; 8] = [0x06, 0x01, 0x01, 0x04, 0x06, 0x10, 0x00, 0x00];
+107
View File
@@ -0,0 +1,107 @@
//! Clean-room MXF demuxer, written from the SMPTE specifications (editions in the crate README):
//! ST 377-1 (file format: KLV, partitions, primer pack, header metadata, index tables, random
//! index pack), ST 378 (OP1a), ST 390 (OP-Atom), ST 379-1/2 (generic container), ST 381-1 (MPEG
//! video mapping), ST 381-3 (AVC mapping), ST 382 (AES3 / Broadcast Wave audio mapping), ST 331
//! (AES3 element data, D-10 sound), ST 2019-4 (VC-3 mapping) and RDD 44 (ProRes mapping).
//!
//! [`open`] walks the file's KLV packets (keys and lengths only; essence payloads are never read
//! while opening), parses the partition packs, the primer pack and header metadata of the most
//! complete partition, and every index table segment, then resolves the material package to the
//! file (source) package tracks that carry essence in this file. The result is a list of
//! [`EssenceTrack`]s with per-edit-unit [`Sample`] tables for pictures (file offset, size,
//! presentation position from the index temporal offsets, random-access flag) and PCM chunk
//! tables for sound, plus the start timecode of the material package.
//!
//! [`MxfWriter`] ([`mod@write`]) writes OP1a and OP-Atom files (VC-3, ProRes, AVC byte stream, PCM)
//! with index tables and start timecode, streaming to any `Write + Seek` sink.
//!
//! One dependency (`chrono`, for the writer's calendar dates), no `unsafe`, builds for
//! `wasm32-unknown-unknown`.
//!
//! From FilmCraft's `filmcraft-mxf` (storytold/filmcraft@4bec22b). Public items keep their names.
//! `ByteSource` is the crate's one source trait.
// FilmCraft allows these lints workspace-wide: index loops follow the specifications' tables.
#![allow(clippy::needless_range_loop, clippy::type_complexity)]
mod essence;
mod file;
mod index;
mod klv;
mod meta;
pub mod write;
pub use crate::source::{ByteSource, ReadSeekSource};
pub use essence::{Codec, PictureInfo, SoundFormat, SoundInfo, aes3_element_samples, decode_pcm};
pub use file::{Chunk, EssenceTrack, MxfFile, OperationalPattern, Partition, PartitionKind, Sample, Timecode, TrackKind, Wrapping, open};
pub use index::{IndexEntry, IndexSegment};
pub use klv::{Rational, Ul};
pub use write::{
CodedKind, ColorSpace, FrameInfo, MxfWriter, OpAtomPcm, PackageIds, Pattern, PictureCoding, PictureDesc, SoundDesc, StartTimecode, Timestamp,
Umid, WriterConfig, encode_pcm, write_opatom_pcm,
};
use std::fmt;
use std::io;
/// Up to `len` bytes at `offset` (fewer at the end of the source).
pub(crate) fn read_upto(src: &(impl ByteSource + ?Sized), offset: u64, len: usize) -> io::Result<Vec<u8>> {
let n = src.len().saturating_sub(offset).min(len as u64) as usize;
let mut v = vec![0u8; n];
src.read_at(offset, &mut v)?;
Ok(v)
}
/// Errors produced by the demuxer.
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
/// Underlying I/O failure.
Io(io::Error),
/// Not an MXF file (no header partition pack).
NotMxf(String),
/// Structurally invalid data that could not be recovered from.
Invalid(String),
/// Valid but unsupported (e.g. essence stored in another file).
Unsupported(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(e) => write!(f, "I/O error: {e}"),
Error::NotMxf(s) => write!(f, "not an MXF file: {s}"),
Error::Invalid(s) => write!(f, "invalid MXF: {s}"),
Error::Unsupported(s) => write!(f, "unsupported MXF: {s}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
/// Result alias for this module.
pub type Result<T> = std::result::Result<T, Error>;
/// Whether `head` (the first bytes of a file) looks like MXF: a header partition pack key within
/// the run-in (at most 64 KiB, ST 377-1 §6.5).
pub fn sniff(head: &[u8]) -> bool {
klv::find_header_partition(head).is_some()
}
#[cfg(test)]
mod hostile_tests;
#[cfg(test)]
mod tests;
+393
View File
@@ -0,0 +1,393 @@
//! Synthetic files: a small OP1a writer (enough of ST 377-1 to exercise the demuxer without
//! ffmpeg) and robustness checks.
use super::*;
fn ul(b: &[u8]) -> [u8; 16] {
let mut k = [0u8; 16];
k[..b.len()].copy_from_slice(b);
k
}
fn ber(n: usize) -> Vec<u8> {
let mut v = vec![0x83];
v.extend_from_slice(&(n as u32).to_be_bytes()[1..]);
v
}
fn klv(out: &mut Vec<u8>, key: [u8; 16], v: &[u8]) {
out.extend_from_slice(&key);
out.extend_from_slice(&ber(v.len()));
out.extend_from_slice(v);
}
struct LocalSet(Vec<u8>);
impl LocalSet {
fn new(uid: u8) -> Self {
let mut s = LocalSet(Vec::new());
s.p(0x3C0A, &uuid(uid));
s
}
fn p(&mut self, tag: u16, v: &[u8]) -> &mut Self {
self.0.extend_from_slice(&tag.to_be_bytes());
self.0.extend_from_slice(&(v.len() as u16).to_be_bytes());
self.0.extend_from_slice(v);
self
}
}
fn uuid(n: u8) -> [u8; 16] {
let mut u = [0xAAu8; 16];
u[15] = n;
u
}
fn umid(n: u8) -> [u8; 32] {
let mut u = [0x55u8; 32];
u[31] = n;
u
}
fn batch(items: &[&[u8]]) -> Vec<u8> {
let mut v = (items.len() as u32).to_be_bytes().to_vec();
v.extend_from_slice(&(items.first().map_or(0, |i| i.len()) as u32).to_be_bytes());
for i in items {
v.extend_from_slice(i);
}
v
}
fn set_key(t: u8) -> [u8; 16] {
ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x53, 0x01, 0x01, 0x0D, 0x01, 0x01, 0x01, 0x01, 0x01, t, 0x00])
}
const PICTURE_DEF: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x01, 0, 0, 0];
const SOUND_DEF: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x02, 0, 0, 0];
const TC_DEF: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x01, 0x01, 0, 0, 0];
const OP1A: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01, 0x09, 0x00];
const PIC_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x01, 0x02, 0x01, 0x01, 0x0D, 0x01, 0x03, 0x01, 0x15, 0x01, 0x05, 0x00];
const SND_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x01, 0x02, 0x01, 0x01, 0x0D, 0x01, 0x03, 0x01, 0x16, 0x01, 0x03, 0x00];
fn partition(kind: u8, this: u64, body_sid: u32, index_sid: u32, hbc: u64) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&1u16.to_be_bytes());
v.extend_from_slice(&3u16.to_be_bytes());
v.extend_from_slice(&1u32.to_be_bytes());
v.extend_from_slice(&this.to_be_bytes());
v.extend_from_slice(&0u64.to_be_bytes());
v.extend_from_slice(&0u64.to_be_bytes());
v.extend_from_slice(&hbc.to_be_bytes());
v.extend_from_slice(&0u64.to_be_bytes());
v.extend_from_slice(&index_sid.to_be_bytes());
v.extend_from_slice(&0u64.to_be_bytes());
v.extend_from_slice(&body_sid.to_be_bytes());
v.extend_from_slice(&OP1A);
v.extend_from_slice(&batch(&[]));
let mut out = Vec::new();
klv(&mut out, ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, kind, 0x04, 0x00]), &v);
out
}
fn track(uid: u8, id: u32, number: u32, seq: u8) -> Vec<u8> {
let mut s = LocalSet::new(uid);
s.p(0x4801, &id.to_be_bytes())
.p(0x4804, &number.to_be_bytes())
.p(0x4B01, &[0, 0, 0, 25, 0, 0, 0, 1])
.p(0x4B02, &0i64.to_be_bytes())
.p(0x4803, &uuid(seq));
s.0
}
fn sequence(uid: u8, def: [u8; 16], dur: i64, comp: u8) -> Vec<u8> {
let mut s = LocalSet::new(uid);
s.p(0x0201, &def).p(0x0202, &dur.to_be_bytes()).p(0x1001, &batch(&[&uuid(comp)]));
s.0
}
fn source_clip(uid: u8, def: [u8; 16], dur: i64, pkg: [u8; 32], track: u32) -> Vec<u8> {
let mut s = LocalSet::new(uid);
s.p(0x0201, &def).p(0x0202, &dur.to_be_bytes()).p(0x1201, &0i64.to_be_bytes()).p(0x1101, &pkg).p(0x1102, &track.to_be_bytes());
s.0
}
/// An OP1a file: `n` frame-wrapped content packages, each a picture element of `pic(i)` and a
/// 16-bit stereo sound element of 1920 frames (sample k of the file = k, -k on the channels);
/// a footer index with temporal offsets `to` and flags `flags`.
fn build(n: usize, pic: impl Fn(usize) -> Vec<u8>, to: &[i8], flags: &[u8]) -> Vec<u8> {
let mut md = Vec::new();
// primer: no dynamic tags
klv(&mut md, ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x05, 0x01, 0x00]), &batch(&[]));
let mut pre = LocalSet::new(1);
pre.p(0x3B03, &uuid(2)).p(0x3B09, &OP1A);
klv(&mut md, set_key(0x2F), &pre.0);
let mut cs = LocalSet::new(2);
cs.p(0x1901, &batch(&[&uuid(3), &uuid(4)]));
klv(&mut md, set_key(0x18), &cs.0);
// material package: timecode, picture, sound
let mut mp = LocalSet::new(3);
mp.p(0x4401, &umid(1)).p(0x4402, &[0, b'M', 0, b'P']).p(0x4403, &batch(&[&uuid(10), &uuid(11), &uuid(12)]));
klv(&mut md, set_key(0x36), &mp.0);
let d = n as i64;
klv(&mut md, set_key(0x3B), &track(10, 1, 0, 20));
klv(&mut md, set_key(0x0F), &sequence(20, TC_DEF, d, 30));
let mut tc = LocalSet::new(30);
tc.p(0x0201, &TC_DEF).p(0x0202, &d.to_be_bytes()).p(0x1501, &90_000i64.to_be_bytes()).p(0x1502, &25u16.to_be_bytes()).p(0x1503, &[0]);
klv(&mut md, set_key(0x14), &tc.0);
klv(&mut md, set_key(0x3B), &track(11, 2, 0, 21));
klv(&mut md, set_key(0x0F), &sequence(21, PICTURE_DEF, d, 31));
klv(&mut md, set_key(0x11), &source_clip(31, PICTURE_DEF, d, umid(2), 2));
klv(&mut md, set_key(0x3B), &track(12, 3, 0, 22));
klv(&mut md, set_key(0x0F), &sequence(22, SOUND_DEF, d, 32));
klv(&mut md, set_key(0x11), &source_clip(32, SOUND_DEF, d, umid(2), 3));
// file package
let mut fp = LocalSet::new(4);
fp.p(0x4401, &umid(2)).p(0x4403, &batch(&[&uuid(13), &uuid(14)])).p(0x4701, &uuid(40));
klv(&mut md, set_key(0x37), &fp.0);
klv(&mut md, set_key(0x3B), &track(13, 2, 0x1501_0500, 23));
klv(&mut md, set_key(0x0F), &sequence(23, PICTURE_DEF, d, 33));
klv(&mut md, set_key(0x11), &source_clip(33, PICTURE_DEF, d, [0; 32], 0));
klv(&mut md, set_key(0x3B), &track(14, 3, 0x1601_0300, 24));
klv(&mut md, set_key(0x0F), &sequence(24, SOUND_DEF, d, 34));
klv(&mut md, set_key(0x11), &source_clip(34, SOUND_DEF, d, [0; 32], 0));
let mut mdesc = LocalSet::new(40);
mdesc.p(0x3001, &[0, 0, 0, 25, 0, 0, 0, 1]).p(0x3F01, &batch(&[&uuid(41), &uuid(42)]));
klv(&mut md, set_key(0x44), &mdesc.0);
let mut cdci = LocalSet::new(41);
cdci.p(0x3006, &2u32.to_be_bytes())
.p(0x3203, &64u32.to_be_bytes())
.p(0x3202, &32u32.to_be_bytes())
.p(0x320E, &[0, 0, 0, 2, 0, 0, 0, 1])
.p(0x3301, &8u32.to_be_bytes())
.p(0x3201, &ul(&[0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0A, 0x04, 0x01, 0x02, 0x02, 0x03, 0x06, 0x03, 0x00]));
klv(&mut md, set_key(0x28), &cdci.0);
let mut wave = LocalSet::new(42);
wave.p(0x3006, &3u32.to_be_bytes())
.p(0x3D03, &[0, 0, 0xBB, 0x80, 0, 0, 0, 1])
.p(0x3D07, &2u32.to_be_bytes())
.p(0x3D01, &16u32.to_be_bytes())
.p(0x3D0A, &4u16.to_be_bytes());
klv(&mut md, set_key(0x48), &wave.0);
let mut ecd = LocalSet::new(5);
ecd.p(0x2701, &umid(2)).p(0x3F07, &1u32.to_be_bytes()).p(0x3F06, &2u32.to_be_bytes());
klv(&mut md, set_key(0x23), &ecd.0);
let mut f = partition(2, 0, 0, 0, md.len() as u64);
f.extend_from_slice(&md);
let body_at = f.len() as u64;
f.extend_from_slice(&partition(3, body_at, 1, 0, 0));
let mut offsets = Vec::new();
let mut cp_base = None;
for i in 0..n {
let at = f.len() as u64;
let base = *cp_base.get_or_insert(at);
offsets.push(at - base);
klv(&mut f, PIC_KEY, &pic(i));
let mut snd = Vec::new();
for k in 0..1920 {
let s = ((i * 1920 + k) % 30000) as i16;
snd.extend_from_slice(&s.to_le_bytes());
snd.extend_from_slice(&(-s).to_le_bytes());
}
klv(&mut f, SND_KEY, &snd);
}
let footer_at = f.len() as u64;
f.extend_from_slice(&partition(4, footer_at, 0, 2, 0));
let mut ix = LocalSet::new(6);
let mut entries = Vec::new();
for i in 0..n {
let mut e = vec![to.get(i).copied().unwrap_or(0) as u8, 0, flags.get(i).copied().unwrap_or(0)];
e.extend_from_slice(&offsets[i].to_be_bytes());
entries.push(e);
}
let refs: Vec<&[u8]> = entries.iter().map(Vec::as_slice).collect();
ix.p(0x3F0B, &[0, 0, 0, 25, 0, 0, 0, 1])
.p(0x3F0C, &0i64.to_be_bytes())
.p(0x3F0D, &(n as i64).to_be_bytes())
.p(0x3F06, &2u32.to_be_bytes())
.p(0x3F07, &1u32.to_be_bytes())
.p(0x3F0A, &batch(&refs));
klv(&mut f, ul(&[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x53, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x10, 0x01, 0x00]), &ix.0);
f
}
/// Stored order I0 P3 B1 B2 P6 B4 B5: display d is stored at d + TO[d].
fn ipbb() -> (Vec<i8>, Vec<u8>) {
(vec![0, 1, 1, -2, 1, 1, -2], vec![0xC0, 0x22, 0x33, 0x33, 0x22, 0x33, 0x33])
}
fn prores_like(i: usize) -> Vec<u8> {
let mut v = vec![0, 0, 0, 40];
v.extend_from_slice(b"icpf");
v.extend(std::iter::repeat_n(i as u8, 32));
v
}
#[test]
fn opens_synthetic_op1a() {
let (to, flags) = ipbb();
let f = build(7, |i| vec![i as u8; 100 + i], &to, &flags);
let m = open(&f).unwrap();
assert_eq!(m.operational_pattern, OperationalPattern::Generalized { item: 1, package: 1 });
assert_eq!(m.operational_pattern.name(), "OP1a");
assert_eq!(m.material_package_name.as_deref(), Some("MP"));
let tc = m.timecode.unwrap();
assert_eq!((tc.start, tc.rounded_base, tc.drop_frame), (90_000, 25, false));
assert_eq!(tc.format(), "01:00:00:00");
let v = m.track_of_kind(TrackKind::Picture).unwrap();
let t = &m.tracks[v];
assert_eq!(t.track_number, 0x1501_0500);
assert_eq!(t.edit_rate, Rational::new(25, 1));
assert_eq!(t.duration, Some(7));
assert_eq!(t.samples.len(), 7);
let p = t.picture.as_ref().unwrap();
assert_eq!((p.frame_width(), p.frame_height()), (64, 32));
assert_eq!(t.codec, Codec::ProRes { profile: Some(3) }, "coding label wins (the bytes do not sniff as anything)");
// presentation order from the temporal offsets
let pts: Vec<i64> = t.samples.iter().map(|s| s.pts).collect();
assert_eq!(pts, vec![0, 3, 1, 2, 6, 4, 5]);
assert_eq!(t.display_order, vec![0, 2, 3, 1, 5, 6, 4]);
assert!(t.temporal_offsets);
assert_eq!(t.sample_at(3), Some(1));
// the index marks only the first picture random-access (ProRes would be intra; this label says ProRes so all are key)
assert!(t.samples.iter().all(|s| s.key));
// sample bytes
for i in 0..7 {
assert_eq!(m.read_sample(&f, v, i).unwrap(), vec![i as u8; 100 + i]);
}
// sound: 7 × 1920 frames, sample k = k on the left, -k on the right
let a = m.track_of_kind(TrackKind::Sound).unwrap();
let s = &m.tracks[a];
assert_eq!(s.codec, Codec::Pcm);
assert_eq!(s.stored_sample_frames(), 7 * 1920);
assert_eq!(s.samples_per_edit_unit(), (48000, 25));
let pcm = m.read_pcm(&f, a, 1900, 50).unwrap();
for k in 0..50 {
assert_eq!(pcm[0][k], (1900 + k) as f32 / 32768.0);
assert_eq!(pcm[1][k], -((1900 + k) as f32) / 32768.0);
}
// past the end: zeros
let tail = m.read_pcm(&f, a, 7 * 1920 - 2, 4).unwrap();
assert_eq!(tail[0][2..], [0.0, 0.0]);
}
#[test]
fn key_frames_from_index_flags_for_long_gop() {
let (to, flags) = ipbb();
// AVC-looking essence (start code + AUD) and no coding label: sniffed as AVC
let f = build(7, |i| vec![0, 0, 0, 1, 9, 0x10, i as u8], &to, &flags);
// replace the ProRes coding label so the codec comes from the bytes
let mut f2 = f.clone();
let lbl = [0x04, 0x01, 0x02, 0x02, 0x03, 0x06, 0x03, 0x00];
let at = f2.windows(8).position(|w| w == lbl).unwrap();
f2[at..at + 8].copy_from_slice(&[0x04, 0x01, 0x02, 0x02, 0x01, 0x31, 0x40, 0x01]);
let m = open(&f2).unwrap();
let t = &m.tracks[m.track_of_kind(TrackKind::Picture).unwrap()];
assert_eq!(t.codec, Codec::Avc { intra: false });
let keys: Vec<bool> = t.samples.iter().map(|s| s.key).collect();
assert_eq!(keys, vec![true, false, false, false, false, false, false]);
assert_eq!(t.sync_before(5), 0);
assert!(t.indexed);
let b: Vec<bool> = t.samples.iter().map(|s| s.b_picture).collect();
assert_eq!(b, vec![false, false, true, true, false, true, true]);
}
#[test]
fn missing_temporal_offsets_with_b_pictures_ask_for_reordering() {
let (_, flags) = ipbb();
let m = open(&build(7, prores_like, &[], &flags)).unwrap();
let t = &m.tracks[0];
assert!(!t.temporal_offsets);
// ProRes is intra-only: the B flags are ignored for key frames, reordering still reported
assert!(t.samples.iter().all(|s| s.key));
}
#[test]
fn not_mxf_and_empty() {
assert!(matches!(open(&b"RIFF....WAVE".to_vec()), Err(Error::NotMxf(_))));
assert!(open(&Vec::new()).is_err());
assert!(!sniff(b"\x1aE\xdf\xa3"));
}
#[test]
fn truncation_never_panics_and_keeps_whole_frames() {
let (to, flags) = ipbb();
let f = build(7, prores_like, &to, &flags);
let mut opened = 0;
for cut in (0..f.len()).step_by(37) {
let part = f[..cut].to_vec();
if let Ok(m) = open(&part) {
opened += 1;
for (ti, t) in m.tracks.iter().enumerate() {
for i in 0..t.samples.len() {
// every listed sample is fully inside the file
assert!(m.read_sample(&part, ti, i).is_ok(), "cut {cut} track {ti} sample {i}");
}
if t.sound.is_some() {
let _ = m.read_pcm(&part, ti, 0, 5000);
}
}
}
}
assert!(opened > 10, "truncated files with complete metadata still open ({opened})");
}
#[test]
fn mutation_never_panics() {
let (to, flags) = ipbb();
let f = build(5, prores_like, &to, &flags);
let mut seed = 0x1234_5678u64;
for _ in 0..600 {
let mut g = f.clone();
for _ in 0..4 {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
let at = (seed as usize) % g.len();
g[at] = (seed >> 32) as u8;
}
if let Ok(m) = open(&g) {
for (ti, t) in m.tracks.iter().enumerate() {
for i in 0..t.samples.len().min(8) {
let _ = m.read_sample(&g, ti, i);
}
let _ = m.read_pcm(&g, ti, 0, 100);
}
}
}
}
#[test]
fn drop_frame_timecode_format() {
let tc = Timecode { start: 107_892, rounded_base: 30, drop_frame: true };
assert_eq!(tc.format(), "01:00:00;00");
let tc = Timecode { start: 1800, rounded_base: 30, drop_frame: true };
assert_eq!(tc.format(), "00:01:00;02");
let tc = Timecode { start: 90_000 + 25 * 61 + 3, rounded_base: 25, drop_frame: false };
assert_eq!(tc.format(), "01:01:01:03");
}
#[test]
fn hostile_pcm_requests_fail_before_allocation() {
let (to, flags) = ipbb();
let bytes = build(5, prores_like, &to, &flags);
let mut file = open(&bytes).unwrap();
let track = file.track_of_kind(TrackKind::Sound).unwrap();
file.tracks[track].sound.as_mut().unwrap().channels = u32::MAX;
assert!(file.read_pcm(&bytes, track, 0, 100).is_err());
file.tracks[track].sound.as_mut().unwrap().channels = 2;
file.tracks[track].sound.as_mut().unwrap().block_align = u32::MAX;
assert!(file.read_pcm(&bytes, track, 0, 100).is_err());
assert!(file.read_pcm(&bytes, track, 0, usize::MAX).is_err());
assert!(crate::mxf::decode_pcm(&[], SoundFormat::Pcm, usize::MAX, 16, 4).is_err());
}
#[test]
fn a_corrupt_index_position_cannot_allocate_a_giant_sparse_table() {
let (to, flags) = ipbb();
let mut bytes = build(5, prores_like, &to, &flags);
let prefix = [0x3f, 0x0c, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0];
let at = bytes.windows(prefix.len()).position(|w| w == prefix).unwrap();
bytes[at + 4..at + 12].copy_from_slice(&49_999_999i64.to_be_bytes());
let file = open(&bytes).unwrap();
assert_eq!(file.tracks[file.track_of_kind(TrackKind::Picture).unwrap()].samples.len(), 5);
}
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
//! Labels and keys used by the writer (SMPTE RP 224 / RP 210 registers, ST 377-1, ST 379-2 and the
//! essence mapping documents).
/// Header metadata set key: 06 0E 2B 34 02 53 01 01 0D 01 01 01 01 01 tt 00.
pub fn set_key(kind: u8) -> [u8; 16] {
[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x53, 0x01, 0x01, 0x0D, 0x01, 0x01, 0x01, 0x01, 0x01, kind, 0x00]
}
/// Partition pack key (kind 2 header, 3 body, 4 footer; status 1 open incomplete … 4 closed complete).
pub fn partition_key(kind: u8, status: u8) -> [u8; 16] {
[0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, kind, status, 0x00]
}
pub const PRIMER_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x05, 0x01, 0x00];
pub const INDEX_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x02, 0x53, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x10, 0x01, 0x00];
pub const RIP_KEY: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x11, 0x01, 0x00];
/// OP1a (ST 378); byte 14 qualifiers: bit 3 multi-track, bit 0 always set.
pub fn op1a(multi_track: bool) -> [u8; 16] {
[0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01, if multi_track { 0x09 } else { 0x01 }, 0x00]
}
/// OP-Atom (ST 390).
pub const OP_ATOM: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x02, 0x0D, 0x01, 0x02, 0x01, 0x10, 0x00, 0x00, 0x00];
/// Data definitions (RP 224).
pub const DEF_PICTURE: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00];
pub const DEF_SOUND: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00];
pub const DEF_TIMECODE: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x01, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00];
/// Generic container: multiple wrappings (ST 379-2).
pub const EC_MULTIPLE: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x03, 0x0D, 0x01, 0x03, 0x01, 0x02, 0x7F, 0x01, 0x00];
/// Essence container label of a generic-container mapping: 06 0E 2B 34 04 01 01 vv 0D 01 03 01 02 mm ww xx.
pub fn essence_container(version: u8, mapping: u8, b14: u8, b15: u8) -> [u8; 16] {
[0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, version, 0x0D, 0x01, 0x03, 0x01, 0x02, mapping, b14, b15]
}
/// Generic container element key: item type (0x15 picture, 0x16 sound), element count, element type, number.
pub fn element_key(item: u8, count: u8, kind: u8, number: u8) -> [u8; 16] {
[0x06, 0x0E, 0x2B, 0x34, 0x01, 0x02, 0x01, 0x01, 0x0D, 0x01, 0x03, 0x01, item, count, kind, number]
}
pub const KLV_FILL: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x01, 0x01, 0x01, 0x02, 0x03, 0x01, 0x02, 0x10, 0x01, 0x00, 0x00, 0x00];
/// Colour labels (RP 224): transfer characteristics, coding equations, colour primaries.
pub const TC_709: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x04, 0x01, 0x01, 0x01, 0x01, 0x02, 0x00, 0x00];
pub const TC_PQ: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, 0x04, 0x01, 0x01, 0x01, 0x01, 0x0A, 0x00, 0x00];
pub const TC_HLG: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, 0x04, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x00, 0x00];
pub const CE_709: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x01, 0x04, 0x01, 0x01, 0x01, 0x02, 0x02, 0x00, 0x00];
pub const CE_2020: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, 0x04, 0x01, 0x01, 0x01, 0x02, 0x06, 0x00, 0x00];
pub const CP_709: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x06, 0x04, 0x01, 0x01, 0x01, 0x03, 0x03, 0x00, 0x00];
pub const CP_2020: [u8; 16] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, 0x04, 0x01, 0x01, 0x01, 0x03, 0x04, 0x00, 0x00];
/// Static local tags written, with their property labels (RP 210), for the primer pack.
pub const PRIMER: &[(u16, [u8; 16])] = &[
(0x3C0A, ul(&[0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x15, 0x02])),
(0x3B02, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x10, 0x02, 0x04])),
(0x3B05, ul(&[0x01, 0x01, 0x01, 0x02, 0x03, 0x01, 0x02, 0x01, 0x05])),
(0x3B07, ul(&[0x01, 0x01, 0x01, 0x02, 0x03, 0x01, 0x02, 0x01, 0x04])),
(0x3B06, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x06, 0x04])),
(0x3B03, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x02, 0x01])),
(0x3B09, ul(&[0x01, 0x01, 0x01, 0x05, 0x01, 0x02, 0x02, 0x03])),
(0x3B0A, ul(&[0x01, 0x01, 0x01, 0x05, 0x01, 0x02, 0x02, 0x10, 0x02, 0x01])),
(0x3B0B, ul(&[0x01, 0x01, 0x01, 0x05, 0x01, 0x02, 0x02, 0x10, 0x02, 0x02])),
(0x3C09, ul(&[0x01, 0x01, 0x01, 0x02, 0x05, 0x20, 0x07, 0x01, 0x01])),
(0x3C01, ul(&[0x01, 0x01, 0x01, 0x02, 0x05, 0x20, 0x07, 0x01, 0x02, 0x01])),
(0x3C02, ul(&[0x01, 0x01, 0x01, 0x02, 0x05, 0x20, 0x07, 0x01, 0x03, 0x01])),
(0x3C04, ul(&[0x01, 0x01, 0x01, 0x02, 0x05, 0x20, 0x07, 0x01, 0x05, 0x01])),
(0x3C05, ul(&[0x01, 0x01, 0x01, 0x02, 0x05, 0x20, 0x07, 0x01, 0x07])),
(0x3C06, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x10, 0x02, 0x03])),
(0x1901, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x05, 0x01])),
(0x1902, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x05, 0x02])),
(0x2701, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x06, 0x01])),
(0x3F06, ul(&[0x01, 0x01, 0x01, 0x04, 0x01, 0x03, 0x04, 0x05])),
(0x3F07, ul(&[0x01, 0x01, 0x01, 0x04, 0x01, 0x03, 0x04, 0x04])),
(0x4401, ul(&[0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x15, 0x10])),
(0x4402, ul(&[0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x03, 0x02, 0x01])),
(0x4405, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x10, 0x01, 0x03])),
(0x4404, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x10, 0x02, 0x05])),
(0x4403, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x06, 0x05])),
(0x4701, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x02, 0x03])),
(0x4801, ul(&[0x01, 0x01, 0x01, 0x02, 0x01, 0x07, 0x01, 0x01])),
(0x4804, ul(&[0x01, 0x01, 0x01, 0x02, 0x01, 0x04, 0x01, 0x03])),
(0x4802, ul(&[0x01, 0x01, 0x01, 0x02, 0x01, 0x07, 0x01, 0x02, 0x01])),
(0x4803, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x02, 0x04])),
(0x4B01, ul(&[0x01, 0x01, 0x01, 0x02, 0x05, 0x30, 0x04, 0x05])),
(0x4B02, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x03, 0x01, 0x03])),
(0x0201, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x07, 0x01])),
(0x0202, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x02, 0x01, 0x01, 0x03])),
(0x1001, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x06, 0x09])),
(0x1201, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x03, 0x01, 0x04])),
(0x1101, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x03, 0x01])),
(0x1102, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x03, 0x02])),
(0x1502, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x04, 0x01, 0x01, 0x02, 0x06])),
(0x1501, ul(&[0x01, 0x01, 0x01, 0x02, 0x07, 0x02, 0x01, 0x03, 0x01, 0x05])),
(0x1503, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x04, 0x01, 0x01, 0x05])),
(0x3F01, ul(&[0x01, 0x01, 0x01, 0x04, 0x06, 0x01, 0x01, 0x04, 0x06, 0x0B])),
(0x3001, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x06, 0x01, 0x01])),
(0x3002, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x06, 0x01, 0x02])),
(0x3004, ul(&[0x01, 0x01, 0x01, 0x02, 0x06, 0x01, 0x01, 0x04, 0x01, 0x02])),
(0x3006, ul(&[0x01, 0x01, 0x01, 0x05, 0x06, 0x01, 0x01, 0x03, 0x05])),
(0x320C, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x03, 0x01, 0x04])),
(0x3203, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x02, 0x02])),
(0x3202, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x02, 0x01])),
(0x3205, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x01, 0x08])),
(0x3204, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x01, 0x07])),
(0x3209, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x01, 0x0C])),
(0x3208, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x01, 0x0B])),
(0x320E, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x01, 0x01, 0x01])),
(0x320D, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x03, 0x02, 0x05])),
(0x3201, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x06, 0x01])),
(0x3210, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02])),
(0x321A, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x02, 0x01, 0x01, 0x03, 0x01])),
(0x3219, ul(&[0x01, 0x01, 0x01, 0x09, 0x04, 0x01, 0x02, 0x01, 0x01, 0x06, 0x01])),
(0x3301, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x05, 0x03, 0x0A])),
(0x3302, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x01, 0x05])),
(0x3308, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x05, 0x01, 0x10])),
(0x3303, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x01, 0x06])),
(0x3304, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x03, 0x03])),
(0x3305, ul(&[0x01, 0x01, 0x01, 0x01, 0x04, 0x01, 0x05, 0x03, 0x04])),
(0x3306, ul(&[0x01, 0x01, 0x01, 0x02, 0x04, 0x01, 0x05, 0x03, 0x05])),
(0x3D03, ul(&[0x01, 0x01, 0x01, 0x05, 0x04, 0x02, 0x03, 0x01, 0x01, 0x01])),
(0x3D02, ul(&[0x01, 0x01, 0x01, 0x04, 0x04, 0x02, 0x03, 0x01, 0x04])),
(0x3D07, ul(&[0x01, 0x01, 0x01, 0x05, 0x04, 0x02, 0x01, 0x01, 0x04])),
(0x3D01, ul(&[0x01, 0x01, 0x01, 0x04, 0x04, 0x02, 0x03, 0x03, 0x04])),
(0x3D0A, ul(&[0x01, 0x01, 0x01, 0x05, 0x04, 0x02, 0x03, 0x02, 0x01])),
(0x3D09, ul(&[0x01, 0x01, 0x01, 0x05, 0x04, 0x02, 0x03, 0x03, 0x05])),
];
/// A property label: 06 0E 2B 34 followed by `rest`, zero padded.
const fn ul(rest: &[u8]) -> [u8; 16] {
let mut k = [0u8; 16];
k[0] = 0x06;
k[1] = 0x0E;
k[2] = 0x2B;
k[3] = 0x34;
let mut i = 0;
while i < rest.len() && i < 12 {
k[4 + i] = rest[i];
i += 1;
}
k
}
+451
View File
@@ -0,0 +1,451 @@
//! Writer tests: files read back with our own demuxer; truncation and mutation never panic.
use std::io::Cursor;
use super::*;
use crate::mxf::{Codec, OperationalPattern, PartitionKind, TrackKind, Wrapping, open};
fn prores_like(i: usize) -> Vec<u8> {
let mut v = vec![0, 0, 0, 40];
v.extend_from_slice(b"icpf");
v.extend(std::iter::repeat_n(i as u8, 32 + i * 3));
v
}
fn vc3_like(i: usize) -> Vec<u8> {
let mut v = vec![0x00, 0x00, 0x02, 0x80, 0x01];
v.extend(std::iter::repeat_n(i as u8 ^ 0x5A, 500 + 7 * i));
v
}
/// 16-bit stereo PCM: frame k = (k, -k).
fn pcm16(start: usize, n: usize) -> Vec<u8> {
let mut v = Vec::new();
for k in start..start + n {
let s = (k % 30000) as i16;
v.extend_from_slice(&s.to_le_bytes());
v.extend_from_slice(&(-s).to_le_bytes());
}
v
}
/// 24-bit mono PCM: frame k = 1000 * k (wrapping inside the 24-bit range).
fn pcm24(start: usize, n: usize) -> Vec<u8> {
let mut v = Vec::new();
for k in start..start + n {
let s = ((k as i64 * 1000) % 8_000_000) as i32 - 4_000_000;
v.extend_from_slice(&s.to_le_bytes()[..3]);
}
v
}
fn op1a_config(rate: Rational, coding: PictureCoding) -> WriterConfig {
let mut cfg = WriterConfig::new(Pattern::Op1a, rate, PackageIds::from_seed("test", "Test Clip"));
cfg.picture = Some(PictureDesc::new(coding, 64, 36));
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: 2, bits: 16 }, SoundDesc { sample_rate: 48_000, channels: 1, bits: 24 }];
cfg.timecode = Some(StartTimecode { frames: 90_000, rate, drop_frame: false });
cfg
}
#[test]
fn op1a_rejects_an_unbounded_pcm_edit_unit_before_padding() {
let mut cfg = WriterConfig::new(Pattern::Op1a, Rational::new(1, 36_000), PackageIds::from_seed("low-rate", "Low Rate"));
cfg.picture = Some(PictureDesc::new(PictureCoding::ProRes { profile: 3 }, 64, 36));
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: 2, bits: 16 }];
let err = MxfWriter::new(Cursor::new(Vec::new()), cfg).err().unwrap();
assert!(err.to_string().contains("PCM edit unit"));
}
/// 10 ProRes-like pictures at 25 fps with stereo 16-bit and mono 24-bit sound pushed in uneven chunks.
fn build_op1a() -> Vec<u8> {
let cfg = op1a_config(Rational::new(25, 1), PictureCoding::ProRes { profile: 3 });
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
let total = 10 * 1920;
let (mut a, mut b) = (0, 0);
for i in 0..10 {
// sound before and after the picture, in chunks unrelated to the edit units
let n = 1500.min(total - a);
w.push_sound(0, &pcm16(a, n)).unwrap();
a += n;
w.push_picture(&prores_like(i), FrameInfo::intra()).unwrap();
let m = 2500.min(total - b);
w.push_sound(1, &pcm24(b, m)).unwrap();
b += m;
}
w.push_sound(0, &pcm16(a, total - a)).unwrap();
assert_eq!(w.edit_units(), 10);
w.finish().unwrap().into_inner()
}
/// The OP1a file's partitions, names, timecode and operational pattern.
fn assert_op1a_metadata(m: &crate::mxf::MxfFile) {
assert!(m.warnings.is_empty(), "{:?}", m.warnings);
assert_eq!(m.operational_pattern, OperationalPattern::Generalized { item: 1, package: 1 });
assert_eq!(m.material_package_name.as_deref(), Some("Test Clip"));
assert_eq!((m.product_name.as_deref(), m.company_name.as_deref()), (Some("craft-media"), Some("craft-media")));
let tc = m.timecode.unwrap();
assert_eq!(tc.format(), "01:00:00:00");
// header rewritten closed and complete; body and footer partitions; footer metadata used
let kinds: Vec<(PartitionKind, u8)> = m.partitions.iter().map(|p| (p.kind, p.status)).collect();
assert_eq!(kinds, vec![(PartitionKind::Header, 4), (PartitionKind::Body, 4), (PartitionKind::Footer, 4)]);
assert!(m.partitions.iter().all(|p| p.footer_partition == m.partitions[2].offset));
}
/// The picture track's parameters and every frame's bytes.
fn assert_op1a_picture(m: &crate::mxf::MxfFile, f: &[u8]) {
let v = m.track_of_kind(TrackKind::Picture).unwrap();
let t = &m.tracks[v];
assert_eq!(t.codec, Codec::ProRes { profile: Some(3) });
assert_eq!(t.duration, Some(10));
assert_eq!(t.edit_rate, Rational::new(25, 1));
assert_eq!(t.wrapping, Wrapping::Frame);
assert!(t.indexed || t.codec.intra_only());
let p = t.picture.as_ref().unwrap();
assert_eq!((p.frame_width(), p.frame_height(), p.component_depth), (64, 36, 10));
assert_eq!(p.aspect_ratio, Rational::new(16, 9));
for i in 0..10 {
assert_eq!(m.read_sample(&f, v, i).unwrap(), prores_like(i));
}
}
/// Both sound tracks' layout and samples.
fn assert_op1a_sound(m: &crate::mxf::MxfFile, f: &[u8]) {
let sounds: Vec<usize> = (0..m.tracks.len()).filter(|&i| m.tracks[i].kind == TrackKind::Sound).collect();
assert_eq!(sounds.len(), 2);
let s0 = &m.tracks[sounds[0]];
assert_eq!(s0.codec, Codec::Pcm);
assert_eq!(s0.stored_sample_frames(), 10 * 1920);
assert_eq!(s0.chunks.len(), 10);
let pcm = m.read_pcm(&f, sounds[0], 1900, 100).unwrap();
for k in 0..100 {
assert_eq!(pcm[0][k], (1900 + k) as f32 / 32768.0);
assert_eq!(pcm[1][k], -((1900 + k) as f32) / 32768.0);
}
let s1 = &m.tracks[sounds[1]];
assert_eq!(s1.sound.as_ref().unwrap().bits, 24);
let pcm = m.read_pcm(&f, sounds[1], 0, 10 * 1920).unwrap();
for k in (0..10 * 1920).step_by(97) {
let want = ((k as i64 * 1000) % 8_000_000) as i32 - 4_000_000;
assert_eq!(pcm[0][k], want as f32 / 8_388_608.0, "sample {k}");
}
}
/// The index has one entry per content package with two sound slices.
fn assert_op1a_index(m: &crate::mxf::MxfFile) {
// index: one entry per content package, two sound slices
let entries: usize = m.index_segments.iter().map(|s| s.entries.len()).sum();
assert_eq!(entries, 10);
assert_eq!(m.index_segments[0].slice_count, 2);
assert_eq!(m.index_segments[0].delta_entries.len(), 3);
}
#[test]
fn op1a_reads_back() {
let f = build_op1a();
let m = open(&f).unwrap();
assert_op1a_metadata(&m);
assert_eq!(m.tracks.len(), 3);
assert_op1a_picture(&m, &f);
assert_op1a_sound(&m, &f);
assert_op1a_index(&m);
}
#[test]
fn ntsc_sound_follows_the_edit_unit_pattern() {
let rate = Rational::new(30000, 1001);
let mut cfg = op1a_config(rate, PictureCoding::Vc3 { cid: 1274 });
cfg.sound.truncate(1);
cfg.timecode = Some(StartTimecode { frames: 107_892, rate, drop_frame: true });
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
let n = 12;
let total = (n as u64 * 48_000 * 1001 / 30_000) as usize;
w.push_sound(0, &pcm16(0, total)).unwrap();
for i in 0..n {
w.push_picture(&vc3_like(i), FrameInfo::intra()).unwrap();
}
let f = w.finish().unwrap().into_inner();
let m = open(&f).unwrap();
assert_eq!(m.timecode.unwrap().format(), "01:00:00;00");
let a = m.track_of_kind(TrackKind::Sound).unwrap();
let sizes: Vec<u64> = m.tracks[a].chunks.iter().map(|c| c.samples).collect();
assert_eq!(sizes[..5], [1601, 1602, 1601, 1602, 1602]);
assert_eq!(m.tracks[a].stored_sample_frames(), total as u64);
let pcm = m.read_pcm(&f, a, 0, total).unwrap();
assert!((0..total).all(|k| pcm[0][k] == (k % 30000) as f32 / 32768.0));
let v = m.track_of_kind(TrackKind::Picture).unwrap();
assert_eq!(m.tracks[v].codec, Codec::Vc3);
}
#[test]
fn long_gop_index_gives_presentation_order_and_key_frames() {
let rate = Rational::new(25, 1);
let mut cfg = op1a_config(rate, PictureCoding::Avc { profile_idc: 100, intra: false });
cfg.sound.clear();
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
// stored I0 P3 B1 B2 P6 B4 B5 I7(IDR)
let order =
[(0, CodedKind::Intra, true), (3, CodedKind::Predicted, false), (1, CodedKind::Bidirectional, false), (2, CodedKind::Bidirectional, false)];
let more =
[(6, CodedKind::Predicted, false), (4, CodedKind::Bidirectional, false), (5, CodedKind::Bidirectional, false), (7, CodedKind::Intra, true)];
for (i, (d, kind, key)) in order.iter().chain(more.iter()).enumerate() {
let au = vec![0, 0, 0, 1, if *key { 0x65 } else { 0x41 }, i as u8];
w.push_picture(&au, FrameInfo { key: *key, kind: *kind, display: Some(*d) }).unwrap();
}
let f = w.finish().unwrap().into_inner();
let m = open(&f).unwrap();
let t = &m.tracks[0];
assert_eq!(t.codec, Codec::Avc { intra: false });
assert!(t.temporal_offsets && t.indexed);
let pts: Vec<i64> = t.samples.iter().map(|s| s.pts).collect();
assert_eq!(pts, vec![0, 3, 1, 2, 6, 4, 5, 7]);
let keys: Vec<bool> = t.samples.iter().map(|s| s.key).collect();
assert_eq!(keys, vec![true, false, false, false, false, false, false, true]);
let b: Vec<bool> = t.samples.iter().map(|s| s.b_picture).collect();
assert_eq!(b, vec![false, false, true, true, false, true, true, false]);
assert_eq!(t.sample_at(4), Some(5));
}
#[test]
fn op_atom_picture_is_clip_wrapped() {
let mut cfg = WriterConfig::new(Pattern::OpAtom, Rational::new(24, 1), PackageIds::from_seed("atom-v", "Atom V"));
cfg.picture = Some(PictureDesc::new(PictureCoding::Vc3 { cid: 1272 }, 64, 36));
cfg.timecode = Some(StartTimecode { frames: 86_400, rate: Rational::new(24, 1), drop_frame: false });
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
for i in 0..9 {
w.push_picture(&vc3_like(i), FrameInfo::intra()).unwrap();
}
assert!(w.push_sound(0, &[0, 0]).is_err());
let f = w.finish().unwrap().into_inner();
let m = open(&f).unwrap();
assert!(m.warnings.is_empty(), "{:?}", m.warnings);
assert_eq!(m.operational_pattern, OperationalPattern::Atom);
assert_eq!(m.timecode.unwrap().format(), "01:00:00:00");
let t = &m.tracks[0];
assert_eq!(t.wrapping, Wrapping::Clip);
assert_eq!(t.codec, Codec::Vc3);
assert_eq!(t.samples.len(), 9);
for i in 0..9 {
assert_eq!(m.read_sample(&f, 0, i).unwrap(), vc3_like(i));
}
}
#[test]
fn op_atom_pcm_helper() {
let ids = PackageIds::from_seed("atom-a", "Atom A1");
let samples: Vec<i32> = (0..4801).map(|k| (k * 7 % 65536) - 32768).collect();
for rate in [None, Some(Rational::new(25, 1))] {
let opts = OpAtomPcm { sample_rate: 48_000, bits: 16, channels: 1, edit_rate: rate, ids: ids.clone(), timecode: None };
let f = write_opatom_pcm(&opts, &samples).unwrap();
let m = open(&f).unwrap();
assert_eq!(m.operational_pattern, OperationalPattern::Atom);
let t = &m.tracks[0];
assert_eq!(t.kind, TrackKind::Sound);
assert_eq!(t.stored_sample_frames(), 4801);
assert_eq!(t.duration, Some(if rate.is_some() { 3 } else { 4801 }));
let pcm = m.read_pcm(&f, 0, 0, 4801).unwrap();
assert!(samples.iter().zip(&pcm[0]).all(|(s, p)| *s as f32 / 32768.0 == *p));
}
// 24-bit stereo
let opts = OpAtomPcm { sample_rate: 48_000, bits: 24, channels: 2, edit_rate: None, ids, timecode: None };
let f = write_opatom_pcm(&opts, &[8_388_607, -8_388_608, 1, -1]).unwrap();
let m = open(&f).unwrap();
let pcm = m.read_pcm(&f, 0, 0, 2).unwrap();
assert_eq!(pcm[0], vec![8_388_607.0 / 8_388_608.0, 1.0 / 8_388_608.0]);
assert_eq!(pcm[1], vec![-1.0, -1.0 / 8_388_608.0]);
}
#[test]
fn package_ids_are_written() {
let f = build_op1a();
let ids = PackageIds::from_seed("test", "Test Clip");
assert!(f.windows(32).any(|w| w == ids.material.0));
assert!(f.windows(32).any(|w| w == ids.file.0));
assert_ne!(ids.material, ids.file);
assert_eq!(Umid::from_seed(b"x"), Umid::from_seed(b"x"));
assert_ne!(Umid::from_seed(b"x"), Umid::from_seed(b"y"));
}
#[test]
fn long_files_split_the_index_into_segments() {
let mut cfg = op1a_config(Rational::new(25, 1), PictureCoding::ProRes { profile: 4 });
cfg.sound.clear();
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
for i in 0..12_000 {
w.push_picture(&[0, 0, 0, 9, b'i', b'c', b'p', b'f', i as u8], FrameInfo::intra()).unwrap();
}
let f = w.finish().unwrap().into_inner();
let m = open(&f).unwrap();
assert!(m.index_segments.len() >= 2);
assert_eq!(m.index_segments.iter().map(|s| s.entries.len()).sum::<usize>(), 12_000);
assert_eq!(m.tracks[0].samples.len(), 12_000);
assert_eq!(m.read_sample(&f, 0, 11_999).unwrap()[8], (11_999 % 256) as u8);
}
#[test]
fn rejects_bad_configs() {
let ids = PackageIds::from_seed("x", "x");
let cfg = WriterConfig::new(Pattern::Op1a, Rational::new(25, 1), ids.clone());
assert!(MxfWriter::new(Cursor::new(Vec::new()), cfg).is_err());
let mut cfg = WriterConfig::new(Pattern::OpAtom, Rational::new(25, 1), ids);
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: 1, bits: 16 }; 2];
assert!(MxfWriter::new(Cursor::new(Vec::new()), cfg).is_err());
}
#[test]
fn timestamps() {
assert_eq!(Timestamp::from_unix(0), Timestamp { year: 1970, month: 1, day: 1, ..Default::default() });
let t = Timestamp::from_unix(1_700_000_000);
assert_eq!((t.year, t.month, t.day, t.hour, t.minute, t.second), (2023, 11, 14, 22, 13, 20));
let t = Timestamp::from_unix(951_782_400); // 2000-02-29
assert_eq!((t.year, t.month, t.day), (2000, 2, 29));
// Years far outside chrono's own range are preserved with month/day/time intact and only the
// year clamped to the MXF `u16`, exactly as the old hand-rolled conversion did (not collapsed
// to the epoch — reviewer FredApps). `10_000_000_000_000` s → 318857-05-20 17:46:40.
let t = Timestamp::from_unix(10_000_000_000_000);
assert_eq!((t.year, t.month, t.day, t.hour, t.minute, t.second), (65535, 5, 20, 17, 46, 40));
// Year 10000 (just past chrono's range) still fits the u16 field.
let t = Timestamp::from_unix(253_402_300_800);
assert_eq!((t.year, t.month, t.day, t.hour, t.minute, t.second), (10000, 1, 1, 0, 0, 0));
// A negative year clamps to 0 but keeps its month/day/time.
let t = Timestamp::from_unix(-10_000_000_000_000);
assert_eq!((t.year, t.month, t.day, t.hour, t.minute, t.second), (0, 8, 13, 6, 13, 20));
// The `i64` extremes the old code accepted.
let t = Timestamp::from_unix(i64::MAX);
assert_eq!((t.year, t.month, t.day, t.hour, t.minute, t.second), (65535, 12, 4, 15, 30, 7));
let t = Timestamp::from_unix(i64::MIN);
assert_eq!((t.year, t.month, t.day, t.hour, t.minute, t.second), (0, 1, 27, 8, 29, 52));
}
#[test]
fn truncated_and_mutated_files_never_panic() {
let f = build_op1a();
let mut opened = 0;
for cut in (0..f.len()).step_by(41) {
let part = f[..cut].to_vec();
if let Ok(m) = open(&part) {
opened += 1;
for (ti, t) in m.tracks.iter().enumerate() {
for i in 0..t.samples.len() {
assert!(m.read_sample(&part, ti, i).is_ok(), "cut {cut}");
}
let _ = m.read_pcm(&part, ti, 0, 3000);
}
}
}
assert!(opened > 20, "a truncated file with a complete header still opens ({opened})");
let atom = write_opatom_pcm(
&OpAtomPcm { sample_rate: 48_000, bits: 24, channels: 1, edit_rate: None, ids: PackageIds::from_seed("m", "m"), timecode: None },
&(0..3000).collect::<Vec<i32>>(),
)
.unwrap();
let mut seed = 0x9E37_79B9u64;
for src in [&f, &atom] {
for cut in (0..src.len()).step_by(53) {
let _ = open(&src[..cut].to_vec()).map(|m| m.read_pcm(&src[..cut].to_vec(), 0, 0, 100).ok());
}
for _ in 0..400 {
let mut g = src.clone();
for _ in 0..6 {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
let at = (seed as usize) % g.len();
g[at] = (seed >> 40) as u8;
}
if let Ok(m) = open(&g) {
for (ti, t) in m.tracks.iter().enumerate() {
for i in 0..t.samples.len().min(12) {
let _ = m.read_sample(&g, ti, i);
}
let _ = m.read_pcm(&g, ti, 0, 500);
}
}
}
}
}
/// #210 review: a whole-clip read of long stereo PCM (more samples than the old fixed
/// 16 777 216-sample request cap, about 175 s at 48 kHz) must work; only reads far past the stored
/// essence are refused.
#[test]
fn long_pcm_reads_backed_by_the_file_are_not_capped() {
let frames = 200 * 48_000;
let samples: Vec<i32> = (0..frames * 2).map(|k| (k % 60_000) as i32 - 30_000).collect();
let opts = OpAtomPcm { sample_rate: 48_000, bits: 16, channels: 2, edit_rate: None, ids: PackageIds::from_seed("long", "Long"), timecode: None };
let f = write_opatom_pcm(&opts, &samples).unwrap();
let m = open(&f).unwrap();
assert_eq!(m.tracks[0].stored_sample_frames(), frames as u64);
// a little past the end is zero padding, as before
let pcm = m.read_pcm(&f, 0, 0, frames + 10).unwrap();
assert_eq!((pcm.len(), pcm[0].len()), (2, frames + 10));
for k in [0, 1, frames / 2, frames - 1] {
assert_eq!(pcm[0][k], samples[2 * k] as f32 / 32768.0, "frame {k}");
assert_eq!(pcm[1][k], samples[2 * k + 1] as f32 / 32768.0, "frame {k}");
}
assert_eq!(pcm[0][frames..], [0.0; 10]);
// a request that is mostly not backed by data is refused before allocating
assert!(m.read_pcm(&f, 0, 0, frames + (1 << 24)).is_err());
}
/// One `push_sound` of a long clip must not cost more than the clip itself: draining the buffered
/// PCM from the front for every edit unit made this quadratic (about 16 s for 10 minutes).
#[test]
fn long_sound_only_push_stays_linear() {
let mut cfg = WriterConfig::new(Pattern::Op1a, Rational::new(25, 1), PackageIds::from_seed("long", "Long Sound"));
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: 1, bits: 16 }];
let frames = 600 * 48_000;
let pcm = (0..frames).flat_map(|k| ((k % 30_000) as i16).to_le_bytes()).collect::<Vec<u8>>();
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
let t = std::time::Instant::now();
w.push_sound(0, &pcm).unwrap();
let pushed = t.elapsed();
assert_eq!(w.edit_units(), 600 * 25);
let f = w.finish().unwrap().into_inner();
assert!(pushed < std::time::Duration::from_secs(3), "push_sound took {pushed:?}");
let m = open(&f).unwrap();
let a = m.track_of_kind(TrackKind::Sound).unwrap();
let got = m.read_pcm(&f, a, 0, frames).unwrap();
for k in [0, 1, 1919, 1920, frames / 2, frames - 1] {
assert_eq!(got[0][k], (k % 30_000) as f32 / 32768.0, "frame {k}");
}
}
/// Opening a file with big pictures must not read most of the file: after a big value the walk
/// refills its window with a few KiB, not 64 KiB.
#[test]
fn open_does_not_read_through_big_pictures() {
use std::cell::Cell;
struct Counting<'a> {
data: &'a [u8],
bytes: Cell<u64>,
}
impl crate::mxf::ByteSource for Counting<'_> {
fn len(&self) -> u64 {
self.data.len() as u64
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
self.bytes.set(self.bytes.get() + buf.len() as u64);
self.data.read_at(offset, buf)
}
}
let mut cfg = op1a_config(Rational::new(25, 1), PictureCoding::ProRes { profile: 3 });
cfg.sound.truncate(1);
let mut w = MxfWriter::new(Cursor::new(Vec::new()), cfg).unwrap();
let frames = 40;
w.push_sound(0, &pcm16(0, frames * 1920)).unwrap();
for i in 0..frames {
let mut pic = prores_like(i);
pic.resize(200_000 + i, i as u8);
w.push_picture(&pic, FrameInfo::intra()).unwrap();
}
let f = w.finish().unwrap().into_inner();
let src = Counting { data: &f, bytes: Cell::new(0) };
let m = open(&src).unwrap();
let v = m.track_of_kind(TrackKind::Picture).unwrap();
assert_eq!(m.tracks[v].samples.len(), frames);
assert!(m.tracks[v].samples.iter().enumerate().all(|(i, s)| s.size as usize == 200_000 + i));
let read = src.bytes.get();
assert!(read * 5 < f.len() as u64, "open read {read} of {} bytes", f.len());
}
+416
View File
@@ -0,0 +1,416 @@
//! Oracle tests for MXF (skipped without ffmpeg/ffprobe).
//!
//! Reading: ffmpeg writes OP1a, OP-Atom and D-10 files; the demuxer's tracks, sample tables, key
//! flags, timecode and PCM are compared with ffprobe's packets and ffmpeg's decoded sound.
//!
//! Writing: the essence of those files is written again with `MxfWriter` (OP1a and OP-Atom);
//! ffprobe must describe the new file as it does the source, and ffmpeg must decode the same
//! pictures and sound from it.
mod common;
use craft_media::CodecId;
use craft_media::mxf::{
Codec, CodedKind, FrameInfo, MxfFile, MxfWriter, OpAtomPcm, OperationalPattern, PackageIds, Pattern, PictureCoding, PictureDesc, Rational,
SoundDesc, SoundFormat, StartTimecode, TrackKind, Wrapping, WriterConfig, open, write_opatom_pcm,
};
use serde_json::Value;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const V25: &[&str] = &["-f", "lavfi", "-i", "testsrc2=size=320x240:rate=25"];
const TONE: &[&str] = &["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000"];
const NOISE: &[&str] = &["-f", "lavfi", "-i", "anoisesrc=color=pink:sample_rate=48000:amplitude=0.5:seed=7"];
const ALL: &[&str] = &["h264_bf2.mxf", "h264_2997df.mxf", "dnxhr_lb.mxf", "prores.mxf", "mpeg2.mxf", "atom_dnxhr.mxf", "atom_pcm.mxf", "d10.mxf"];
fn spec(name: &str) -> Vec<&'static str> {
let cat = |p: &[&[&'static str]]| p.concat();
let x264: &[&str] = &["-c:v", "libx264", "-pix_fmt", "yuv420p"];
match name {
// long-GOP H.264 with B pictures (ffmpeg writes no temporal offsets)
"h264_bf2.mxf" => {
cat(&[V25, NOISE, &["-t", "2"], x264, &["-bf", "2", "-g", "12", "-c:a", "pcm_s16le", "-ac", "2", "-timecode", "01:00:00:00"]])
}
"h264_2997df.mxf" => cat(&[
&["-f", "lavfi", "-i", "testsrc2=size=320x240:rate=30000/1001"],
NOISE,
&["-t", "1"],
x264,
&["-bf", "0", "-g", "10", "-c:a", "pcm_s24le", "-ac", "2", "-timecode", "01:00:00;00"],
]),
"dnxhr_lb.mxf" => {
cat(&[V25, TONE, &["-t", "1", "-c:v", "dnxhd", "-profile:v", "dnxhr_lb", "-pix_fmt", "yuv422p", "-c:a", "pcm_s24le", "-ac", "2"]])
}
"prores.mxf" => cat(&[V25, TONE, &["-t", "1", "-c:v", "prores_ks", "-profile:v", "3", "-pix_fmt", "yuv422p10le", "-c:a", "pcm_s16le"]]),
"mpeg2.mxf" => cat(&[V25, NOISE, &["-t", "1", "-c:v", "mpeg2video", "-bf", "2", "-c:a", "pcm_s16le"]]),
"atom_dnxhr.mxf" => cat(&[V25, &["-t", "1", "-c:v", "dnxhd", "-profile:v", "dnxhr_lb", "-pix_fmt", "yuv422p", "-f", "mxf_opatom"]]),
"atom_pcm.mxf" => cat(&[NOISE, &["-t", "1", "-c:a", "pcm_s24le", "-ac", "1", "-f", "mxf_opatom"]]),
// D-10 (IMX): intra MPEG-2 at a constant rate, sound as 8-channel AES3 elements
"d10.mxf" => cat(&[
&["-f", "lavfi", "-i", "testsrc2=size=720x608:rate=25"],
NOISE,
&["-t", "0.4", "-c:v", "mpeg2video", "-pix_fmt", "yuv422p", "-minrate", "30M", "-maxrate", "30M", "-b:v", "30M"],
&["-bufsize", "1200000", "-rc_init_occupancy", "1200000", "-intra_vlc", "1", "-non_linear_quant", "1", "-g", "1"],
&["-flags", "+ildct+low_delay", "-dc", "10", "-ps", "1", "-qmin", "1", "-qmax", "3", "-c:a", "pcm_s16le", "-ac", "4", "-f", "mxf_d10"],
]),
other => panic!("unknown fixture {other}"),
}
}
fn fixture(name: &str) -> Option<PathBuf> {
let ffmpeg = common::ffmpeg_or_skip("mxf oracle")?;
let out = common::fixtures_dir("mxf").join(name);
common::fixtures::generate(&out, |tmp: &Path| {
let mut c = Command::new(&ffmpeg);
c.args(["-v", "error", "-y", "-nostdin"]).args(spec(name));
// OP1a is chosen by the `.mxf` extension; the others name their muxer
c.arg(tmp).status().is_ok_and(|s| s.success())
})
}
/// ffprobe's packets of stream `index`: (pts, size, key).
fn packets(all: &[Value], index: i64) -> Vec<(Option<i64>, u64, bool)> {
all.iter()
.filter(|p| common::int(&p["stream_index"]) == Some(index))
.map(|p| (common::int(&p["pts"]), common::int(&p["size"]).unwrap_or(0) as u64, p["flags"].as_str().is_some_and(|f| f.starts_with('K'))))
.collect()
}
/// ffmpeg's decode of the first sound stream as planar f32 (`channels` channels).
fn ffmpeg_pcm(path: &Path, channels: usize) -> Option<Vec<Vec<f32>>> {
let ff = common::ffmpeg()?;
let out = Command::new(ff)
.args(["-v", "error", "-nostdin", "-i"])
.arg(path)
.args(["-map", "0:a:0", "-f", "f32le", "-acodec", "pcm_f32le", "-"])
.stdin(Stdio::null())
.output()
.unwrap();
assert!(out.status.success() && out.stderr.is_empty(), "{}: {}", path.display(), String::from_utf8_lossy(&out.stderr));
let samples: Vec<f32> = out.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect();
Some((0..channels).map(|c| samples.iter().skip(c).step_by(channels).copied().collect()).collect())
}
/// MD5 of ffmpeg's decode of one stream (`v:0` or `a:0`), and whatever it complained about.
fn decode_md5(path: &Path, stream: &str) -> Option<String> {
let ff = common::ffmpeg()?;
let out = Command::new(ff)
.args(["-v", "error", "-nostdin", "-i"])
.arg(path)
.args(["-map", &format!("0:{stream}"), "-f", "md5", "-"])
.stdin(Stdio::null())
.output()
.unwrap();
assert!(out.status.success() && out.stderr.is_empty(), "{}: {}", path.display(), String::from_utf8_lossy(&out.stderr));
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn expected_codec(name: &str) -> Codec {
match name {
"h264" => Codec::Avc { intra: false },
"dnxhd" => Codec::Vc3,
"prores" => Codec::ProRes { profile: None },
"mpeg2video" => Codec::Mpeg2,
other => panic!("ffprobe codec {other} has no mapping in this test"),
}
}
struct Opened {
path: PathBuf,
bytes: Vec<u8>,
mxf: MxfFile,
}
/// Open a fixture and compare every track with ffprobe.
fn check(name: &str) -> Option<Opened> {
let path = fixture(name)?;
let bytes = std::fs::read(&path).unwrap();
let mxf = open(&bytes).unwrap();
// ffmpeg's D-10 files have no index table: the key frames come from a scan, with a warning
let expected: &[&str] =
if name == "d10.mxf" { &["no index table with random-access flags: key frames found by scanning the essence"] } else { &[] };
assert_eq!(mxf.warnings, expected, "{name}");
let streams = common::streams(&path)?;
let all = common::packets(&path)?;
let av: Vec<&Value> = streams.iter().filter(|s| s["codec_type"] == "video" || s["codec_type"] == "audio").collect();
// a D-10 file's four channels are one sound track here and one stream in ffprobe
assert_eq!(mxf.tracks.len(), av.len(), "{name}: track count");
for (t, s) in mxf.tracks.iter().zip(&av) {
let index = common::int(&s["index"]).unwrap();
let pk = packets(&all, index);
let params = t.codec_params();
if s["codec_type"] == "video" {
assert_eq!(t.kind, TrackKind::Picture, "{name}");
let want = expected_codec(s["codec_name"].as_str().unwrap());
// the ProRes profile comes from the coding label
assert_eq!(std::mem::discriminant(&t.codec), std::mem::discriminant(&want), "{name}: {:?}", t.codec);
let (w, h) = (common::int(&s["width"]).unwrap() as u32, common::int(&s["height"]).unwrap() as u32);
if name == "d10.mxf" {
// D-10 stores 608 lines (two fields of 304, with 32 lines above the picture) and
// displays 576: ffprobe reports the stored size, `codec_params` the displayed one
let pic = t.picture.as_ref().unwrap();
assert_eq!((pic.stored_width, pic.stored_height * 2), (w, h), "{name}");
assert_eq!((params.width, params.height), (Some(720), Some(576)), "{name}");
} else {
assert_eq!((params.width, params.height), (Some(w), Some(h)), "{name}");
}
assert_eq!(t.samples.len(), pk.len(), "{name}: picture count");
for (i, (sample, p)) in t.samples.iter().zip(&pk).enumerate() {
assert_eq!(u64::from(sample.size), p.1, "{name}: picture {i} size");
if t.codec == Codec::Mpeg2 {
// ffprobe marks every I picture; `key` is the index's random-access bit, which
// the I picture of an open GOP does not have (see `op1a_files_match_ffprobe`)
assert!(!sample.key || p.2, "{name}: picture {i} is a key only here");
} else {
assert_eq!(sample.key, p.2, "{name}: picture {i} key flag");
}
}
assert!(t.samples.first().is_some_and(|s| s.key), "{name}: the first picture is a key");
// presentation order, where the index has temporal offsets: ffprobe's PTS order
if t.temporal_offsets {
let first = pk.iter().filter_map(|p| p.0).min().unwrap();
let theirs: Vec<i64> = pk.iter().map(|p| p.0.unwrap() - first).collect();
let ours: Vec<i64> = t.samples.iter().map(|s| s.pts).collect();
assert_eq!(ours, theirs, "{name}: presentation positions");
}
let mut order = t.display_order.clone();
order.sort_unstable();
assert_eq!(order, (0..t.samples.len()).collect::<Vec<_>>(), "{name}: the display order is a permutation");
} else {
assert_eq!(t.kind, TrackKind::Sound, "{name}");
let info = t.sound.as_ref().unwrap();
let channels = common::int(&s["channels"]).unwrap() as usize;
assert_eq!(info.channels as usize, channels, "{name}: channels");
assert_eq!((params.codec, params.sample_rate), (CodecId::Pcm, Some(48_000)), "{name}");
// every stored sample, exactly as ffmpeg decodes it
let theirs = ffmpeg_pcm(&path, channels)?;
let frames = theirs[0].len();
assert_eq!(t.stored_sample_frames(), frames as u64, "{name}: sample frames");
let track = mxf.tracks.iter().position(|x| std::ptr::eq(x, t)).unwrap();
let ours = mxf.read_pcm(&bytes, track, 0, frames).unwrap();
assert!(ours == theirs, "{name}: PCM differs from ffmpeg's decode");
// and a read from the middle
let part = mxf.read_pcm(&bytes, track, 1000, 5000).unwrap();
assert!(part.iter().zip(&theirs).all(|(a, b)| a[..] == b[1000..6000]), "{name}: PCM from sample 1000");
}
}
Some(Opened { path, bytes, mxf })
}
#[test]
fn op1a_files_match_ffprobe() {
for name in ["h264_bf2.mxf", "h264_2997df.mxf", "dnxhr_lb.mxf", "prores.mxf", "mpeg2.mxf"] {
let Some(o) = check(name) else { return };
let m = &o.mxf;
assert_eq!(m.operational_pattern, OperationalPattern::Generalized { item: 1, package: 1 }, "{name}");
assert!(m.tracks.iter().all(|t| t.wrapping == Wrapping::Frame), "{name}");
let v = &m.tracks[m.track_of_kind(TrackKind::Picture).unwrap()];
match name {
"h264_bf2.mxf" => {
assert_eq!(m.timecode.map(|t| t.format()).as_deref(), Some("01:00:00:00"));
// B pictures in the index, no temporal offsets: the order is the caller's to find
assert!(v.needs_reorder && !v.temporal_offsets && v.samples.iter().any(|s| s.b_picture));
assert!(v.samples.iter().filter(|s| s.key).count() > 1);
}
"h264_2997df.mxf" => {
assert_eq!(m.timecode.map(|t| t.format()).as_deref(), Some("01:00:00;00"));
assert_eq!(v.edit_rate, Rational::new(30_000, 1001));
assert!(!v.needs_reorder);
}
// ffmpeg's profile 3 is 422 HQ, 4 in the RDD 44 coding label
"prores.mxf" => assert_eq!(v.codec, Codec::ProRes { profile: Some(4) }),
"dnxhr_lb.mxf" => assert!(v.samples.iter().all(|s| s.key) && v.codec.intra_only()),
_ => {
// long-GOP MPEG-2 with open GOPs: the first I picture is a random-access point,
// the later ones are I pictures (index flags 00xx) without the random-access bit
let flags: Vec<u8> = m.index_segments.iter().flat_map(|s| s.entries.iter().map(|e| e.flags)).collect();
assert_eq!(flags.len(), v.samples.len());
assert!(flags[0] & 0x80 != 0 && v.samples[0].key);
let open_gop: Vec<usize> = (1..flags.len()).filter(|&i| flags[i] & 0x30 == 0 && flags[i] & 0x80 == 0).collect();
assert!(!open_gop.is_empty(), "flags {flags:02x?}");
assert!(open_gop.iter().all(|&i| !v.samples[i].key));
}
}
}
}
#[test]
fn op_atom_and_d10_files_match_ffprobe() {
let Some(o) = check("atom_dnxhr.mxf") else { return };
assert_eq!(o.mxf.operational_pattern, OperationalPattern::Atom);
assert_eq!((o.mxf.tracks[0].wrapping, o.mxf.tracks[0].codec), (Wrapping::Clip, Codec::Vc3));
let Some(o) = check("atom_pcm.mxf") else { return };
assert_eq!(o.mxf.operational_pattern, OperationalPattern::Atom);
assert_eq!(o.mxf.tracks[0].sound_format, SoundFormat::Pcm);
assert!(o.mxf.tracks[0].codec_params().pcm.is_some_and(|p| p.bits == 24 && !p.big_endian));
let Some(o) = check("d10.mxf") else { return };
let a = &o.mxf.tracks[o.mxf.track_of_kind(TrackKind::Sound).unwrap()];
assert_eq!((a.sound_format, a.codec), (SoundFormat::Aes3Element, Codec::Aes3Element));
assert_eq!(a.codec_params().pcm, None, "AES3 element data is not plain PCM");
assert_eq!(o.mxf.tracks[o.mxf.track_of_kind(TrackKind::Picture).unwrap()].codec, Codec::Mpeg2);
}
/// `profile_idc` of the first SPS in an Annex B access unit.
fn avc_profile(au: &[u8]) -> Option<u8> {
au.windows(5).find(|w| w[..3] == [0, 0, 1] && w[3] & 0x1F == 7).map(|w| w[4])
}
/// The writer's picture coding for a track read from a file.
fn coding_of(codec: Codec, first: &[u8]) -> PictureCoding {
match codec {
// the VC-3 compression id is at byte 40 of the frame header
Codec::Vc3 => PictureCoding::Vc3 { cid: u32::from_be_bytes(first[40..44].try_into().unwrap()) },
Codec::ProRes { profile } => PictureCoding::ProRes { profile: profile.unwrap() },
Codec::Avc { intra } => PictureCoding::Avc { profile_idc: avc_profile(first).unwrap(), intra },
other => panic!("the writer has no mapping for {other:?}"),
}
}
/// Write the essence of an OP1a file again as OP1a: every picture and the raw sound bytes.
fn rewrite_op1a(o: &Opened, name: &str) -> PathBuf {
let m = &o.mxf;
let vi = m.track_of_kind(TrackKind::Picture).unwrap();
let ai = m.track_of_kind(TrackKind::Sound).unwrap();
let (v, a) = (&m.tracks[vi], &m.tracks[ai]);
let first = m.read_sample(&o.bytes, vi, 0).unwrap();
let pic = v.picture.as_ref().unwrap();
let mut cfg = WriterConfig::new(Pattern::Op1a, v.edit_rate, PackageIds::from_seed(name, "Rewritten"));
cfg.picture = Some(PictureDesc::new(coding_of(v.codec, &first), pic.frame_width(), pic.frame_height()));
let s = a.sound.as_ref().unwrap();
cfg.sound = vec![SoundDesc { sample_rate: 48_000, channels: s.channels, bits: s.bits }];
let tc = m.timecode.unwrap();
cfg.timecode = Some(StartTimecode { frames: tc.start, rate: v.edit_rate, drop_frame: tc.drop_frame });
let out = common::fixtures_dir("mxf").join(format!("rewritten_{name}"));
let mut w = MxfWriter::new(std::io::BufWriter::new(std::fs::File::create(&out).unwrap()), cfg).unwrap();
for (i, sample) in v.samples.iter().enumerate() {
let data = m.read_sample(&o.bytes, vi, i).unwrap();
// no B pictures in these files: stored order is presentation order
let info = if sample.key { FrameInfo::intra() } else { FrameInfo { key: false, kind: CodedKind::Predicted, display: None } };
w.push_picture(&data, info).unwrap();
}
for c in &a.chunks {
w.push_sound(0, &o.bytes[c.offset as usize..(c.offset + c.size) as usize]).unwrap();
}
assert_eq!(w.edit_units(), v.samples.len() as u64);
w.finish().unwrap();
out
}
/// Files written by `MxfWriter` from ffmpeg-encoded essence: ffprobe describes them as it does
/// the source, ffmpeg decodes the same pictures and sound, and our own reader reads them back.
#[test]
fn written_op1a_files_are_read_by_ffmpeg_like_their_source() {
for name in ["dnxhr_lb.mxf", "prores.mxf", "h264_2997df.mxf"] {
let Some(o) = check(name) else { return };
let out = rewrite_op1a(&o, name);
let (Some(src), Some(new)) = (common::streams(&o.path), common::streams(&out)) else { return };
for (a, b) in src.iter().zip(&new) {
for key in ["codec_type", "codec_name", "width", "height", "pix_fmt", "sample_rate", "channels", "r_frame_rate", "bits_per_sample"] {
assert_eq!(a[key], b[key], "{name}: stream {key}");
}
}
assert_eq!(src.len(), new.len(), "{name}: stream count");
let (Some(pa), Some(pb)) = (common::packets(&o.path), common::packets(&out)) else { return };
for index in 0..2 {
let (a, b) = (packets(&pa, index), packets(&pb, index));
assert_eq!(a.len(), b.len(), "{name}: stream {index} packet count");
if index == 0 {
assert!(a.iter().zip(&b).all(|(x, y)| (x.1, x.2) == (y.1, y.2)), "{name}: picture sizes and key flags");
} else {
// at 29.97 an edit unit holds 1601 or 1602 samples, in an order each writer picks
assert_eq!(a.iter().map(|p| p.1).sum::<u64>(), b.iter().map(|p| p.1).sum::<u64>(), "{name}: sound bytes");
}
}
let (Some(fa), Some(fb)) = (common::probe_json(&o.path, &["-show_format"]), common::probe_json(&out, &["-show_format"])) else { return };
assert_eq!(fb["format"]["format_name"], "mxf", "{name}");
assert_eq!(fa["format"]["tags"]["timecode"], fb["format"]["tags"]["timecode"], "{name}: timecode");
assert_eq!(fb["format"]["tags"]["product_name"], "craft-media", "{name}");
for stream in ["v:0", "a:0"] {
assert_eq!(decode_md5(&o.path, stream), decode_md5(&out, stream), "{name}: ffmpeg's decode of {stream}");
}
// and our reader gives back what went in
let bytes = std::fs::read(&out).unwrap();
let m = open(&bytes).unwrap();
assert!(m.warnings.is_empty(), "{name}: {:?}", m.warnings);
assert_eq!(m.operational_pattern, OperationalPattern::Generalized { item: 1, package: 1 });
let (vi, src_vi) = (m.track_of_kind(TrackKind::Picture).unwrap(), o.mxf.track_of_kind(TrackKind::Picture).unwrap());
for i in 0..m.tracks[vi].samples.len() {
assert_eq!(m.read_sample(&bytes, vi, i).unwrap(), o.mxf.read_sample(&o.bytes, src_vi, i).unwrap(), "{name}: picture {i}");
}
}
}
#[test]
fn written_op_atom_files_are_read_by_ffmpeg() {
// picture: the DNxHR frames of ffmpeg's OP-Atom file, clip-wrapped again
let Some(o) = check("atom_dnxhr.mxf") else { return };
let v = &o.mxf.tracks[0];
let first = o.mxf.read_sample(&o.bytes, 0, 0).unwrap();
let mut cfg = WriterConfig::new(Pattern::OpAtom, v.edit_rate, PackageIds::from_seed("atom", "Atom"));
cfg.picture = Some(PictureDesc::new(coding_of(v.codec, &first), 320, 240));
let mut w = MxfWriter::new(std::io::Cursor::new(Vec::new()), cfg).unwrap();
for i in 0..v.samples.len() {
w.push_picture(&o.mxf.read_sample(&o.bytes, 0, i).unwrap(), FrameInfo::intra()).unwrap();
}
let out = common::fixtures_dir("mxf").join("rewritten_atom_dnxhr.mxf");
std::fs::write(&out, w.finish().unwrap().into_inner()).unwrap();
let Some(streams) = common::streams(&out) else { return };
assert_eq!((streams.len(), &streams[0]["codec_name"]), (1, &Value::from("dnxhd")));
let Some(all) = common::packets(&out) else { return };
assert_eq!(all.len(), v.samples.len());
assert_eq!(decode_md5(&o.path, "v:0"), decode_md5(&out, "v:0"));
// sound: 24-bit samples, read by ffmpeg bit for bit
let samples: Vec<i32> = (0..48_000).map(|k| ((k * 7919) % 16_777_216) - 8_388_608).collect();
for (label, edit_rate) in [("samples", None), ("frames", Some(Rational::new(25, 1)))] {
let opts = OpAtomPcm { sample_rate: 48_000, bits: 24, channels: 1, edit_rate, ids: PackageIds::from_seed(label, "A1"), timecode: None };
let out = common::fixtures_dir("mxf").join(format!("written_atom_pcm_{label}.mxf"));
std::fs::write(&out, write_opatom_pcm(&opts, &samples).unwrap()).unwrap();
let Some(pcm) = ffmpeg_pcm(&out, 1) else { return };
let want: Vec<f32> = samples.iter().map(|s| *s as f32 / 8_388_608.0).collect();
assert!(pcm[0] == want, "{label}: ffmpeg decodes other samples");
let Some(streams) = common::streams(&out) else { return };
assert_eq!((&streams[0]["codec_name"], &streams[0]["sample_rate"]), (&Value::from("pcm_s24le"), &Value::from("48000")));
}
}
/// Every fixture, truncated and damaged: opening and reading never panics.
#[test]
fn damaged_ffmpeg_files_never_panic() {
for name in ALL {
let Some(path) = fixture(name) else { return };
let bytes = std::fs::read(&path).unwrap();
let mut x = 0x2545_F491u32;
let mut next = move || {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
x as usize
};
// damage where the structure is: the header and the footer, not the essence in between
let edges = 16_384.min(bytes.len() / 2);
for round in 0..60 {
let mut d = bytes.clone();
if round % 4 == 0 {
d.truncate(next() % d.len());
}
for _ in 0..1 + round % 12 {
let at = if next() % 2 == 0 { next() % edges } else { d.len().saturating_sub(1 + next() % edges) };
if let Some(b) = d.get_mut(at) {
*b = [0, 0xFF, 0x80, next() as u8][next() % 4];
}
}
let Ok(m) = open(&d) else { continue };
for (ti, t) in m.tracks.iter().enumerate() {
let _ = (t.codec_params(), t.sync_before(t.samples.len()), m.timecode.map(|t| t.format()));
let n = t.samples.len();
for i in (0..n).step_by((n / 8).max(1)).chain(n.checked_sub(1)) {
let _ = m.read_sample(&d, ti, i);
}
let _ = m.read_pcm(&d, ti, 0, 4800);
let _ = m.read_pcm(&d, ti, t.stored_sample_frames().saturating_sub(100), 4800);
}
}
}
}