Skip to main content

imferno_core/mxf/
mod.rs

1//! SMPTE ST 377-1: Material Exchange Format (MXF) header parser.
2//!
3//! Reads the header partition pack from an MXF file and extracts:
4//! - Operational Pattern UL (OP1a, OP1b, etc.)
5//! - Essence Container ULs (codec container labels)
6//!
7//! Scope: partition-pack level only. Full header metadata set parsing
8//! (Preface, MaterialPackage, essence descriptors) is out of scope for
9//! this phase — CPL EssenceDescriptors are the primary source of format info.
10
11pub mod codes;
12
13use std::io::Read;
14use std::path::Path;
15use thiserror::Error;
16
17/// A rational number representing a sample rate (numerator/denominator).
18///
19/// Used for `SampleRate` fields in MXF essence descriptors (ST 377-1).
20/// Distinct from `st2067_3::EditRate` — same representation, different domain.
21#[derive(Debug, Clone, PartialEq)]
22pub struct SampleRate {
23    pub numerator: i64,
24    pub denominator: i64,
25}
26
27// ─── Error ────────────────────────────────────────────────────────────────────
28
29#[derive(Debug, Error)]
30pub enum MxfParseError {
31    #[error("IO error: {0}")]
32    Io(#[from] std::io::Error),
33    #[error("Not a valid MXF file: invalid header partition pack key")]
34    NotMxf,
35    #[error("KLV parse error at byte offset {offset}: {message}")]
36    KlvError { offset: u64, message: String },
37    #[error("Header partition pack missing or too short (got {got} bytes, need ≥ {need})")]
38    PartitionPackTooShort { got: usize, need: usize },
39}
40
41type Result<T> = std::result::Result<T, MxfParseError>;
42
43// ─── Public types ─────────────────────────────────────────────────────────────
44
45/// Header-level information extracted from an MXF file.
46///
47/// Populated by parsing the Header Partition Pack KLV triplet only —
48/// no header metadata sets are parsed.
49#[derive(Debug, Clone)]
50pub struct MxfHeaderInfo {
51    /// MXF format version (major, minor) from the partition pack.
52    pub version: (u16, u16),
53    /// Operational Pattern UL as a `urn:smpte:ul:` string.
54    ///
55    /// Common values: `OP1a` = `urn:smpte:ul:060e2b34.04010102.0d010201.01010900`
56    pub operational_pattern: String,
57    /// Essence Container ULs from the partition pack's EssenceContainers batch.
58    pub essence_containers: Vec<String>,
59    /// Descriptor extracted from header metadata (currently always `None`).
60    pub descriptor: Option<MxfDescriptor>,
61}
62
63/// Essence descriptor information from MXF header metadata.
64///
65/// Populated only if header metadata parsing is implemented. Currently always
66/// `None` — CPL EssenceDescriptors are the source of truth.
67#[derive(Debug, Clone)]
68pub enum MxfDescriptor {
69    Video(MxfVideoDescriptor),
70    Audio(MxfAudioDescriptor),
71    TimedText(MxfTimedTextDescriptor),
72}
73
74/// Video essence descriptor from MXF header metadata.
75#[derive(Debug, Clone)]
76pub struct MxfVideoDescriptor {
77    pub stored_width: u32,
78    pub stored_height: u32,
79    pub sample_rate: SampleRate,
80    /// Raw PictureCompression UL string — pass to `VideoCodec::from_ul`.
81    pub picture_compression_ul: Option<String>,
82    /// Raw ColorPrimaries UL string — pass to `ColorPrimaries::from_ul`.
83    pub color_primaries_ul: Option<String>,
84    /// Raw TransferCharacteristic UL string — pass to `TransferCharacteristic::from_ul`.
85    pub transfer_characteristic_ul: Option<String>,
86}
87
88/// Audio essence descriptor from MXF header metadata.
89#[derive(Debug, Clone)]
90pub struct MxfAudioDescriptor {
91    pub sample_rate: SampleRate,
92    pub channel_count: u32,
93    pub quantization_bits: u32,
94}
95
96/// Timed text (subtitle/caption) descriptor from MXF header metadata.
97#[derive(Debug, Clone)]
98pub struct MxfTimedTextDescriptor {
99    pub namespace_uri: Option<String>,
100}
101
102// ─── Parser ───────────────────────────────────────────────────────────────────
103
104/// Parse header-level information from an MXF file on disk.
105pub fn parse_mxf_header_info(path: &Path) -> Result<MxfHeaderInfo> {
106    let file = std::fs::File::open(path)?;
107    let mut reader = std::io::BufReader::new(file);
108    parse_mxf_header_info_from_reader(&mut reader)
109}
110
111/// Parse header-level information from an MXF byte stream.
112///
113/// Reads only the Header Partition Pack KLV triplet. Does not seek.
114pub fn parse_mxf_header_info_from_reader<R: Read>(reader: &mut R) -> Result<MxfHeaderInfo> {
115    // ── Step 1: Read KLV key (16 bytes) ──────────────────────────────────────
116    let mut key = [0u8; 16];
117    reader.read_exact(&mut key).map_err(|e| {
118        if e.kind() == std::io::ErrorKind::UnexpectedEof {
119            MxfParseError::NotMxf
120        } else {
121            MxfParseError::Io(e)
122        }
123    })?;
124
125    // Verify it is an MXF Header Partition Pack key.
126    // SMPTE ST 377-1 §7.1 — all partition pack keys share the same 12-byte prefix:
127    // 06 0E 2B 34 02 05 01 01 0D 01 02 01
128    // Byte 12 = 01 (header), 02 (body), 03 (footer)
129    // We only accept header partition packs.
130    const MXF_PP_PREFIX: [u8; 12] = [
131        0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01,
132    ];
133    if key[..12] != MXF_PP_PREFIX || key[12] != 0x01 {
134        return Err(MxfParseError::NotMxf);
135    }
136
137    // ── Step 2: BER-decode the length ─────────────────────────────────────────
138    let length = read_ber_length(reader, 16)?;
139
140    // Minimum valid partition pack body is 88 bytes (0 essence containers).
141    const MIN_PP_BODY: u64 = 88;
142    if length < MIN_PP_BODY {
143        return Err(MxfParseError::PartitionPackTooShort {
144            got: length as usize,
145            need: MIN_PP_BODY as usize,
146        });
147    }
148
149    // ── Step 3: Read partition pack body ─────────────────────────────────────
150    let body_len = length.min(4096) as usize; // cap to avoid absurd allocations
151    let mut body = vec![0u8; body_len];
152    reader.read_exact(&mut body)?;
153
154    // ── Step 4: Parse the fixed fields ───────────────────────────────────────
155    // SMPTE ST 377-1:2011, Table 13 — Partition Pack value layout (all big-endian)
156    // Offset  0  MajorVersion       UInt16
157    // Offset  2  MinorVersion       UInt16
158    // Offset  4  KAGSize            UInt32
159    // Offset  8  ThisPartition      UInt64
160    // Offset 16  PreviousPartition  UInt64
161    // Offset 24  FooterPartition    UInt64
162    // Offset 32  HeaderByteCount    UInt64
163    // Offset 40  IndexByteCount     UInt64
164    // Offset 48  IndexSID           UInt32
165    // Offset 52  BodyOffset         UInt64
166    // Offset 60  BodySID            UInt32
167    // Offset 64  OperationalPattern UL[16]
168    // Offset 80  EssenceContainers  batch(count:u32, size:u32, UL[16]...)
169
170    let major_version = u16::from_be_bytes([body[0], body[1]]);
171    let minor_version = u16::from_be_bytes([body[2], body[3]]);
172
173    // OperationalPattern is at offset 64 in the partition pack value.
174    let operational_pattern = format_ul(&body[64..80]);
175
176    // ── Step 5: Parse EssenceContainers batch at offset 80 ───────────────────
177    let mut essence_containers = Vec::new();
178    if body.len() >= 88 {
179        // Batch header: 4-byte count + 4-byte element size
180        let count = u32::from_be_bytes([body[80], body[81], body[82], body[83]]) as usize;
181        let elem_size = u32::from_be_bytes([body[84], body[85], body[86], body[87]]) as usize;
182
183        if elem_size == 16 {
184            let mut offset = 88;
185            for _ in 0..count {
186                if offset + 16 <= body.len() {
187                    essence_containers.push(format_ul(&body[offset..offset + 16]));
188                    offset += 16;
189                } else {
190                    break;
191                }
192            }
193        }
194    }
195
196    Ok(MxfHeaderInfo {
197        version: (major_version, minor_version),
198        operational_pattern,
199        essence_containers,
200        descriptor: None,
201    })
202}
203
204// ─── Helpers ─────────────────────────────────────────────────────────────────
205
206/// Read a BER-encoded length from `reader`.
207/// `key_offset` is used for error messages (byte offset of the key start).
208fn read_ber_length<R: Read>(reader: &mut R, key_offset: u64) -> Result<u64> {
209    let mut first = [0u8; 1];
210    reader.read_exact(&mut first)?;
211    let first = first[0];
212
213    if first < 0x80 {
214        return Ok(first as u64);
215    }
216
217    if first == 0x80 {
218        return Err(MxfParseError::KlvError {
219            offset: key_offset + 16,
220            message: "Indefinite BER length not supported in partition packs".to_string(),
221        });
222    }
223
224    let num_bytes = (first & 0x7F) as usize;
225    if num_bytes > 8 {
226        return Err(MxfParseError::KlvError {
227            offset: key_offset + 16,
228            message: format!("BER length too wide: {num_bytes} bytes"),
229        });
230    }
231
232    let mut buf = [0u8; 8];
233    reader.read_exact(&mut buf[8 - num_bytes..])?;
234    Ok(u64::from_be_bytes(buf))
235}
236
237/// Format 16 raw UL bytes as `urn:smpte:ul:xxxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx`.
238fn format_ul(bytes: &[u8]) -> String {
239    if bytes.len() < 16 {
240        return format!("(invalid-ul:{}-bytes)", bytes.len());
241    }
242    format!(
243        "urn:smpte:ul:{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.\
244         {:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}",
245        bytes[0],
246        bytes[1],
247        bytes[2],
248        bytes[3],
249        bytes[4],
250        bytes[5],
251        bytes[6],
252        bytes[7],
253        bytes[8],
254        bytes[9],
255        bytes[10],
256        bytes[11],
257        bytes[12],
258        bytes[13],
259        bytes[14],
260        bytes[15],
261    )
262}
263
264// ─── Tests ────────────────────────────────────────────────────────────────────
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use std::io::Cursor;
270
271    /// Helper: build a minimal valid MXF header partition pack byte stream.
272    /// Key (16) + BER length (1) + partition pack body (88).
273    fn make_minimal_mxf_stream(op_ul: [u8; 16]) -> Vec<u8> {
274        let mut stream = Vec::new();
275
276        // Key: Header Partition Pack (Closed and Complete = 01 02 04 00)
277        stream.extend_from_slice(&[
278            0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02,
279            0x04, 0x00,
280        ]);
281        // BER length = 88 (fits in 1 byte)
282        stream.push(88);
283
284        // Partition pack body (88 bytes):
285        // MajorVersion = 1
286        stream.extend_from_slice(&[0x00, 0x01]);
287        // MinorVersion = 3
288        stream.extend_from_slice(&[0x00, 0x03]);
289        // KAGSize = 512
290        stream.extend_from_slice(&[0x00, 0x00, 0x02, 0x00]);
291        // ThisPartition = 0
292        stream.extend_from_slice(&[0u8; 8]);
293        // PreviousPartition = 0
294        stream.extend_from_slice(&[0u8; 8]);
295        // FooterPartition = 0
296        stream.extend_from_slice(&[0u8; 8]);
297        // HeaderByteCount = 0
298        stream.extend_from_slice(&[0u8; 8]);
299        // IndexByteCount = 0
300        stream.extend_from_slice(&[0u8; 8]);
301        // IndexSID = 0
302        stream.extend_from_slice(&[0u8; 4]);
303        // BodyOffset = 0
304        stream.extend_from_slice(&[0u8; 8]);
305        // BodySID = 0
306        stream.extend_from_slice(&[0u8; 4]);
307        // OperationalPattern UL
308        stream.extend_from_slice(&op_ul);
309        // EssenceContainers batch: count=0, element_size=16
310        stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // count
311        stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x10]); // element_size
312
313        assert_eq!(stream.len(), 16 + 1 + 88);
314        stream
315    }
316
317    /// SMPTE ST 377-1 §7.1: a valid MXF file starts with a Header Partition Pack key.
318    #[test]
319    fn valid_header_partition_pack_parsed() {
320        // OP1a UL: 060E2B34.04010102.0D010201.01010900
321        let op1a: [u8; 16] = [
322            0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x02, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01,
323            0x09, 0x00,
324        ];
325        let stream = make_minimal_mxf_stream(op1a);
326        let mut cursor = Cursor::new(stream);
327        let info = parse_mxf_header_info_from_reader(&mut cursor).unwrap();
328
329        assert_eq!(info.version, (1, 3));
330        assert_eq!(
331            info.operational_pattern,
332            "urn:smpte:ul:060e2b34.04010102.0d010201.01010900"
333        );
334        assert!(info.essence_containers.is_empty());
335        assert!(info.descriptor.is_none());
336    }
337
338    /// SMPTE ST 377-1 §7.1: non-MXF files must be rejected.
339    #[test]
340    fn non_mxf_data_rejected() {
341        let data = vec![0u8; 105];
342        let mut cursor = Cursor::new(data);
343        assert!(matches!(
344            parse_mxf_header_info_from_reader(&mut cursor),
345            Err(MxfParseError::NotMxf)
346        ));
347    }
348
349    /// Body-type partition pack key (key[12] = 0x02) must be rejected — we
350    /// only accept header partition packs.
351    #[test]
352    fn body_partition_pack_rejected() {
353        let mut key = vec![
354            0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x02, 0x02,
355            0x04, 0x00, // key[12] = 0x02 = body
356        ];
357        key.extend_from_slice(&[0u8; 89]);
358        let mut cursor = Cursor::new(key);
359        assert!(matches!(
360            parse_mxf_header_info_from_reader(&mut cursor),
361            Err(MxfParseError::NotMxf)
362        ));
363    }
364
365    /// SMPTE ST 377-1 §7.1: EssenceContainers batch is correctly parsed.
366    #[test]
367    fn essence_containers_parsed() {
368        let op: [u8; 16] = [
369            0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x02, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01,
370            0x09, 0x00,
371        ];
372        // JPEG 2000 Frame-wrapped container UL
373        let ec: [u8; 16] = [
374            0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, 0x0D, 0x01, 0x03, 0x01, 0x02, 0x0C,
375            0x01, 0x00,
376        ];
377
378        let mut stream = Vec::new();
379        // Key: Header Partition Pack (Closed and Complete)
380        stream.extend_from_slice(&[
381            0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02,
382            0x04, 0x00,
383        ]);
384        // BER length = 88 + 16 = 104 (one essence container)
385        stream.push(104);
386
387        // Fixed fields (80 bytes): versions + padding to OP
388        stream.extend_from_slice(&[0x00, 0x01]); // MajorVersion = 1
389        stream.extend_from_slice(&[0x00, 0x03]); // MinorVersion = 3
390        stream.extend_from_slice(&[0x00, 0x00, 0x02, 0x00]); // KAGSize
391        stream.extend_from_slice(&[0u8; 8 * 5 + 4 + 8 + 4]); // padding to OP offset
392        stream.extend_from_slice(&op); // OperationalPattern at offset 68
393                                       // EssenceContainers batch: count=1, element_size=16, then 1 UL
394        stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); // count
395        stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x10]); // element_size
396        stream.extend_from_slice(&ec);
397
398        let mut cursor = Cursor::new(stream);
399        let info = parse_mxf_header_info_from_reader(&mut cursor).unwrap();
400
401        assert_eq!(info.essence_containers.len(), 1);
402        assert_eq!(
403            info.essence_containers[0],
404            "urn:smpte:ul:060e2b34.0401010d.0d010301.020c0100"
405        );
406    }
407
408    /// Real MXF files from the test corpus parse without error.
409    #[test]
410    #[ignore = "requires test-data MXF files (large)"]
411    fn real_meridian_mxf_parses() {
412        let path = std::path::Path::new(
413            "../../test-data/MERIDIAN_Netflix_Photon_161006/MERIDIAN_Netflix_Photon_161006_00.mxf",
414        );
415        if !path.exists() {
416            return; // skip if test data not present
417        }
418        let info = parse_mxf_header_info(path).unwrap();
419        assert!(!info.operational_pattern.is_empty());
420        println!("OP: {}", info.operational_pattern);
421        for ec in &info.essence_containers {
422            println!("EC: {ec}");
423        }
424    }
425}