#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MxfTrackType {
Video,
Audio,
Data,
}
impl std::fmt::Display for MxfTrackType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MxfTrackType::Video => write!(f, "Video"),
MxfTrackType::Audio => write!(f, "Audio"),
MxfTrackType::Data => write!(f, "Data"),
}
}
}
#[derive(Debug, Clone)]
pub struct MxfEssenceTrack {
pub track_type: MxfTrackType,
pub codec_label: [u8; 16],
}
#[derive(Debug, Clone)]
pub struct MxfInfo {
pub operational_pattern: String,
pub essence_tracks: Vec<MxfEssenceTrack>,
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MxfProbeError {
NotMxf,
TruncatedData,
ParseError(String),
}
impl std::fmt::Display for MxfProbeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MxfProbeError::NotMxf => write!(f, "not an MXF file"),
MxfProbeError::TruncatedData => write!(f, "truncated MXF data"),
MxfProbeError::ParseError(msg) => write!(f, "MXF parse error: {msg}"),
}
}
}
impl std::error::Error for MxfProbeError {}
const MXF_PARTITION_KEY_PREFIX: [u8; 12] = [
0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01,
];
const MIN_MXF_SIZE: usize = 17;
const OP_KEY_PREFIX: [u8; 7] = [0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01];
const ESSENCE_KEY_PREFIX: [u8; 7] = [0x06, 0x0E, 0x2B, 0x34, 0x01, 0x01, 0x01];
const MAX_TRACKS: usize = 8;
pub struct MxfProber;
impl MxfProber {
pub fn probe(data: &[u8]) -> Result<MxfInfo, MxfProbeError> {
if data.len() < MIN_MXF_SIZE {
return Err(MxfProbeError::TruncatedData);
}
if !Self::is_mxf_header(data) {
return Err(MxfProbeError::NotMxf);
}
let operational_pattern = Self::parse_operational_pattern(data);
let essence_tracks = Self::extract_essence_tracks(data);
let duration_ms = Self::extract_duration_ms(data);
Ok(MxfInfo {
operational_pattern,
essence_tracks,
duration_ms,
})
}
fn is_mxf_header(data: &[u8]) -> bool {
if data.len() < 16 {
return false;
}
if data[..12] != MXF_PARTITION_KEY_PREFIX {
return false;
}
let kind = data[13];
matches!(kind, 0x01 | 0x03 | 0x04)
}
fn parse_operational_pattern(data: &[u8]) -> String {
let prefix = &OP_KEY_PREFIX;
let search_end = data.len().saturating_sub(prefix.len() + 2);
for i in 0..search_end {
if &data[i..i + 7] == prefix.as_slice() {
let discriminator = data[i + 7];
let op = match discriminator {
0x01 => "OP1a",
0x02 => "OP1b",
0x03 => "OP1c",
0x04 => "OP2a",
0x05 => "OP2b",
0x06 => "OP2c",
0x07 => "OP3a",
0x08 => "OP3b",
0x09 => "OP3c",
0x10 => "OPAtom",
_ => continue, };
return op.to_owned();
}
}
"Unknown".to_owned()
}
fn extract_essence_tracks(data: &[u8]) -> Vec<MxfEssenceTrack> {
let mut tracks = Vec::new();
let prefix = &ESSENCE_KEY_PREFIX;
let search_end = data.len().saturating_sub(16);
let mut i = 0usize;
while i < search_end && tracks.len() < MAX_TRACKS {
if &data[i..i + 7] != prefix.as_slice() {
i += 1;
continue;
}
if i + 16 > data.len() {
break;
}
let mut codec_label = [0u8; 16];
codec_label.copy_from_slice(&data[i..i + 16]);
let essence_type_byte = codec_label[12];
let track_type = match essence_type_byte {
0x01 => MxfTrackType::Video,
0x02 => MxfTrackType::Audio,
_ => MxfTrackType::Data,
};
let already_seen = tracks
.iter()
.any(|t: &MxfEssenceTrack| t.codec_label == codec_label);
if !already_seen {
tracks.push(MxfEssenceTrack {
track_type,
codec_label,
});
}
i += 16; }
tracks
}
fn extract_duration_ms(data: &[u8]) -> Option<u64> {
let offsets: &[usize] = &[72, 80, 88];
for &offset in offsets {
if offset + 8 > data.len() {
continue;
}
let mut buf = [0u8; 8];
buf.copy_from_slice(&data[offset..offset + 8]);
let frame_count = u64::from_be_bytes(buf);
if frame_count > 0 && frame_count <= 900_000 {
return Some(frame_count * 40);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_mxf_header(partition_kind: u8) -> Vec<u8> {
let mut buf = vec![0u8; 64];
buf[..12].copy_from_slice(&MXF_PARTITION_KEY_PREFIX);
buf[12] = 0x01; buf[13] = partition_kind;
buf[14] = 0x01; buf[15] = 0x01; buf[16] = 0x04; buf
}
#[test]
fn test_is_mxf_header_valid_header_partition() {
let buf = minimal_mxf_header(0x01);
assert!(MxfProber::is_mxf_header(&buf));
}
#[test]
fn test_is_mxf_header_valid_body_partition() {
let buf = minimal_mxf_header(0x03);
assert!(MxfProber::is_mxf_header(&buf));
}
#[test]
fn test_is_mxf_header_valid_footer_partition() {
let buf = minimal_mxf_header(0x04);
assert!(MxfProber::is_mxf_header(&buf));
}
#[test]
fn test_is_mxf_header_invalid_partition_kind() {
let mut buf = minimal_mxf_header(0xFF);
buf[13] = 0xFF; assert!(!MxfProber::is_mxf_header(&buf));
}
#[test]
fn test_is_mxf_header_wrong_magic() {
let mut buf = vec![0u8; 64];
buf[0] = 0xDE;
buf[1] = 0xAD;
buf[2] = 0xBE;
buf[3] = 0xEF;
assert!(!MxfProber::is_mxf_header(&buf));
}
#[test]
fn test_is_mxf_header_too_short() {
let buf = [0x06u8, 0x0E, 0x2B, 0x34];
assert!(!MxfProber::is_mxf_header(&buf));
}
#[test]
fn test_probe_empty_returns_truncated() {
let result = MxfProber::probe(&[]);
assert!(matches!(result, Err(MxfProbeError::TruncatedData)));
}
#[test]
fn test_probe_too_short_returns_truncated() {
let buf = vec![0u8; MIN_MXF_SIZE - 1];
let result = MxfProber::probe(&buf);
assert!(matches!(result, Err(MxfProbeError::TruncatedData)));
}
#[test]
fn test_probe_not_mxf_returns_not_mxf() {
let buf = vec![0xFFu8; 64];
let result = MxfProber::probe(&buf);
assert!(matches!(result, Err(MxfProbeError::NotMxf)));
}
#[test]
fn test_probe_jpeg_magic_returns_not_mxf() {
let mut buf = vec![0u8; 64];
buf[0] = 0xFF;
buf[1] = 0xD8;
let result = MxfProber::probe(&buf);
assert!(matches!(result, Err(MxfProbeError::NotMxf)));
}
#[test]
fn test_probe_valid_header_partition_succeeds() {
let buf = minimal_mxf_header(0x01);
let result = MxfProber::probe(&buf);
assert!(result.is_ok(), "expected Ok, got {:?}", result);
}
#[test]
fn test_probe_returns_unknown_op_when_no_op_label() {
let buf = minimal_mxf_header(0x01);
let info = MxfProber::probe(&buf).expect("probe should succeed");
assert_eq!(info.operational_pattern, "Unknown");
}
#[test]
fn test_probe_detects_op1a() {
let mut buf = minimal_mxf_header(0x01);
buf.resize(128, 0);
buf[32..39].copy_from_slice(&OP_KEY_PREFIX);
buf[39] = 0x01; let info = MxfProber::probe(&buf).expect("probe should succeed");
assert_eq!(info.operational_pattern, "OP1a");
}
#[test]
fn test_probe_detects_op3c() {
let mut buf = minimal_mxf_header(0x01);
buf.resize(128, 0);
buf[40..47].copy_from_slice(&OP_KEY_PREFIX);
buf[47] = 0x09; let info = MxfProber::probe(&buf).expect("probe should succeed");
assert_eq!(info.operational_pattern, "OP3c");
}
#[test]
fn test_probe_detects_essence_video_track() {
let mut buf = minimal_mxf_header(0x01);
buf.resize(200, 0);
buf[80..87].copy_from_slice(&ESSENCE_KEY_PREFIX);
buf[80 + 12] = 0x01; let info = MxfProber::probe(&buf).expect("probe should succeed");
let has_video = info
.essence_tracks
.iter()
.any(|t| t.track_type == MxfTrackType::Video);
assert!(
has_video,
"expected a Video track, got {:?}",
info.essence_tracks
);
}
#[test]
fn test_probe_detects_essence_audio_track() {
let mut buf = minimal_mxf_header(0x01);
buf.resize(200, 0);
buf[80..87].copy_from_slice(&ESSENCE_KEY_PREFIX);
buf[80 + 12] = 0x02; let info = MxfProber::probe(&buf).expect("probe should succeed");
let has_audio = info
.essence_tracks
.iter()
.any(|t| t.track_type == MxfTrackType::Audio);
assert!(
has_audio,
"expected an Audio track, got {:?}",
info.essence_tracks
);
}
#[test]
fn test_probe_no_essence_tracks_when_none_present() {
let buf = minimal_mxf_header(0x01);
let info = MxfProber::probe(&buf).expect("probe should succeed");
assert!(
info.essence_tracks.is_empty(),
"expected no tracks, got {:?}",
info.essence_tracks
);
}
#[test]
fn test_probe_duration_none_for_minimal_buffer() {
let buf = minimal_mxf_header(0x01);
let info = MxfProber::probe(&buf).expect("probe should succeed");
assert!(info.duration_ms.is_none());
}
#[test]
fn test_mxf_track_type_display() {
assert_eq!(MxfTrackType::Video.to_string(), "Video");
assert_eq!(MxfTrackType::Audio.to_string(), "Audio");
assert_eq!(MxfTrackType::Data.to_string(), "Data");
}
#[test]
fn test_mxf_probe_error_display() {
assert!(MxfProbeError::NotMxf.to_string().contains("MXF"));
assert!(MxfProbeError::TruncatedData
.to_string()
.contains("truncated"));
let pe = MxfProbeError::ParseError("bad field".to_owned());
assert!(pe.to_string().contains("bad field"));
}
}