use std::io::Cursor;
use oxideav_core::{Demuxer, ReadSeek};
use oxideav_mkv::ebml::{write_element_id, write_vint};
use oxideav_mkv::ids;
fn elem_uint(id: u32, value: u64) -> Vec<u8> {
let n = if value == 0 {
1
} else {
(64 - value.leading_zeros()).div_ceil(8) as usize
};
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(n as u64, 0));
for i in (0..n).rev() {
out.push(((value >> (i * 8)) & 0xFF) as u8);
}
out
}
fn elem_str(id: u32, s: &str) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(s.len() as u64, 0));
out.extend_from_slice(s.as_bytes());
out
}
fn elem_float_be_f64(id: u32, value: f64) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(8, 0));
out.extend_from_slice(&value.to_be_bytes());
out
}
fn elem_master(id: u32, body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(body.len() as u64, 0));
out.extend_from_slice(body);
out
}
fn elem_bin(id: u32, data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(data.len() as u64, 0));
out.extend_from_slice(data);
out
}
fn simple_block(track: u8, tc_offset: i16, keyframe: bool, payload: u8) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&write_vint(track as u64, 0));
body.extend_from_slice(&tc_offset.to_be_bytes());
body.push(if keyframe { 0x80 } else { 0x00 });
body.push(payload);
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(ids::SIMPLE_BLOCK));
out.extend_from_slice(&write_vint(body.len() as u64, 0));
out.extend_from_slice(&body);
out
}
fn chapter_atom(uid: u64, start_ns: u64, end_ns: Option<u64>, title: &str) -> Vec<u8> {
let mut atom = Vec::new();
atom.extend_from_slice(&elem_uint(ids::CHAPTER_UID, uid));
atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, start_ns));
if let Some(e) = end_ns {
atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_END, e));
}
let mut display = Vec::new();
display.extend_from_slice(&elem_str(ids::CHAP_STRING, title));
display.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE, "eng"));
atom.extend_from_slice(&elem_master(ids::CHAPTER_DISPLAY, &display));
elem_master(ids::CHAPTER_ATOM, &atom)
}
fn build_mkv_with_chapters() -> Vec<u8> {
let mut ebml_body = Vec::new();
ebml_body.extend_from_slice(&elem_uint(ids::EBML_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_READ_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_ID_LENGTH, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_SIZE_LENGTH, 8));
ebml_body.extend_from_slice(&elem_str(ids::EBML_DOC_TYPE, "matroska"));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_VERSION, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_READ_VERSION, 2));
let ebml_header = elem_master(ids::EBML_HEADER, &ebml_body);
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 3000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let track_entry = elem_master(ids::TRACK_ENTRY, &track_body);
let tracks = elem_master(ids::TRACKS, &track_entry);
let mut edition_body = Vec::new();
edition_body.extend_from_slice(&chapter_atom(0xC1, 0, Some(1_000_000_000), "Intro"));
edition_body.extend_from_slice(&chapter_atom(
0xC2,
1_000_000_000,
Some(2_000_000_000),
"Verse",
));
edition_body.extend_from_slice(&chapter_atom(0xC3, 2_000_000_000, None, "Outro"));
let edition = elem_master(ids::EDITION_ENTRY, &edition_body);
let chapters = elem_master(ids::CHAPTERS, &edition);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&chapters);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut out = Vec::new();
out.extend_from_slice(&ebml_header);
out.extend_from_slice(&segment);
out
}
#[test]
fn chapters_surface_in_metadata() {
let bytes = build_mkv_with_chapters();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open(rs, &oxideav_core::NullCodecResolver).expect("demux open");
let md: Vec<(String, String)> = dmx.metadata().to_vec();
let get = |k: &str| md.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(get("chapter:1:title").as_deref(), Some("Intro"));
assert_eq!(get("chapter:1:start_ms").as_deref(), Some("0"));
assert_eq!(get("chapter:1:end_ms").as_deref(), Some("1000"));
assert_eq!(get("chapter:2:title").as_deref(), Some("Verse"));
assert_eq!(get("chapter:2:start_ms").as_deref(), Some("1000"));
assert_eq!(get("chapter:2:end_ms").as_deref(), Some("2000"));
assert_eq!(get("chapter:3:title").as_deref(), Some("Outro"));
assert_eq!(get("chapter:3:start_ms").as_deref(), Some("2000"));
assert!(
get("chapter:3:end_ms").is_none(),
"atom without ChapterTimeEnd should not emit end_ms"
);
}
#[test]
fn typed_chapters_match_flat_metadata() {
let bytes = build_mkv_with_chapters();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver)
.expect("demux open_typed");
let eds = dmx.chapters();
assert_eq!(eds.len(), 1, "exactly one EditionEntry");
let ed = &eds[0];
assert!(!ed.default);
assert!(!ed.ordered);
assert!(ed.uid.is_none(), "no EditionUID was written");
assert_eq!(ed.chapters.len(), 3, "three top-level atoms");
let c1 = &ed.chapters[0];
assert_eq!(c1.index, 1);
assert_eq!(c1.uid, Some(0xC1));
assert_eq!(c1.time_start_ns, 0);
assert_eq!(c1.time_end_ns, Some(1_000_000_000));
assert!(!c1.hidden);
assert!(c1.string_uid.is_none());
assert!(c1.children.is_empty());
assert!(c1.enabled);
assert!(c1.segment_uuid.is_none());
assert!(c1.segment_edition_uid.is_none());
assert!(c1.physical_equiv.is_none());
assert_eq!(c1.displays.len(), 1);
assert_eq!(c1.displays[0].string, "Intro");
assert_eq!(c1.displays[0].language, "eng");
assert!(c1.displays[0].language_bcp47.is_none());
assert!(c1.displays[0].country.is_none());
let c3 = &ed.chapters[2];
assert_eq!(c3.index, 3);
assert_eq!(c3.time_end_ns, None);
assert_eq!(c3.displays[0].string, "Outro");
}
fn build_ebml_header() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&elem_uint(ids::EBML_VERSION, 1));
body.extend_from_slice(&elem_uint(ids::EBML_READ_VERSION, 1));
body.extend_from_slice(&elem_uint(ids::EBML_MAX_ID_LENGTH, 4));
body.extend_from_slice(&elem_uint(ids::EBML_MAX_SIZE_LENGTH, 8));
body.extend_from_slice(&elem_str(ids::EBML_DOC_TYPE, "matroska"));
body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_VERSION, 4));
body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_READ_VERSION, 2));
elem_master(ids::EBML_HEADER, &body)
}
fn build_min_segment_skeleton() -> Vec<u8> {
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 3000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let track_entry = elem_master(ids::TRACK_ENTRY, &track_body);
let tracks = elem_master(ids::TRACKS, &track_entry);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut out = Vec::new();
out.extend_from_slice(&info);
out.extend_from_slice(&tracks);
out.extend_from_slice(&cluster);
out
}
#[test]
fn typed_chapters_capture_multilingual_displays_and_nesting() {
let ebml_header = build_ebml_header();
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 3000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let tracks = elem_master(ids::TRACKS, &elem_master(ids::TRACK_ENTRY, &track_body));
let mut child_display = Vec::new();
child_display.extend_from_slice(&elem_str(ids::CHAP_STRING, "Sous-chapitre"));
child_display.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE, "fre"));
child_display.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE_BCP47, "fr-CA"));
let mut child_atom = Vec::new();
child_atom.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0x10));
child_atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 500_000_000));
child_atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_END, 800_000_000));
child_atom.extend_from_slice(&elem_master(ids::CHAPTER_DISPLAY, &child_display));
let child_atom = elem_master(ids::CHAPTER_ATOM, &child_atom);
let mut display_eng = Vec::new();
display_eng.extend_from_slice(&elem_str(ids::CHAP_STRING, "Intro"));
display_eng.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE, "eng"));
let mut display_fra = Vec::new();
display_fra.extend_from_slice(&elem_str(ids::CHAP_STRING, "Introduction"));
display_fra.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE, "fre"));
display_fra.extend_from_slice(&elem_str(ids::CHAP_COUNTRY, "FR"));
let mut parent_atom = Vec::new();
parent_atom.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0xAA));
parent_atom.extend_from_slice(&elem_str(ids::CHAPTER_STRING_UID, "cue-intro-1"));
parent_atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 0));
parent_atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_END, 1_000_000_000));
parent_atom.extend_from_slice(&elem_uint(ids::CHAPTER_FLAG_HIDDEN, 1));
parent_atom.extend_from_slice(&elem_master(ids::CHAPTER_DISPLAY, &display_eng));
parent_atom.extend_from_slice(&elem_master(ids::CHAPTER_DISPLAY, &display_fra));
parent_atom.extend_from_slice(&child_atom);
let parent_atom = elem_master(ids::CHAPTER_ATOM, &parent_atom);
let mut edition_body = Vec::new();
edition_body.extend_from_slice(&elem_uint(ids::EDITION_UID, 0xBEEF));
edition_body.extend_from_slice(&elem_uint(ids::EDITION_FLAG_DEFAULT, 1));
edition_body.extend_from_slice(&elem_uint(ids::EDITION_FLAG_ORDERED, 1));
edition_body.extend_from_slice(&parent_atom);
let chapters = elem_master(
ids::CHAPTERS,
&elem_master(ids::EDITION_ENTRY, &edition_body),
);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&chapters);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver)
.expect("demux open_typed");
let eds = dmx.chapters();
assert_eq!(eds.len(), 1);
let ed = &eds[0];
assert_eq!(ed.uid, Some(0xBEEF));
assert!(ed.default);
assert!(ed.ordered);
assert_eq!(ed.chapters.len(), 1, "one top-level atom (the parent)");
let parent = &ed.chapters[0];
assert_eq!(parent.index, 1, "depth-first: parent gets index 1");
assert_eq!(parent.uid, Some(0xAA));
assert_eq!(parent.string_uid.as_deref(), Some("cue-intro-1"));
assert_eq!(parent.time_start_ns, 0);
assert_eq!(parent.time_end_ns, Some(1_000_000_000));
assert!(parent.hidden);
assert_eq!(parent.displays.len(), 2, "eng + fra");
let eng = &parent.displays[0];
assert_eq!(eng.string, "Intro");
assert_eq!(eng.language, "eng");
assert!(eng.language_bcp47.is_none());
assert!(eng.country.is_none());
let fra = &parent.displays[1];
assert_eq!(fra.string, "Introduction");
assert_eq!(fra.language, "fre");
assert!(fra.language_bcp47.is_none());
assert_eq!(fra.country.as_deref(), Some("FR"));
assert_eq!(parent.children.len(), 1);
let child = &parent.children[0];
assert_eq!(child.index, 2, "depth-first: child gets index 2");
assert_eq!(child.uid, Some(0x10));
assert_eq!(child.time_start_ns, 500_000_000);
assert_eq!(child.time_end_ns, Some(800_000_000));
assert!(!child.hidden);
assert_eq!(child.displays.len(), 1);
assert_eq!(child.displays[0].string, "Sous-chapitre");
assert_eq!(child.displays[0].language, "fre");
assert_eq!(child.displays[0].language_bcp47.as_deref(), Some("fr-CA"));
let md: Vec<(String, String)> = dmx.metadata().to_vec();
let get = |k: &str| md.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(get("chapter:1:start_ms").as_deref(), Some("0"));
assert_eq!(get("chapter:1:end_ms").as_deref(), Some("1000"));
assert_eq!(get("chapter:1:title").as_deref(), Some("Intro"));
assert_eq!(get("chapter:2:start_ms").as_deref(), Some("500"));
assert_eq!(get("chapter:2:end_ms").as_deref(), Some("800"));
assert_eq!(get("chapter:2:title").as_deref(), Some("Sous-chapitre"));
}
#[test]
fn typed_chapters_capture_enabled_flag_segment_link_and_physical_equiv() {
let ebml_header = build_ebml_header();
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 3000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let tracks = elem_master(ids::TRACKS, &elem_master(ids::TRACK_ENTRY, &track_body));
let linked_uuid: [u8; 16] = [
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x0F, 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43,
0x21,
];
let mut linked_atom = Vec::new();
linked_atom.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0xA1));
linked_atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 0));
linked_atom.extend_from_slice(&elem_uint(ids::CHAPTER_FLAG_ENABLED, 0));
let mut uuid_elem = Vec::new();
uuid_elem.extend_from_slice(&write_element_id(ids::CHAPTER_SEGMENT_UUID));
uuid_elem.extend_from_slice(&write_vint(linked_uuid.len() as u64, 0));
uuid_elem.extend_from_slice(&linked_uuid);
linked_atom.extend_from_slice(&uuid_elem);
linked_atom.extend_from_slice(&elem_uint(ids::CHAPTER_SEGMENT_EDITION_UID, 0xED));
linked_atom.extend_from_slice(&elem_uint(ids::CHAPTER_PHYSICAL_EQUIV, 60));
let linked_atom = elem_master(ids::CHAPTER_ATOM, &linked_atom);
let mut vanilla_display = Vec::new();
vanilla_display.extend_from_slice(&elem_str(ids::CHAP_STRING, "Plain"));
vanilla_display.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE, "eng"));
let mut vanilla_atom = Vec::new();
vanilla_atom.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0xA2));
vanilla_atom.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 1_000_000_000));
vanilla_atom.extend_from_slice(&elem_master(ids::CHAPTER_DISPLAY, &vanilla_display));
let vanilla_atom = elem_master(ids::CHAPTER_ATOM, &vanilla_atom);
let mut edition_body = Vec::new();
edition_body.extend_from_slice(&linked_atom);
edition_body.extend_from_slice(&vanilla_atom);
let chapters = elem_master(
ids::CHAPTERS,
&elem_master(ids::EDITION_ENTRY, &edition_body),
);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&chapters);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver)
.expect("demux open_typed");
let eds = dmx.chapters();
assert_eq!(eds.len(), 1);
let ed = &eds[0];
assert_eq!(ed.chapters.len(), 2);
let linked = &ed.chapters[0];
assert_eq!(linked.uid, Some(0xA1));
assert!(!linked.enabled, "ChapterFlagEnabled = 0 must surface");
assert_eq!(
linked.segment_uuid.as_deref(),
Some(&linked_uuid[..]),
"16-byte ChapterSegmentUUID preserved verbatim"
);
assert_eq!(linked.segment_edition_uid, Some(0xED));
assert_eq!(linked.physical_equiv, Some(60));
assert!(linked.displays.is_empty());
let vanilla = &ed.chapters[1];
assert_eq!(vanilla.uid, Some(0xA2));
assert!(
vanilla.enabled,
"ChapterFlagEnabled spec default = 1 must materialise as true"
);
assert!(vanilla.segment_uuid.is_none());
assert!(vanilla.segment_edition_uid.is_none());
assert!(vanilla.physical_equiv.is_none());
assert_eq!(vanilla.displays.len(), 1);
assert_eq!(vanilla.displays[0].string, "Plain");
}
#[test]
fn typed_chapters_drop_segment_edition_uid_zero() {
let ebml_header = build_ebml_header();
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 3000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let tracks = elem_master(ids::TRACKS, &elem_master(ids::TRACK_ENTRY, &track_body));
let mut atom_body = Vec::new();
atom_body.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0xB1));
atom_body.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 0));
atom_body.extend_from_slice(&elem_uint(ids::CHAPTER_SEGMENT_EDITION_UID, 0));
let atom = elem_master(ids::CHAPTER_ATOM, &atom_body);
let chapters = elem_master(ids::CHAPTERS, &elem_master(ids::EDITION_ENTRY, &atom));
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&chapters);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver)
.expect("demux open_typed");
let eds = dmx.chapters();
assert_eq!(eds.len(), 1);
let ed = &eds[0];
assert_eq!(ed.chapters.len(), 1);
assert!(
ed.chapters[0].segment_edition_uid.is_none(),
"ChapterSegmentEditionUID=0 must surface as None (spec range: not 0)"
);
}
#[test]
fn typed_chapters_empty_when_no_chapters_element() {
let ebml_header = build_ebml_header();
let segment = elem_master(ids::SEGMENT, &build_min_segment_skeleton());
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver)
.expect("demux open_typed");
assert!(
dmx.chapters().is_empty(),
"file with no Chapters element should expose no editions"
);
}
#[test]
fn typed_chapters_capture_chap_process_subtree() {
let ebml_header = build_ebml_header();
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 3000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let tracks = elem_master(ids::TRACKS, &elem_master(ids::TRACK_ENTRY, &track_body));
let cmd1_during = {
let mut b = Vec::new();
b.extend_from_slice(&elem_uint(
ids::CHAP_PROCESS_TIME,
ids::CHAP_PROCESS_TIME_DURING,
));
b.extend_from_slice(&elem_bin(ids::CHAP_PROCESS_DATA, &[0xDE, 0xAD, 0xBE, 0xEF]));
elem_master(ids::CHAP_PROCESS_COMMAND, &b)
};
let cmd1_after = {
let mut b = Vec::new();
b.extend_from_slice(&elem_uint(
ids::CHAP_PROCESS_TIME,
ids::CHAP_PROCESS_TIME_AFTER,
));
b.extend_from_slice(&elem_bin(ids::CHAP_PROCESS_DATA, &[0x01, 0x02, 0x03]));
elem_master(ids::CHAP_PROCESS_COMMAND, &b)
};
let chap_process1 = {
let mut b = Vec::new();
b.extend_from_slice(&elem_uint(
ids::CHAP_PROCESS_CODEC_ID,
ids::CHAP_PROCESS_CODEC_DVD_MENU,
));
b.extend_from_slice(&elem_bin(ids::CHAP_PROCESS_PRIVATE, &[0xAA, 0xBB]));
b.extend_from_slice(&cmd1_during);
b.extend_from_slice(&cmd1_after);
elem_master(ids::CHAP_PROCESS, &b)
};
let atom1 = {
let mut a = Vec::new();
a.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0xA1));
a.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 0));
a.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_END, 1_000_000_000));
let mut disp = Vec::new();
disp.extend_from_slice(&elem_str(ids::CHAP_STRING, "Menu"));
disp.extend_from_slice(&elem_str(ids::CHAP_LANGUAGE, "eng"));
a.extend_from_slice(&elem_master(ids::CHAPTER_DISPLAY, &disp));
a.extend_from_slice(&chap_process1);
elem_master(ids::CHAPTER_ATOM, &a)
};
let cmd2 = {
let mut b = Vec::new();
b.extend_from_slice(&elem_bin(ids::CHAP_PROCESS_DATA, &[0xFF]));
elem_master(ids::CHAP_PROCESS_COMMAND, &b)
};
let chap_process2 = {
elem_master(ids::CHAP_PROCESS, &cmd2)
};
let atom2 = {
let mut a = Vec::new();
a.extend_from_slice(&elem_uint(ids::CHAPTER_UID, 0xA2));
a.extend_from_slice(&elem_uint(ids::CHAPTER_TIME_START, 1_000_000_000));
a.extend_from_slice(&chap_process2);
elem_master(ids::CHAPTER_ATOM, &a)
};
let mut edition_body = Vec::new();
edition_body.extend_from_slice(&atom1);
edition_body.extend_from_slice(&atom2);
let chapters = elem_master(
ids::CHAPTERS,
&elem_master(ids::EDITION_ENTRY, &edition_body),
);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&chapters);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver)
.expect("demux open_typed");
let eds = dmx.chapters();
assert_eq!(eds.len(), 1);
let ed = &eds[0];
assert_eq!(ed.chapters.len(), 2, "two top-level atoms");
let c1 = &ed.chapters[0];
assert_eq!(c1.uid, Some(0xA1));
assert_eq!(c1.chap_processes.len(), 1, "one ChapProcess on atom 1");
let p1 = &c1.chap_processes[0];
assert_eq!(p1.codec_id, ids::CHAP_PROCESS_CODEC_DVD_MENU);
assert_eq!(p1.private.as_deref(), Some(&[0xAA, 0xBB][..]));
assert_eq!(p1.commands.len(), 2, "two ChapProcessCommands");
assert_eq!(p1.commands[0].time, ids::CHAP_PROCESS_TIME_DURING);
assert_eq!(p1.commands[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
assert_eq!(p1.commands[1].time, ids::CHAP_PROCESS_TIME_AFTER);
assert_eq!(p1.commands[1].data, vec![0x01, 0x02, 0x03]);
let c2 = &ed.chapters[1];
assert_eq!(c2.uid, Some(0xA2));
assert_eq!(c2.chap_processes.len(), 1, "one ChapProcess on atom 2");
let p2 = &c2.chap_processes[0];
assert_eq!(p2.codec_id, ids::CHAP_PROCESS_CODEC_MATROSKA_SCRIPT);
assert!(p2.private.is_none(), "no ChapProcessPrivate written");
assert_eq!(p2.commands.len(), 1);
assert_eq!(p2.commands[0].time, ids::CHAP_PROCESS_TIME_DURING);
assert_eq!(p2.commands[0].data, vec![0xFF]);
}