#[cfg(not(target_arch = "wasm32"))]
pub mod audio_mca;
pub mod codes;
#[cfg(not(target_arch = "wasm32"))]
pub mod essence;
#[cfg(not(target_arch = "wasm32"))]
pub mod metadata;
#[cfg(not(target_arch = "wasm32"))]
pub mod timed_text;
use std::io::Read;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub struct SampleRate {
pub numerator: i64,
pub denominator: i64,
}
#[derive(Debug, Error)]
pub enum MxfParseError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Not a valid MXF file: invalid header partition pack key")]
NotMxf,
#[error("KLV parse error at byte offset {offset}: {message}")]
KlvError { offset: u64, message: String },
#[error("Header partition pack missing or too short (got {got} bytes, need ≥ {need})")]
PartitionPackTooShort { got: usize, need: usize },
#[error("Header partition pack body too large (got {got} bytes, parser cap is {cap})")]
PartitionPackTooLarge { got: usize, cap: usize },
}
type Result<T> = std::result::Result<T, MxfParseError>;
#[derive(Debug, Clone)]
pub struct MxfHeaderInfo {
pub version: (u16, u16),
pub operational_pattern: String,
pub essence_containers: Vec<String>,
pub descriptor: Option<MxfDescriptor>,
}
#[derive(Debug, Clone)]
pub enum MxfDescriptor {
Video(MxfVideoDescriptor),
Audio(MxfAudioDescriptor),
TimedText(MxfTimedTextDescriptor),
}
#[derive(Debug, Clone)]
pub struct MxfVideoDescriptor {
pub stored_width: u32,
pub stored_height: u32,
pub sample_rate: SampleRate,
pub picture_compression_ul: Option<String>,
pub color_primaries_ul: Option<String>,
pub transfer_characteristic_ul: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MxfAudioDescriptor {
pub sample_rate: SampleRate,
pub channel_count: u32,
pub quantization_bits: u32,
}
#[derive(Debug, Clone)]
pub struct MxfTimedTextDescriptor {
pub namespace_uri: Option<String>,
}
pub fn parse_mxf_header_info(path: &Path) -> Result<MxfHeaderInfo> {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
parse_mxf_header_info_from_reader(&mut reader)
}
pub fn parse_mxf_header_info_from_reader<R: Read>(reader: &mut R) -> Result<MxfHeaderInfo> {
let mut key = [0u8; 16];
reader.read_exact(&mut key).map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
MxfParseError::NotMxf
} else {
MxfParseError::Io(e)
}
})?;
const MXF_PP_PREFIX: [u8; 12] = [
0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01,
];
if key[..12] != MXF_PP_PREFIX || key[12] != 0x01 {
return Err(MxfParseError::NotMxf);
}
let length = read_ber_length(reader, 16)?;
const MIN_PP_BODY: u64 = 88;
if length < MIN_PP_BODY {
return Err(MxfParseError::PartitionPackTooShort {
got: length as usize,
need: MIN_PP_BODY as usize,
});
}
const MAX_PP_BODY: u64 = 4096;
if length > MAX_PP_BODY {
return Err(MxfParseError::PartitionPackTooLarge {
got: length as usize,
cap: MAX_PP_BODY as usize,
});
}
let body_len = length as usize;
let mut body = vec![0u8; body_len];
reader.read_exact(&mut body)?;
let major_version = u16::from_be_bytes([body[0], body[1]]);
let minor_version = u16::from_be_bytes([body[2], body[3]]);
let operational_pattern = format_ul(&body[64..80]);
let mut essence_containers = Vec::new();
if body.len() >= 88 {
let count = u32::from_be_bytes([body[80], body[81], body[82], body[83]]) as usize;
let elem_size = u32::from_be_bytes([body[84], body[85], body[86], body[87]]) as usize;
if elem_size == 16 {
let mut offset = 88;
for _ in 0..count {
if offset + 16 <= body.len() {
essence_containers.push(format_ul(&body[offset..offset + 16]));
offset += 16;
} else {
break;
}
}
}
}
Ok(MxfHeaderInfo {
version: (major_version, minor_version),
operational_pattern,
essence_containers,
descriptor: None,
})
}
fn read_ber_length<R: Read>(reader: &mut R, key_offset: u64) -> Result<u64> {
let mut first = [0u8; 1];
reader.read_exact(&mut first)?;
let first = first[0];
if first < 0x80 {
return Ok(first as u64);
}
if first == 0x80 {
return Err(MxfParseError::KlvError {
offset: key_offset + 16,
message: "Indefinite BER length not supported in partition packs".to_string(),
});
}
let num_bytes = (first & 0x7F) as usize;
if num_bytes > 8 {
return Err(MxfParseError::KlvError {
offset: key_offset + 16,
message: format!("BER length too wide: {num_bytes} bytes"),
});
}
let mut buf = [0u8; 8];
reader.read_exact(&mut buf[8 - num_bytes..])?;
Ok(u64::from_be_bytes(buf))
}
fn format_ul(bytes: &[u8]) -> String {
if bytes.len() < 16 {
return format!("(invalid-ul:{}-bytes)", bytes.len());
}
format!(
"urn:smpte:ul:{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.\
{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}",
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes[4],
bytes[5],
bytes[6],
bytes[7],
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15],
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn make_minimal_mxf_stream(op_ul: [u8; 16]) -> Vec<u8> {
let mut stream = Vec::new();
stream.extend_from_slice(&[
0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02,
0x04, 0x00,
]);
stream.push(88);
stream.extend_from_slice(&[0x00, 0x01]);
stream.extend_from_slice(&[0x00, 0x03]);
stream.extend_from_slice(&[0x00, 0x00, 0x02, 0x00]);
stream.extend_from_slice(&[0u8; 8]);
stream.extend_from_slice(&[0u8; 8]);
stream.extend_from_slice(&[0u8; 8]);
stream.extend_from_slice(&[0u8; 8]);
stream.extend_from_slice(&[0u8; 8]);
stream.extend_from_slice(&[0u8; 4]);
stream.extend_from_slice(&[0u8; 8]);
stream.extend_from_slice(&[0u8; 4]);
stream.extend_from_slice(&op_ul);
stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x10]);
assert_eq!(stream.len(), 16 + 1 + 88);
stream
}
#[test]
fn valid_header_partition_pack_parsed() {
let op1a: [u8; 16] = [
0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x02, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01,
0x09, 0x00,
];
let stream = make_minimal_mxf_stream(op1a);
let mut cursor = Cursor::new(stream);
let info = parse_mxf_header_info_from_reader(&mut cursor).unwrap();
assert_eq!(info.version, (1, 3));
assert_eq!(
info.operational_pattern,
"urn:smpte:ul:060e2b34.04010102.0d010201.01010900"
);
assert!(info.essence_containers.is_empty());
assert!(info.descriptor.is_none());
}
#[test]
fn non_mxf_data_rejected() {
let data = vec![0u8; 105];
let mut cursor = Cursor::new(data);
assert!(matches!(
parse_mxf_header_info_from_reader(&mut cursor),
Err(MxfParseError::NotMxf)
));
}
#[test]
fn body_partition_pack_rejected() {
let mut key = vec![
0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x02, 0x02,
0x04, 0x00, ];
key.extend_from_slice(&[0u8; 89]);
let mut cursor = Cursor::new(key);
assert!(matches!(
parse_mxf_header_info_from_reader(&mut cursor),
Err(MxfParseError::NotMxf)
));
}
#[test]
fn oversized_partition_pack_returns_too_large() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&[
0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01,
0x09, 0x00,
]);
bytes.extend_from_slice(&[0x84, 0x00, 0x00, 0x13, 0x88]);
bytes.extend(std::iter::repeat_n(0u8, 5000));
let mut cursor = Cursor::new(bytes);
assert!(
matches!(
parse_mxf_header_info_from_reader(&mut cursor),
Err(MxfParseError::PartitionPackTooLarge {
got: 5000,
cap: 4096
})
),
"expected PartitionPackTooLarge {{ got: 5000, cap: 4096 }}"
);
}
#[test]
fn essence_containers_parsed() {
let op: [u8; 16] = [
0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x02, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x01,
0x09, 0x00,
];
let ec: [u8; 16] = [
0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, 0x0D, 0x01, 0x03, 0x01, 0x02, 0x0C,
0x01, 0x00,
];
let mut stream = Vec::new();
stream.extend_from_slice(&[
0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02,
0x04, 0x00,
]);
stream.push(104);
stream.extend_from_slice(&[0x00, 0x01]); stream.extend_from_slice(&[0x00, 0x03]); stream.extend_from_slice(&[0x00, 0x00, 0x02, 0x00]); stream.extend_from_slice(&[0u8; 8 * 5 + 4 + 8 + 4]); stream.extend_from_slice(&op); stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); stream.extend_from_slice(&[0x00, 0x00, 0x00, 0x10]); stream.extend_from_slice(&ec);
let mut cursor = Cursor::new(stream);
let info = parse_mxf_header_info_from_reader(&mut cursor).unwrap();
assert_eq!(info.essence_containers.len(), 1);
assert_eq!(
info.essence_containers[0],
"urn:smpte:ul:060e2b34.0401010d.0d010301.020c0100"
);
}
#[test]
#[ignore = "requires test-data MXF files (large)"]
fn real_meridian_mxf_parses() {
let path = std::path::Path::new(
"../../test-data/MERIDIAN_Netflix_Photon_161006/MERIDIAN_Netflix_Photon_161006_00.mxf",
);
if !path.exists() {
return; }
let info = parse_mxf_header_info(path).unwrap();
assert!(!info.operational_pattern.is_empty());
println!("OP: {}", info.operational_pattern);
for ec in &info.essence_containers {
println!("EC: {ec}");
}
}
}