mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
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:
Generated
+1
@@ -1067,6 +1067,7 @@ dependencies = [
|
||||
name = "craft-media"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-media-testkit",
|
||||
"proptest",
|
||||
"serde",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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][..]]);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user