use super::ebml;
use super::timeline::TimelineContinuity;
use crate::disc::{
AudioChannels, AudioStream, Chapter, Codec, ColorSpace, HdrFormat, Resolution, SampleRate,
SubtitleStream, VideoStream,
};
use std::io::{self, Seek, Write};
const CICP_PRIMARIES_BT709: u8 = 1;
const CICP_PRIMARIES_BT470BG: u8 = 5;
const CICP_PRIMARIES_BT601_525: u8 = 6;
const CICP_PRIMARIES_BT2020: u8 = 9;
const CICP_PRIMARIES_UNSPECIFIED: u8 = 2;
const CICP_TRANSFER_BT709: u8 = 1;
const CICP_TRANSFER_BT470BG: u8 = 5;
const CICP_TRANSFER_BT601_525: u8 = 6;
const CICP_TRANSFER_PQ: u8 = 16;
const CICP_TRANSFER_HLG: u8 = 18;
const CICP_TRANSFER_UNSPECIFIED: u8 = 2;
const CICP_MATRIX_BT709: u8 = 1;
const CICP_MATRIX_BT470BG: u8 = 5;
const CICP_MATRIX_BT601_525: u8 = 6;
const CICP_MATRIX_BT2020NC: u8 = 9;
const CICP_MATRIX_UNSPECIFIED: u8 = 2;
const COLOUR_RANGE_LIMITED: u8 = 1;
const BLOCK_ADD_ID_TYPE_DVCC: u64 = 0x6476_6343;
pub(crate) fn cicp_for_video(v: &VideoStream) -> (u8, u8, u8, u8) {
if let Some(c) = v.measured_cicp {
return (c.matrix, c.transfer, c.primaries, c.range);
}
let (m, t, p, r) = match v.color_space {
ColorSpace::Bt2020 => (
CICP_MATRIX_BT2020NC,
CICP_TRANSFER_PQ,
CICP_PRIMARIES_BT2020,
COLOUR_RANGE_LIMITED,
),
ColorSpace::Bt709 => (
CICP_MATRIX_BT709,
CICP_TRANSFER_BT709,
CICP_PRIMARIES_BT709,
COLOUR_RANGE_LIMITED,
),
ColorSpace::Bt470bg => (
CICP_MATRIX_BT470BG,
CICP_TRANSFER_BT470BG,
CICP_PRIMARIES_BT470BG,
COLOUR_RANGE_LIMITED,
),
ColorSpace::Smpte170m => (
CICP_MATRIX_BT601_525,
CICP_TRANSFER_BT601_525,
CICP_PRIMARIES_BT601_525,
COLOUR_RANGE_LIMITED,
),
ColorSpace::Unknown => (
CICP_MATRIX_UNSPECIFIED,
CICP_TRANSFER_UNSPECIFIED,
CICP_PRIMARIES_UNSPECIFIED,
COLOUR_RANGE_LIMITED,
),
};
let t = match v.hdr {
HdrFormat::Hdr10 | HdrFormat::Hdr10Plus | HdrFormat::DolbyVision => CICP_TRANSFER_PQ,
HdrFormat::Hlg => CICP_TRANSFER_HLG,
_ => t,
};
(m, t, p, r)
}
pub struct MkvTrack {
pub track_type: u64, pub codec_id: &'static str,
pub language: String,
pub name: String, pub codec_private: Option<Vec<u8>>,
pub is_default: bool,
pub is_forced: bool,
pub pixel_width: u32,
pub pixel_height: u32,
pub default_duration_ns: u64, pub display_width: u32, pub display_height: u32, pub colour_matrix: u8, pub colour_transfer: u8, pub colour_primaries: u8, pub colour_range: u8, pub interlaced: bool,
pub field_order: u8,
pub field_duration_ns: u64,
pub sample_rate: f64,
pub channels: u8,
pub bit_depth: u8,
pub dv_config: Option<Vec<u8>>,
pub hdr10: Option<crate::mux::codec::Hdr10Metadata>,
}
pub fn dolby_vision_config(profile: u8, level: u8, bl_compat_id: u8) -> Vec<u8> {
let mut v = vec![0u8; 24];
v[0] = 1; v[1] = 0; v[2] = ((profile & 0x7F) << 1) | ((level >> 5) & 0x01);
v[3] = ((level & 0x1F) << 3) | (1 << 2) | (1 << 1) | 1; v[4] = (bl_compat_id & 0x0F) << 4;
v
}
const HDR10_CHROMATICITY_UNIT: f64 = 0.00002;
const HDR10_LUMINANCE_UNIT: f64 = 0.0001;
fn write_hdr10<W: Write + Seek>(w: &mut W, h: &crate::mux::codec::Hdr10Metadata) -> io::Result<()> {
let chroma = |v: u16| -> f64 { v as f64 * HDR10_CHROMATICITY_UNIT };
let lum = |v: u32| -> f64 { v as f64 * HDR10_LUMINANCE_UNIT };
let mm_pos = ebml::start_master(w, ebml::MASTERING_METADATA)?;
ebml::write_float(
w,
ebml::PRIMARY_R_CHROMATICITY_X,
chroma(h.display_primaries_x[2]),
)?;
ebml::write_float(
w,
ebml::PRIMARY_R_CHROMATICITY_Y,
chroma(h.display_primaries_y[2]),
)?;
ebml::write_float(
w,
ebml::PRIMARY_G_CHROMATICITY_X,
chroma(h.display_primaries_x[0]),
)?;
ebml::write_float(
w,
ebml::PRIMARY_G_CHROMATICITY_Y,
chroma(h.display_primaries_y[0]),
)?;
ebml::write_float(
w,
ebml::PRIMARY_B_CHROMATICITY_X,
chroma(h.display_primaries_x[1]),
)?;
ebml::write_float(
w,
ebml::PRIMARY_B_CHROMATICITY_Y,
chroma(h.display_primaries_y[1]),
)?;
ebml::write_float(w, ebml::WHITE_POINT_CHROMATICITY_X, chroma(h.white_point_x))?;
ebml::write_float(w, ebml::WHITE_POINT_CHROMATICITY_Y, chroma(h.white_point_y))?;
ebml::write_float(
w,
ebml::LUMINANCE_MAX,
lum(h.max_display_mastering_luminance),
)?;
ebml::write_float(
w,
ebml::LUMINANCE_MIN,
lum(h.min_display_mastering_luminance),
)?;
ebml::end_master(w, mm_pos)?;
ebml::write_uint(w, ebml::MAX_CLL, h.max_content_light_level as u64)?;
ebml::write_uint(w, ebml::MAX_FALL, h.max_pic_average_light_level as u64)?;
Ok(())
}
impl MkvTrack {
pub fn video(v: &VideoStream) -> Self {
let codec_id = match v.codec {
Codec::H264 => ebml::CODEC_H264,
Codec::Hevc => ebml::CODEC_HEVC,
Codec::Vc1 => ebml::CODEC_VC1,
Codec::Mpeg2 => ebml::CODEC_MPEG2,
_ => ebml::CODEC_MPEG2,
};
let (w, h) = if matches!(v.resolution, Resolution::Unknown) {
(0, 0)
} else {
v.resolution.pixels()
};
let (num, den) = v.frame_rate.as_fraction();
let default_duration_ns = if num > 0 {
(1_000_000_000u64 * den as u64) / num as u64
} else {
0
};
let (matrix, transfer, primaries, range) = cicp_for_video(v);
let (display_width, display_height) = match v.display_aspect {
Some((an, ad)) if an > 0 && ad > 0 && h > 0 => ((h * an + ad / 2) / ad, h),
_ => (w, h),
};
Self {
track_type: ebml::TRACK_TYPE_VIDEO,
codec_id,
language: "und".into(),
name: v.label.clone(),
codec_private: None,
is_default: !v.secondary,
is_forced: false,
pixel_width: w,
pixel_height: h,
default_duration_ns,
display_width,
display_height,
colour_matrix: matrix,
colour_transfer: transfer,
colour_primaries: primaries,
colour_range: range,
interlaced: v.resolution.is_interlaced(),
field_order: ebml::FIELD_ORDER_UNDETERMINED,
field_duration_ns: 0,
sample_rate: 0.0,
channels: 0,
bit_depth: 0,
dv_config: if matches!(v.hdr, HdrFormat::DolbyVision) {
Some(dolby_vision_config(7, 6, 0))
} else {
None
},
hdr10: None,
}
}
pub fn audio(a: &AudioStream) -> Self {
let codec_id = match a.codec {
Codec::Ac3 => ebml::CODEC_AC3,
Codec::Ac3Plus => ebml::CODEC_EAC3,
Codec::TrueHd => ebml::CODEC_TRUEHD,
Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => ebml::CODEC_DTS,
Codec::Lpcm => ebml::CODEC_PCM_BE,
_ => ebml::CODEC_AC3,
};
let sr = if matches!(a.sample_rate, SampleRate::Unknown) {
0.0
} else {
a.sample_rate.hz()
};
let ch = if matches!(a.channels, AudioChannels::Unknown) {
0
} else {
a.channels.count()
};
let name = a.label.clone();
Self {
track_type: ebml::TRACK_TYPE_AUDIO,
codec_id,
language: a.language.clone(),
name,
codec_private: None,
is_default: !a.secondary,
is_forced: false,
pixel_width: 0,
pixel_height: 0,
default_duration_ns: 0,
display_width: 0,
display_height: 0,
colour_matrix: 0,
colour_transfer: 0,
colour_primaries: 0,
colour_range: 0,
interlaced: false,
field_order: ebml::FIELD_ORDER_UNDETERMINED,
field_duration_ns: 0,
sample_rate: sr,
channels: ch,
bit_depth: 0,
dv_config: None,
hdr10: None,
}
}
pub fn subtitle(s: &SubtitleStream) -> Self {
let codec_id = match s.codec {
Codec::DvdSub => ebml::CODEC_VOBSUB,
_ => ebml::CODEC_PGS,
};
Self {
track_type: ebml::TRACK_TYPE_SUBTITLE,
codec_id,
language: s.language.clone(),
name: String::new(),
codec_private: s.codec_data.clone(),
is_default: false,
is_forced: s.forced,
pixel_width: 0,
pixel_height: 0,
default_duration_ns: 0,
display_width: 0,
display_height: 0,
colour_matrix: 0,
colour_transfer: 0,
colour_primaries: 0,
colour_range: 0,
interlaced: false,
field_order: ebml::FIELD_ORDER_UNDETERMINED,
field_duration_ns: 0,
sample_rate: 0.0,
channels: 0,
bit_depth: 0,
dv_config: None,
hdr10: None,
}
}
}
struct CuePoint {
timestamp_ticks: i64, track: usize,
cluster_pos: u64, }
struct SeekPositionFixup {
target_id: u32,
value_offset: u64, }
pub struct MkvMuxer<W: Write + Seek> {
writer: W,
segment_start: u64,
cluster_open: bool,
cluster_pos: u64,
cluster_size_pos: u64,
cluster_ts_ticks: i64,
base_pts_ticks: Option<i64>,
last_pts_ticks: std::collections::HashMap<usize, i64>,
track_is_video: Vec<bool>,
primary_video_track: Option<usize>,
continuity: TimelineContinuity,
cues: Vec<CuePoint>,
frame_count: u64,
dropped_pre_cluster: u64,
seek_fixups: Vec<SeekPositionFixup>,
cues_seek_entry_pos: Option<u64>,
info_offset: u64,
tracks_offset: u64,
chapters_offset: Option<u64>,
track_bytes: Vec<u64>,
track_uids: Vec<u64>,
duration_secs: f64,
ac3_channel_fixups: std::collections::HashMap<usize, Ac3ChannelFixup>,
opening_capture: Option<crate::diag::OpeningCapture>,
}
struct Ac3ChannelFixup {
value_offset: u64,
claimed: u8,
corrected: bool,
}
const TIMESTAMP_SCALE_NS: i64 = 100_000;
const CLUSTER_DURATION_TICKS: i64 = 2_000 * 1_000_000 / TIMESTAMP_SCALE_NS;
const MAX_BLOCK_REL: i64 = i16::MAX as i64;
const MIN_BLOCK_REL: i64 = i16::MIN as i64;
fn monotonic_ts(prev: Option<i64>, pts_ticks: i64) -> i64 {
match prev {
Some(p) => pts_ticks.max(p.saturating_add(1)),
None => pts_ticks,
}
}
fn block_ts(is_video: bool, prev: Option<i64>, pts_ticks: i64) -> i64 {
if is_video {
pts_ticks
} else {
monotonic_ts(prev, pts_ticks)
}
}
fn track_vint(track_num: usize) -> ([u8; 2], usize) {
if track_num < 0x80 {
([(track_num as u8) | 0x80, 0], 1)
} else {
debug_assert!(
track_num < 0x4000,
"track number {track_num} exceeds the 14-bit 2-byte EBML VINT range"
);
([0x40 | ((track_num >> 8) as u8), track_num as u8], 2)
}
}
impl<W: Write + Seek> MkvMuxer<W> {
pub fn new(
mut writer: W,
tracks: &[MkvTrack],
title: Option<&str>,
duration_secs: f64,
chapters: &[Chapter],
) -> io::Result<Self> {
let ebml_pos = ebml::start_master(&mut writer, ebml::EBML)?;
ebml::write_uint(&mut writer, ebml::EBML_VERSION, 1)?;
ebml::write_uint(&mut writer, ebml::EBML_READ_VERSION, 1)?;
ebml::write_uint(&mut writer, ebml::EBML_MAX_ID_LENGTH, 4)?;
ebml::write_uint(&mut writer, ebml::EBML_MAX_SIZE_LENGTH, 8)?;
ebml::write_string(&mut writer, ebml::EBML_DOC_TYPE, "matroska")?;
ebml::write_uint(&mut writer, ebml::EBML_DOC_TYPE_VERSION, 4)?;
ebml::write_uint(&mut writer, ebml::EBML_DOC_TYPE_READ_VERSION, 2)?;
ebml::end_master(&mut writer, ebml_pos)?;
ebml::write_id(&mut writer, ebml::SEGMENT)?;
ebml::write_unknown_size(&mut writer)?;
let segment_start = writer.stream_position()?;
let mut seek_fixups: Vec<SeekPositionFixup> = Vec::new();
let seekhead_pos = ebml::start_master(&mut writer, ebml::SEEK_HEAD)?;
let mut targets: Vec<u32> = vec![ebml::INFO, ebml::TRACKS];
if !chapters.is_empty() {
targets.push(ebml::CHAPTERS);
}
targets.push(ebml::CUES);
let seek_id_be = (ebml::SEEK as u16).to_be_bytes();
let seek_inner_id_be = (ebml::SEEK_ID as u16).to_be_bytes();
let seek_pos_id_be = (ebml::SEEK_POSITION as u16).to_be_bytes();
let mut cues_seek_entry_pos: Option<u64> = None;
for target_id in &targets {
let entry_pos = writer.stream_position()?;
if *target_id == ebml::CUES {
cues_seek_entry_pos = Some(entry_pos);
}
writer.write_all(&[seek_id_be[0], seek_id_be[1], 0x92])?;
writer.write_all(&[seek_inner_id_be[0], seek_inner_id_be[1], 0x84])?;
writer.write_all(&target_id.to_be_bytes())?;
writer.write_all(&[seek_pos_id_be[0], seek_pos_id_be[1], 0x88])?;
let value_offset = writer.stream_position()?;
writer.write_all(&[0u8; 8])?;
seek_fixups.push(SeekPositionFixup {
target_id: *target_id,
value_offset,
});
}
ebml::end_master(&mut writer, seekhead_pos)?;
let info_start = writer.stream_position()?;
let info_offset = info_start - segment_start;
let info_pos = ebml::start_master(&mut writer, ebml::INFO)?;
ebml::write_uint(
&mut writer,
ebml::TIMESTAMP_SCALE,
TIMESTAMP_SCALE_NS as u64,
)?;
if duration_secs > 0.0 {
let duration_ticks = duration_secs * 1_000_000_000.0 / TIMESTAMP_SCALE_NS as f64;
ebml::write_float(&mut writer, ebml::DURATION, duration_ticks)?;
}
ebml::write_string(&mut writer, ebml::MUXING_APP, crate::MUX_APP)?;
ebml::write_string(&mut writer, ebml::WRITING_APP, crate::MUX_APP)?;
if let Some(t) = title {
ebml::write_string(&mut writer, ebml::TITLE, t)?;
}
ebml::end_master(&mut writer, info_pos)?;
let tracks_start = writer.stream_position()?;
let tracks_offset = tracks_start - segment_start;
let tracks_pos = ebml::start_master(&mut writer, ebml::TRACKS)?;
let mut track_uids: Vec<u64> = Vec::with_capacity(tracks.len());
let mut ac3_channel_fixups: std::collections::HashMap<usize, Ac3ChannelFixup> =
std::collections::HashMap::new();
for (i, track) in tracks.iter().enumerate() {
let track_uid = (i + 1) as u64 | 0x100_0000;
track_uids.push(track_uid);
let entry_pos = ebml::start_master(&mut writer, ebml::TRACK_ENTRY)?;
ebml::write_uint(&mut writer, ebml::TRACK_NUMBER, (i + 1) as u64)?;
ebml::write_uint(&mut writer, ebml::TRACK_UID, track_uid)?;
ebml::write_uint(&mut writer, ebml::TRACK_TYPE, track.track_type)?;
ebml::write_uint(&mut writer, ebml::FLAG_LACING, 0)?;
ebml::write_string(&mut writer, ebml::CODEC_ID, track.codec_id)?;
ebml::write_string(&mut writer, ebml::LANGUAGE, &track.language)?;
if !track.name.is_empty() {
ebml::write_string(&mut writer, ebml::TRACK_NAME, &track.name)?;
}
if !track.is_default {
ebml::write_uint(&mut writer, ebml::FLAG_DEFAULT, 0)?;
}
if track.is_forced {
ebml::write_uint(&mut writer, ebml::FLAG_FORCED, 1)?;
}
if let Some(ref cp) = track.codec_private {
ebml::write_binary(&mut writer, ebml::CODEC_PRIVATE, cp)?;
}
if track.default_duration_ns > 0 {
ebml::write_uint(
&mut writer,
ebml::DEFAULT_DURATION,
track.default_duration_ns,
)?;
}
if track.track_type == ebml::TRACK_TYPE_VIDEO
&& track.interlaced
&& track.field_duration_ns > 0
{
ebml::write_uint(
&mut writer,
ebml::DEFAULT_DECODED_FIELD_DURATION,
track.field_duration_ns,
)?;
}
if track.track_type == ebml::TRACK_TYPE_VIDEO && track.pixel_width > 0 {
let vid_pos = ebml::start_master(&mut writer, ebml::VIDEO)?;
ebml::write_uint(&mut writer, ebml::PIXEL_WIDTH, track.pixel_width as u64)?;
ebml::write_uint(&mut writer, ebml::PIXEL_HEIGHT, track.pixel_height as u64)?;
ebml::write_uint(
&mut writer,
ebml::FLAG_INTERLACED,
if track.interlaced {
ebml::INTERLACED_INTERLACED
} else {
ebml::INTERLACED_PROGRESSIVE
},
)?;
if track.interlaced && track.field_order != ebml::FIELD_ORDER_UNDETERMINED {
ebml::write_uint(&mut writer, ebml::FIELD_ORDER, track.field_order as u64)?;
}
if track.display_width > 0 && track.display_height > 0 {
ebml::write_uint(&mut writer, ebml::DISPLAY_WIDTH, track.display_width as u64)?;
ebml::write_uint(
&mut writer,
ebml::DISPLAY_HEIGHT,
track.display_height as u64,
)?;
}
if track.colour_matrix > 0 || track.colour_transfer > 0 || track.hdr10.is_some() {
let col_pos = ebml::start_master(&mut writer, ebml::COLOUR)?;
ebml::write_uint(
&mut writer,
ebml::MATRIX_COEFFICIENTS,
track.colour_matrix as u64,
)?;
ebml::write_uint(
&mut writer,
ebml::TRANSFER_CHARACTERISTICS,
track.colour_transfer as u64,
)?;
ebml::write_uint(&mut writer, ebml::PRIMARIES, track.colour_primaries as u64)?;
ebml::write_uint(&mut writer, ebml::RANGE, track.colour_range as u64)?;
if let Some(h) = track.hdr10 {
write_hdr10(&mut writer, &h)?;
}
ebml::end_master(&mut writer, col_pos)?;
}
ebml::end_master(&mut writer, vid_pos)?;
}
if let Some(ref dvcc) = track.dv_config {
let map_pos = ebml::start_master(&mut writer, ebml::BLOCK_ADDITION_MAPPING)?;
ebml::write_uint(&mut writer, ebml::BLOCK_ADD_ID_TYPE, BLOCK_ADD_ID_TYPE_DVCC)?;
ebml::write_binary(&mut writer, ebml::BLOCK_ADD_ID_EXTRA_DATA, dvcc)?;
ebml::end_master(&mut writer, map_pos)?;
}
if track.track_type == ebml::TRACK_TYPE_AUDIO && track.sample_rate > 0.0 {
let aud_pos = ebml::start_master(&mut writer, ebml::AUDIO)?;
ebml::write_float(&mut writer, ebml::SAMPLING_FREQUENCY, track.sample_rate)?;
if track.channels > 0 {
ebml::write_id(&mut writer, ebml::CHANNELS)?;
ebml::write_size(&mut writer, 1)?;
let value_offset = writer.stream_position()?;
writer.write_all(&[track.channels])?;
if track.codec_id == ebml::CODEC_AC3 {
ac3_channel_fixups.insert(
i,
Ac3ChannelFixup {
value_offset,
claimed: track.channels,
corrected: false,
},
);
}
}
if track.bit_depth > 0 {
ebml::write_uint(&mut writer, ebml::BIT_DEPTH, track.bit_depth as u64)?;
}
ebml::end_master(&mut writer, aud_pos)?;
}
ebml::end_master(&mut writer, entry_pos)?;
}
ebml::end_master(&mut writer, tracks_pos)?;
let mut chapters_offset: Option<u64> = None;
if !chapters.is_empty() {
let chapters_start = writer.stream_position()?;
chapters_offset = Some(chapters_start - segment_start);
let chapters_pos = ebml::start_master(&mut writer, ebml::CHAPTERS)?;
let edition_pos = ebml::start_master(&mut writer, ebml::EDITION_ENTRY)?;
for (i, ch) in chapters.iter().enumerate() {
let atom_pos = ebml::start_master(&mut writer, ebml::CHAPTER_ATOM)?;
ebml::write_uint(&mut writer, ebml::CHAPTER_UID, (i + 1) as u64)?;
let time_ns = (ch.time_secs * 1_000_000_000.0) as u64;
ebml::write_uint(&mut writer, ebml::CHAPTER_TIME_START, time_ns)?;
let display_pos = ebml::start_master(&mut writer, ebml::CHAPTER_DISPLAY)?;
ebml::write_string(&mut writer, ebml::CHAP_STRING, &ch.name)?;
ebml::write_string(&mut writer, ebml::CHAP_LANGUAGE, "und")?;
ebml::end_master(&mut writer, display_pos)?;
ebml::end_master(&mut writer, atom_pos)?;
}
ebml::end_master(&mut writer, edition_pos)?;
ebml::end_master(&mut writer, chapters_pos)?;
}
Ok(Self {
writer,
segment_start,
cluster_open: false,
cluster_pos: 0,
cluster_size_pos: 0,
cluster_ts_ticks: 0,
base_pts_ticks: None,
last_pts_ticks: std::collections::HashMap::new(),
track_is_video: tracks
.iter()
.map(|t| t.track_type == ebml::TRACK_TYPE_VIDEO)
.collect(),
primary_video_track: tracks
.iter()
.position(|t| t.track_type == ebml::TRACK_TYPE_VIDEO),
continuity: TimelineContinuity::new(),
cues: Vec::new(),
frame_count: 0,
dropped_pre_cluster: 0,
seek_fixups,
cues_seek_entry_pos,
info_offset,
tracks_offset,
chapters_offset,
track_bytes: vec![0u64; tracks.len()],
track_uids,
duration_secs,
ac3_channel_fixups,
opening_capture: None,
})
}
pub fn set_opening_capture(&mut self, capture: Option<crate::diag::OpeningCapture>) {
self.opening_capture = capture;
}
pub fn write_frame(
&mut self,
track_idx: usize,
pts_ns: i64,
keyframe: bool,
data: &[u8],
duration_ns: Option<u64>,
) -> io::Result<()> {
if let Some(cap) = self.opening_capture.as_mut() {
cap.record(track_idx, pts_ns, keyframe, data);
}
let is_video = self.track_is_video.get(track_idx).copied().unwrap_or(false);
let drives_epoch = Some(track_idx) == self.primary_video_track;
let pts_ns = self.continuity.adjust(pts_ns, drives_epoch);
let raw_ticks = pts_ns / TIMESTAMP_SCALE_NS;
let cluster_driver = self.primary_video_track.unwrap_or(0);
let is_video_key = keyframe && track_idx == cluster_driver;
let base = match self.base_pts_ticks {
Some(b) => b,
None => {
if !is_video_key {
self.dropped_pre_cluster += 1;
return Ok(());
}
self.base_pts_ticks = Some(raw_ticks);
raw_ticks
}
};
let pts_ticks = (raw_ticks - base).max(0);
let pts_ticks = block_ts(
is_video,
self.last_pts_ticks.get(&track_idx).copied(),
pts_ticks,
);
let needs_new_cluster = !self.cluster_open
|| (is_video_key && (pts_ticks - self.cluster_ts_ticks) >= CLUSTER_DURATION_TICKS);
if needs_new_cluster {
if !is_video_key {
if !self.cluster_open {
self.dropped_pre_cluster += 1;
}
return Ok(());
}
self.start_cluster(pts_ticks)?;
self.cues.push(CuePoint {
timestamp_ticks: pts_ticks,
track: track_idx + 1,
cluster_pos: self.cluster_pos - self.segment_start,
});
} else {
let rel = pts_ticks - self.cluster_ts_ticks;
if !(MIN_BLOCK_REL..=MAX_BLOCK_REL).contains(&rel) {
self.start_cluster(pts_ticks)?;
self.cues.push(CuePoint {
timestamp_ticks: pts_ticks,
track: track_idx + 1,
cluster_pos: self.cluster_pos - self.segment_start,
});
}
}
self.last_pts_ticks.insert(track_idx, pts_ticks);
let relative_ts = (pts_ticks - self.cluster_ts_ticks) as i16;
match duration_ns {
Some(dur_ns) => {
let duration_ticks = (dur_ns as i64 / TIMESTAMP_SCALE_NS).max(1) as u64;
self.write_block_group(track_idx + 1, relative_ts, keyframe, data, duration_ticks)?;
}
None => {
self.write_simple_block(track_idx + 1, relative_ts, keyframe, data)?;
}
}
self.frame_count += 1;
if let Some(b) = self.track_bytes.get_mut(track_idx) {
*b += data.len() as u64;
}
if let Some(fixup) = self.ac3_channel_fixups.get_mut(&track_idx) {
if !fixup.corrected {
match super::codec::ac3::acmod_channels(data) {
Some(actual) if actual > 0 => {
if actual != fixup.claimed {
tracing::warn!(
target: "mux",
"AC-3 track {track_idx}: IFO claimed {} channels but bitstream acmod says {}; trusting the bitstream (possible wrong-stream selection)",
fixup.claimed,
actual,
);
let here = self.writer.stream_position()?;
self.writer
.seek(std::io::SeekFrom::Start(fixup.value_offset))?;
self.writer.write_all(&[actual])?;
self.writer.seek(std::io::SeekFrom::Start(here))?;
}
fixup.corrected = true;
}
_ => {}
}
}
}
Ok(())
}
pub fn finish(mut self) -> io::Result<()> {
if self.frame_count == 0 {
return Err(crate::error::Error::MkvInvalid.into());
}
self.end_cluster()?;
let cues_start = self.writer.stream_position()?;
let cues_offset = cues_start - self.segment_start;
let have_cues = !self.cues.is_empty();
if !self.cues.is_empty() {
let cues_pos = ebml::start_master(&mut self.writer, ebml::CUES)?;
for cue in &self.cues {
let cp_pos = ebml::start_master(&mut self.writer, ebml::CUE_POINT)?;
ebml::write_uint(&mut self.writer, ebml::CUE_TIME, cue.timestamp_ticks as u64)?;
let ctp_pos = ebml::start_master(&mut self.writer, ebml::CUE_TRACK_POSITIONS)?;
ebml::write_uint(&mut self.writer, ebml::CUE_TRACK, cue.track as u64)?;
ebml::write_uint(
&mut self.writer,
ebml::CUE_CLUSTER_POSITION,
cue.cluster_pos,
)?;
ebml::end_master(&mut self.writer, ctp_pos)?;
ebml::end_master(&mut self.writer, cp_pos)?;
}
ebml::end_master(&mut self.writer, cues_pos)?;
}
self.write_bps_tags()?;
for fixup in &self.seek_fixups {
if fixup.target_id == ebml::CUES && !have_cues {
continue;
}
let offset = match fixup.target_id {
ebml::INFO => self.info_offset,
ebml::TRACKS => self.tracks_offset,
ebml::CHAPTERS => self
.chapters_offset
.expect("CHAPTERS seek fixup present => chapters_offset is Some"),
ebml::CUES => cues_offset,
_ => 0,
};
self.writer
.seek(std::io::SeekFrom::Start(fixup.value_offset))?;
self.writer.write_all(&offset.to_be_bytes())?;
}
if !have_cues {
if let Some(entry_pos) = self.cues_seek_entry_pos {
self.writer.seek(std::io::SeekFrom::Start(entry_pos))?;
ebml::write_id(&mut self.writer, ebml::VOID)?;
ebml::write_size(&mut self.writer, 19)?;
self.writer.write_all(&[0u8; 19])?;
}
}
self.writer.seek(std::io::SeekFrom::End(0))?;
self.writer.flush()?;
Ok(())
}
fn write_bps_tags(&mut self) -> io::Result<()> {
if self.duration_secs <= 0.0 {
return Ok(());
}
if self.track_bytes.iter().all(|&b| b == 0) {
return Ok(());
}
let tags_pos = ebml::start_master(&mut self.writer, ebml::TAGS)?;
let entries: Vec<(u64, u64)> = self
.track_uids
.iter()
.zip(self.track_bytes.iter())
.map(|(&uid, &bytes)| (uid, bytes))
.collect();
for (uid, bytes) in entries {
if bytes == 0 {
continue;
}
let bps = ((bytes as f64) * 8.0 / self.duration_secs).round() as u64;
let tag_pos = ebml::start_master(&mut self.writer, ebml::TAG)?;
let targets_pos = ebml::start_master(&mut self.writer, ebml::TARGETS)?;
ebml::write_uint(&mut self.writer, ebml::TAG_TRACK_UID, uid)?;
ebml::end_master(&mut self.writer, targets_pos)?;
let st_pos = ebml::start_master(&mut self.writer, ebml::SIMPLE_TAG)?;
ebml::write_string(&mut self.writer, ebml::TAG_NAME, "BPS")?;
ebml::write_string(&mut self.writer, ebml::TAG_STRING, &bps.to_string())?;
ebml::end_master(&mut self.writer, st_pos)?;
ebml::end_master(&mut self.writer, tag_pos)?;
}
ebml::end_master(&mut self.writer, tags_pos)?;
Ok(())
}
fn start_cluster(&mut self, ts_ticks: i64) -> io::Result<()> {
if self.cluster_open {
self.end_cluster()?;
}
self.cluster_pos = self.writer.stream_position()?;
self.cluster_size_pos = ebml::start_master(&mut self.writer, ebml::CLUSTER)?;
ebml::write_uint(&mut self.writer, ebml::CLUSTER_TIMESTAMP, ts_ticks as u64)?;
self.cluster_ts_ticks = ts_ticks;
self.cluster_open = true;
Ok(())
}
fn end_cluster(&mut self) -> io::Result<()> {
if self.cluster_open {
ebml::end_master(&mut self.writer, self.cluster_size_pos)?;
self.cluster_open = false;
}
Ok(())
}
fn write_simple_block(
&mut self,
track_num: usize,
relative_ts: i16,
keyframe: bool,
data: &[u8],
) -> io::Result<()> {
let (tv, tv_len) = track_vint(track_num);
let track_vint = &tv[..tv_len];
let flags: u8 = if keyframe { 0x80 } else { 0x00 };
let block_size = track_vint.len() + 2 + 1 + data.len(); ebml::write_id(&mut self.writer, ebml::SIMPLE_BLOCK)?;
ebml::write_size(&mut self.writer, block_size as u64)?;
self.writer.write_all(track_vint)?;
self.writer.write_all(&relative_ts.to_be_bytes())?;
self.writer.write_all(&[flags])?;
self.writer.write_all(data)?;
Ok(())
}
fn write_block_group(
&mut self,
track_num: usize,
relative_ts: i16,
keyframe: bool,
data: &[u8],
duration_ticks: u64,
) -> io::Result<()> {
let (tv, tv_len) = track_vint(track_num);
let track_vint = &tv[..tv_len];
let _ = keyframe;
let flags: u8 = 0x00;
let block_size = track_vint.len() + 2 + 1 + data.len();
let bg_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_GROUP)?;
ebml::write_id(&mut self.writer, ebml::BLOCK)?;
ebml::write_size(&mut self.writer, block_size as u64)?;
self.writer.write_all(track_vint)?;
self.writer.write_all(&relative_ts.to_be_bytes())?;
self.writer.write_all(&[flags])?;
self.writer.write_all(data)?;
ebml::write_uint(&mut self.writer, ebml::BLOCK_DURATION, duration_ticks)?;
ebml::end_master(&mut self.writer, bg_pos)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn video_track_anamorphic_display_aspect() {
let base = VideoStream {
pid: 0xE0,
codec: Codec::Mpeg2,
resolution: Resolution::R576i,
frame_rate: crate::disc::FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
display_aspect: Some((16, 9)),
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&base);
assert_eq!((t.pixel_width, t.pixel_height), (720, 576));
assert_eq!(
(t.display_width, t.display_height),
(1024, 576),
"16:9 anamorphic must emit a 16:9 DAR, not square-pixel 720x576"
);
let square = VideoStream {
display_aspect: None,
..base
};
let t2 = MkvTrack::video(&square);
assert_eq!(
(t2.display_width, t2.display_height),
(720, 576),
"square pixels: display == pixel"
);
}
#[test]
fn measured_cicp_overrides_color_space_enum() {
let base = VideoStream {
pid: 0xE0,
codec: Codec::Hevc,
resolution: Resolution::R2160p,
frame_rate: crate::disc::FrameRate::F24,
hdr: HdrFormat::Hdr10, color_space: ColorSpace::Bt2020,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t_enum = MkvTrack::video(&base);
assert_eq!(
(
t_enum.colour_matrix,
t_enum.colour_transfer,
t_enum.colour_primaries
),
(
CICP_MATRIX_BT2020NC,
CICP_TRANSFER_PQ,
CICP_PRIMARIES_BT2020
),
"enum fallback derives BT.2020/PQ"
);
let measured = VideoStream {
measured_cicp: Some(crate::disc::MeasuredCicp {
matrix: CICP_MATRIX_BT709,
transfer: CICP_TRANSFER_BT709,
primaries: CICP_PRIMARIES_BT709,
range: COLOUR_RANGE_LIMITED,
}),
..base
};
let t = MkvTrack::video(&measured);
assert_eq!(
(t.colour_matrix, t.colour_transfer, t.colour_primaries),
(CICP_MATRIX_BT709, CICP_TRANSFER_BT709, CICP_PRIMARIES_BT709),
"measured CICP must override the enum, transfer included"
);
}
#[test]
fn unknown_color_space_emits_unspecified_cicp() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Hevc,
resolution: Resolution::R1080p,
frame_rate: crate::disc::FrameRate::F24,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Unknown,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert_eq!(
(
t.colour_matrix,
t.colour_transfer,
t.colour_primaries,
t.colour_range
),
(
CICP_MATRIX_UNSPECIFIED,
CICP_TRANSFER_UNSPECIFIED,
CICP_PRIMARIES_UNSPECIFIED,
COLOUR_RANGE_LIMITED
),
"Unknown colorimetry must emit CICP 'unspecified' (2), not 0"
);
}
fn find_id(data: &[u8], id: u32) -> Option<usize> {
let bytes = id.to_be_bytes();
let start = if bytes[0] != 0 {
0
} else if bytes[1] != 0 {
1
} else if bytes[2] != 0 {
2
} else {
3
};
let needle = &bytes[start..];
data.windows(needle.len()).position(|w| w == needle)
}
fn make_video_track() -> MkvTrack {
MkvTrack {
track_type: ebml::TRACK_TYPE_VIDEO,
codec_id: ebml::CODEC_H264,
language: "und".into(),
name: String::new(),
codec_private: Some(vec![0x00, 0x01, 0x02, 0x03]),
is_default: true,
is_forced: false,
pixel_width: 1920,
pixel_height: 1080,
default_duration_ns: 41708333,
display_width: 1920,
display_height: 1080,
colour_matrix: 0,
colour_transfer: 0,
colour_primaries: 0,
colour_range: 0,
interlaced: false,
field_order: ebml::FIELD_ORDER_UNDETERMINED,
field_duration_ns: 0,
sample_rate: 0.0,
channels: 0,
bit_depth: 0,
dv_config: None,
hdr10: None,
}
}
fn make_audio_track() -> MkvTrack {
MkvTrack {
track_type: ebml::TRACK_TYPE_AUDIO,
codec_id: ebml::CODEC_AC3,
language: "eng".into(),
name: "English".into(),
codec_private: None,
is_default: true,
is_forced: false,
pixel_width: 0,
pixel_height: 0,
default_duration_ns: 0,
display_width: 0,
display_height: 0,
colour_matrix: 0,
colour_transfer: 0,
colour_primaries: 0,
colour_range: 0,
interlaced: false,
field_order: ebml::FIELD_ORDER_UNDETERMINED,
field_duration_ns: 0,
sample_rate: 48000.0,
channels: 6,
bit_depth: 0,
dv_config: None,
hdr10: None,
}
}
fn audio_stream(codec: Codec) -> AudioStream {
use crate::disc::{AudioChannels, LabelPurpose, SampleRate};
AudioStream {
pid: 0x1100,
codec,
channels: AudioChannels::Surround51,
language: "eng".into(),
sample_rate: SampleRate::S48,
secondary: false,
purpose: LabelPurpose::Normal,
label: String::new(),
}
}
#[test]
fn dts_variants_map_to_registered_a_dts_codec_id() {
for codec in [Codec::Dts, Codec::DtsHdMa, Codec::DtsHdHr] {
let track = MkvTrack::audio(&audio_stream(codec));
assert_eq!(
track.codec_id, "A_DTS",
"{codec:?} must map to registered codec ID A_DTS, got {}",
track.codec_id
);
}
assert_eq!(MkvTrack::audio(&audio_stream(Codec::Ac3)).codec_id, "A_AC3");
assert_eq!(
MkvTrack::audio(&audio_stream(Codec::TrueHd)).codec_id,
"A_TRUEHD"
);
}
#[test]
fn dolby_vision_config_profile7() {
let c = dolby_vision_config(7, 6, 0);
assert_eq!(c.len(), 24);
assert_eq!(c[0], 1); assert_eq!(c[1], 0); assert_eq!(c[2] >> 1, 7, "dv_profile must be 7");
assert_eq!(c[3] & 0b0000_0111, 0b0000_0111, "rpu+el+bl all present");
}
#[test]
fn mkv_writes_ebml_header() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let muxer = MkvMuxer::new(buf, &tracks, Some("Test"), 120.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(data.len() >= 4);
assert_eq!(&data[0..4], &[0x1A, 0x45, 0xDF, 0xA3]);
}
#[test]
fn mkv_writes_segment() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let muxer = MkvMuxer::new(buf, &tracks, None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::SEGMENT).is_some(),
"Segment element not found in output"
);
}
#[test]
fn mkv_write_frame_creates_cluster() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let mut muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap();
muxer
.write_frame(0, 0, true, &[0xDE, 0xAD, 0xBE, 0xEF], None)
.unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::CLUSTER).is_some(),
"Cluster element not found after write_frame"
);
}
#[test]
fn mkv_finish_writes_cues_element() {
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let tracks = [make_video_track()];
let mut muxer = MkvMuxer::new(writer, &tracks, Some("Cue Test"), 60.0, &[]).unwrap();
muxer
.write_frame(0, 0, true, &[0x01, 0x02, 0x03], None)
.unwrap();
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
assert!(
find_id(&data, ebml::CUES).is_some(),
"Cues element (0x1C53BB6B) not found after finish()"
);
}
#[test]
fn zero_cues_voids_seekhead_entry_no_dangling_pointer() {
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let tracks = [make_video_track()];
let mut muxer = MkvMuxer::new(writer, &tracks, Some("NoCue"), 60.0, &[]).unwrap();
muxer
.write_frame(0, 0, true, &[0x01, 0x02, 0x03], None)
.unwrap();
let cues_entry_pos = muxer.cues_seek_entry_pos.expect("CUES seek entry recorded");
muxer.cues.clear();
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
assert!(
find_id(&data, ebml::CUES).is_none(),
"no Cues element expected when there are zero cue points"
);
let entry = &data[cues_entry_pos as usize..cues_entry_pos as usize + 2];
assert_eq!(
entry,
&[ebml::VOID as u8, 0x80 | 19],
"CUES Seek entry must be Void(19), not a live Seek pointer"
);
assert!(
find_id(&data, ebml::INFO).is_some() && find_id(&data, ebml::TRACKS).is_some(),
"Info and Tracks must still be present and seekable"
);
}
#[test]
fn monotonic_ts_forces_strictly_increasing() {
assert_eq!(monotonic_ts(None, 1000), 1000);
assert_eq!(monotonic_ts(Some(1000), 1000), 1001);
assert_eq!(monotonic_ts(Some(1001), 1000), 1002);
assert_eq!(monotonic_ts(Some(1000), 1040), 1040);
let raw = [1000i64, 1000, 1000, 999, 1032, 1032, 1064];
let mut prev: Option<i64> = None;
let mut out = Vec::new();
for &p in &raw {
let t = monotonic_ts(prev, p);
out.push(t);
prev = Some(t);
}
assert!(
out.windows(2).all(|w| w[1] > w[0]),
"not strictly monotonic: {out:?}"
);
assert_eq!(out, [1000, 1001, 1002, 1003, 1032, 1033, 1064]);
}
#[test]
fn block_ts_exempts_video_from_monotonic_nudge() {
assert_eq!(
block_ts(true, Some(1040), 1000),
1000,
"video B-frame PTS preserved"
);
assert_eq!(
block_ts(true, Some(1000), 1000),
1000,
"video dup-ms PTS preserved"
);
let gop = [1000i64, 960, 920, 1080, 1040];
let mut prev = None;
let out: Vec<i64> = gop
.iter()
.map(|&p| {
let t = block_ts(true, prev, p);
prev = Some(t);
t
})
.collect();
assert_eq!(out, gop, "video timestamps must be left exactly as-is");
assert_eq!(
block_ts(false, Some(1000), 1000),
1001,
"audio dup-ms nudged"
);
assert_eq!(
block_ts(false, Some(1001), 1000),
1002,
"subtitle back-tick nudged"
);
}
#[test]
fn second_video_track_pts_not_clobbered() {
use std::io::Cursor;
let tracks = vec![make_video_track(), make_video_track()];
let buf = Cursor::new(Vec::new());
let mux = MkvMuxer::new(buf, &tracks, None, 0.0, &[]).unwrap();
assert_eq!(mux.track_is_video, vec![true, true]);
assert_eq!(block_ts(mux.track_is_video[1], Some(1040), 1000), 1000);
}
#[test]
fn clip_boundary_with_straggler_yields_monotonic_clusters() {
let tracks = [make_video_track(), make_audio_track()];
let ms = |m: i64| m * 1_000_000;
let frames: Vec<(usize, i64, bool, Vec<u8>)> = vec![
(0, ms(0), true, vec![0x01; 16]),
(1, ms(0), true, vec![0xA0; 8]),
(0, ms(600_000), true, vec![0x02; 16]), (1, ms(600_000), true, vec![0xA1; 8]),
(0, ms(0), true, vec![0x03; 16]),
(1, ms(599_500), true, vec![0xA2; 8]),
(1, ms(0), true, vec![0xA3; 8]),
(0, ms(5_000), true, vec![0x04; 16]), ];
let (data, frame_count) = mux_to_bytes(&tracks, &[], &frames);
assert_eq!(frame_count, 8, "all frames written (none dropped)");
let tick = |ms: i64| ms * 1_000_000 / TIMESTAMP_SCALE_NS; let clusters = find_clusters(&data);
let ts: Vec<u64> = clusters.iter().map(|&(_, _, t)| t).collect();
assert!(!ts.is_empty(), "expected clusters");
assert!(
ts.windows(2).all(|w| w[1] >= w[0]),
"cluster timestamps must be monotonic, got {ts:?}"
);
let max = *ts.iter().max().unwrap() as i64;
assert!(
max >= tick(600_000),
"timeline must span past the boundary, got {max} ticks"
);
assert!(
max < tick(1_000_000),
"no ratchet: max cluster ts {max} ticks must stay near 605s"
);
}
#[test]
fn epoch_driver_follows_primary_video_not_index_zero() {
let tracks = [make_audio_track(), make_video_track()];
{
let buf = Cursor::new(Vec::new());
let mux = MkvMuxer::new(buf, &tracks, None, 0.0, &[]).unwrap();
assert_eq!(
mux.primary_video_track,
Some(1),
"primary video must be the first VIDEO track (index 1), not 0"
);
}
let ms = |m: i64| m * 1_000_000;
let frames: Vec<(usize, i64, bool, Vec<u8>)> = vec![
(1, ms(0), true, vec![0x01; 16]), (0, ms(0), true, vec![0xA0; 8]), (1, ms(600_000), true, vec![0x02; 16]), (0, ms(600_000), true, vec![0xA1; 8]), (1, ms(0), true, vec![0x03; 16]),
(0, ms(599_500), true, vec![0xA2; 8]),
(0, ms(0), true, vec![0xA3; 8]), (1, ms(5_000), true, vec![0x04; 16]), ];
let (data, frame_count) = mux_to_bytes(&tracks, &[], &frames);
assert_eq!(frame_count, 8, "all frames written (none dropped)");
let tick = |ms: i64| ms * 1_000_000 / TIMESTAMP_SCALE_NS;
let clusters = find_clusters(&data);
let ts: Vec<u64> = clusters.iter().map(|&(_, _, t)| t).collect();
assert!(!ts.is_empty(), "expected clusters");
assert!(
ts.windows(2).all(|w| w[1] >= w[0]),
"cluster timestamps must be monotonic, got {ts:?}"
);
let max = *ts.iter().max().unwrap() as i64;
assert!(
max >= tick(600_000),
"timeline must span past the boundary, got {max} ticks"
);
assert!(
max < tick(1_000_000),
"no ratchet: max cluster ts {max} ticks must stay near 605s"
);
}
#[test]
fn mkv_multiple_tracks() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track(), make_audio_track()];
let mut muxer = MkvMuxer::new(buf, &tracks, Some("Multi"), 120.0, &[]).unwrap();
muxer
.write_frame(0, 0, true, &[0x00, 0x00, 0x01], None)
.unwrap();
muxer
.write_frame(1, 0, false, &[0x0B, 0x77, 0x00], None)
.unwrap();
muxer
.write_frame(0, 40_000_000, false, &[0x00, 0x00, 0x01], None)
.unwrap();
muxer
.write_frame(1, 32_000_000, false, &[0x0B, 0x77, 0x01], None)
.unwrap();
let data = muxer.writer.into_inner();
assert!(data.len() > 100, "output too small for multi-track MKV");
}
#[test]
fn mkv_keyframe_flag() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let mut muxer = MkvMuxer::new(buf, &tracks, None, 10.0, &[]).unwrap();
let pos_before_kf = muxer.writer.position();
muxer.write_frame(0, 0, true, &[0xAA], None).unwrap();
let pos_after_kf = muxer.writer.position();
muxer
.write_frame(0, 1_000_000, false, &[0xBB], None)
.unwrap();
let pos_after_nkf = muxer.writer.position();
let data = muxer.writer.into_inner();
let kf_region = &data[pos_before_kf as usize..pos_after_kf as usize];
let nkf_region = &data[pos_after_kf as usize..pos_after_nkf as usize];
fn extract_flags(region: &[u8]) -> u8 {
let sb_pos = region.iter().position(|&b| b == 0xA3).unwrap();
let after_id = sb_pos + 1;
let size_byte = region[after_id];
let size_len = if size_byte & 0x80 != 0 { 1 } else { 2 };
let track_vint_pos = after_id + size_len;
let track_vint_len = 1; let ts_pos = track_vint_pos + track_vint_len;
let flags_pos = ts_pos + 2;
region[flags_pos]
}
let kf_flags = extract_flags(kf_region);
let nkf_flags = extract_flags(nkf_region);
assert_eq!(
kf_flags & 0x80,
0x80,
"keyframe flag should be set (0x80), got 0x{:02X}",
kf_flags
);
assert_eq!(
nkf_flags & 0x80,
0x00,
"non-keyframe flag should be clear, got 0x{:02X}",
nkf_flags
);
}
#[test]
fn mkv_writes_chapters_element() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let chapters = vec![
Chapter {
time_secs: 0.0,
name: "Chapter 1".into(),
},
Chapter {
time_secs: 300.0,
name: "Chapter 2".into(),
},
Chapter {
time_secs: 600.0,
name: "Chapter 3".into(),
},
];
let muxer = MkvMuxer::new(buf, &tracks, Some("Chapter Test"), 900.0, &chapters).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::CHAPTERS).is_some(),
"Chapters element (0x1043A770) not found in output"
);
assert!(
find_id(&data, ebml::EDITION_ENTRY).is_some(),
"EditionEntry element not found"
);
assert!(
find_id(&data, ebml::CHAPTER_ATOM).is_some(),
"ChapterAtom element not found"
);
}
#[test]
fn mkv_no_chapters_when_empty() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let muxer = MkvMuxer::new(buf, &tracks, Some("No Chapters"), 60.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::CHAPTERS).is_none(),
"Chapters element should not be present when no chapters given"
);
}
#[test]
fn mkv_default_flag_on_first_video_and_audio() {
let video = make_video_track(); let audio1 = make_audio_track(); let mut audio2 = make_audio_track();
audio2.is_default = false;
audio2.language = "fra".into();
let buf = Cursor::new(Vec::new());
let tracks = [video, audio1, audio2];
let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let flag_default_id = ebml::FLAG_DEFAULT.to_be_bytes();
let _needle = &[flag_default_id[3]]; let count = data.windows(1).filter(|w| w[0] == 0x88).count();
assert!(
count >= 1,
"FlagDefault should be written for non-default tracks"
);
}
#[test]
fn mkv_forced_flag_on_forced_subtitle() {
use crate::disc::SubtitleStream;
let video = make_video_track();
let forced_sub = MkvTrack::subtitle(&SubtitleStream {
pid: 0x1200,
codec: Codec::Pgs,
language: "eng".into(),
forced: true,
qualifier: crate::disc::LabelQualifier::Forced,
codec_data: None,
});
assert!(forced_sub.is_forced);
let buf = Cursor::new(Vec::new());
let tracks = [video, forced_sub];
let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::FLAG_FORCED).is_some(),
"FlagForced element should be present for forced subtitle track"
);
}
#[test]
fn mkv_no_forced_flag_on_non_forced_subtitle() {
use crate::disc::SubtitleStream;
let video = make_video_track();
let sub = MkvTrack::subtitle(&SubtitleStream {
pid: 0x1200,
codec: Codec::Pgs,
language: "eng".into(),
forced: false,
qualifier: crate::disc::LabelQualifier::None,
codec_data: None,
});
assert!(!sub.is_forced);
let buf = Cursor::new(Vec::new());
let tracks = [video, sub];
let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::FLAG_FORCED).is_none(),
"FlagForced element should not be present for non-forced subtitle"
);
}
use std::sync::{Arc, Mutex};
struct SharedWriter(Arc<Mutex<Cursor<Vec<u8>>>>);
impl Write for SharedWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.lock().unwrap().flush()
}
}
impl Seek for SharedWriter {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.0.lock().unwrap().seek(pos)
}
}
fn frames_for(duration_secs: f64, gop_secs: f64) -> Vec<(usize, i64, bool, Vec<u8>)> {
let video_interval_ns: i64 = 1_000_000_000 / 24;
let audio_interval_ns: i64 = (1024i64 * 1_000_000_000) / 48_000;
let gop_frames = (gop_secs * 24.0).round() as i64;
let mut out: Vec<(usize, i64, bool, Vec<u8>)> = Vec::new();
let total_ns = (duration_secs * 1_000_000_000.0) as i64;
let mut vi: i64 = 0;
loop {
let pts = vi * video_interval_ns;
if pts >= total_ns {
break;
}
let keyframe = vi % gop_frames == 0;
out.push((0, pts, keyframe, vec![0xAB; 64]));
vi += 1;
}
let mut ai: i64 = 0;
loop {
let pts = ai * audio_interval_ns;
if pts >= total_ns {
break;
}
out.push((1, pts, true, vec![0xCD; 32]));
ai += 1;
}
out.sort_by_key(|f| f.1);
out
}
fn mux_to_bytes(
tracks: &[MkvTrack],
chapters: &[Chapter],
frames: &[(usize, i64, bool, Vec<u8>)],
) -> (Vec<u8>, u64) {
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, tracks, None, 0.0, chapters).unwrap();
for (t, pts, kf, data) in frames {
muxer.write_frame(*t, *pts, *kf, data, None).unwrap();
}
let frame_count = muxer.frame_count;
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
(data, frame_count)
}
fn locate_segment(data: &[u8]) -> (usize, usize) {
let segment_id_pos = find_id(data, ebml::SEGMENT).expect("segment id not found");
(segment_id_pos, segment_id_pos + 4 + 8)
}
fn segment_children(data: &[u8]) -> Vec<(u32, usize, u64)> {
let (_, seg_start) = locate_segment(data);
let mut out = Vec::new();
let mut cursor = Cursor::new(&data[seg_start..]);
while (cursor.position() as usize) < data.len() - seg_start {
let pos_before = cursor.position();
let (id, size, hdr_len) = match ebml::read_element_header(&mut cursor) {
Ok(v) => v,
Err(_) => break,
};
let data_abs = seg_start + pos_before as usize + hdr_len;
out.push((id, data_abs, size));
cursor
.seek(io::SeekFrom::Current(size as i64))
.expect("seek past element body");
}
out
}
fn master_children(data: &[u8], body_start: usize, body_size: usize) -> Vec<(u32, usize, u64)> {
let mut out = Vec::new();
let body = &data[body_start..body_start + body_size];
let mut cursor = Cursor::new(body);
while (cursor.position() as usize) < body.len() {
let pos_before = cursor.position();
let (id, size, hdr_len) = match ebml::read_element_header(&mut cursor) {
Ok(v) => v,
Err(_) => break,
};
let child_abs = body_start + pos_before as usize + hdr_len;
out.push((id, child_abs, size));
cursor
.seek(io::SeekFrom::Current(size as i64))
.expect("seek past child body");
}
out
}
fn first_track_entry(data: &[u8]) -> (usize, usize) {
let (tracks_start, tracks_size) = segment_children(data)
.into_iter()
.find_map(|(id, off, sz)| (id == ebml::TRACKS).then_some((off, sz as usize)))
.expect("Tracks element present");
let (_, te_start, te_size) = master_children(data, tracks_start, tracks_size)
.into_iter()
.find(|(id, _, _)| *id == ebml::TRACK_ENTRY)
.expect("TrackEntry present");
(te_start, te_size as usize)
}
fn find_clusters(data: &[u8]) -> Vec<(usize, u64, u64)> {
let mut out = Vec::new();
for (id, body_start, body_size) in segment_children(data) {
if id == ebml::CLUSTER {
let mut cursor = Cursor::new(&data[body_start..body_start + body_size as usize]);
let (tid, tsize, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(
tid,
ebml::CLUSTER_TIMESTAMP,
"cluster must start with timestamp"
);
let ts = ebml::read_uint_val(&mut cursor, tsize as usize).unwrap();
out.push((body_start, body_size, ts));
}
}
out
}
fn first_simple_block(cluster_body: &[u8]) -> (u64, u8) {
let mut cursor = Cursor::new(cluster_body);
loop {
let (id, size, _) = ebml::read_element_header(&mut cursor).unwrap();
if id == ebml::SIMPLE_BLOCK {
let body_start = cursor.position() as usize;
let b0 = cluster_body[body_start];
let (track_num, vint_len) = if b0 & 0x80 != 0 {
((b0 & 0x7F) as u64, 1usize)
} else if b0 & 0x40 != 0 {
let b1 = cluster_body[body_start + 1];
((((b0 & 0x3F) as u64) << 8) | b1 as u64, 2)
} else {
panic!("unsupported track vint width");
};
let flags = cluster_body[body_start + vint_len + 2];
return (track_num, flags);
}
cursor.seek(io::SeekFrom::Current(size as i64)).unwrap();
}
}
fn parse_cues(data: &[u8]) -> Vec<(u64, u64, u64)> {
let mut out = Vec::new();
let (cues_id, cues_body_start, cues_body_size) = segment_children(data)
.into_iter()
.find(|(id, _, _)| *id == ebml::CUES)
.expect("cues element not found");
assert_eq!(cues_id, ebml::CUES);
let cues_body = &data[cues_body_start..cues_body_start + cues_body_size as usize];
let mut cursor = Cursor::new(cues_body);
while (cursor.position() as usize) < cues_body.len() {
let (id, size, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(id, ebml::CUE_POINT);
let cp_end = cursor.position() + size;
let mut cue_time = 0u64;
let mut cue_track = 0u64;
let mut cue_pos = 0u64;
while cursor.position() < cp_end {
let (sid, ssize, _) = ebml::read_element_header(&mut cursor).unwrap();
match sid {
ebml::CUE_TIME => {
cue_time = ebml::read_uint_val(&mut cursor, ssize as usize).unwrap();
}
ebml::CUE_TRACK_POSITIONS => {
let ctp_end = cursor.position() + ssize;
while cursor.position() < ctp_end {
let (iid, isize_, _) = ebml::read_element_header(&mut cursor).unwrap();
match iid {
ebml::CUE_TRACK => {
cue_track =
ebml::read_uint_val(&mut cursor, isize_ as usize).unwrap();
}
ebml::CUE_CLUSTER_POSITION => {
cue_pos =
ebml::read_uint_val(&mut cursor, isize_ as usize).unwrap();
}
_ => {
cursor.seek(io::SeekFrom::Current(isize_ as i64)).unwrap();
}
}
}
}
_ => {
cursor.seek(io::SeekFrom::Current(ssize as i64)).unwrap();
}
}
}
out.push((cue_time, cue_track, cue_pos));
}
out
}
fn parse_seekhead(data: &[u8]) -> Vec<(u32, u64)> {
let mut out = Vec::new();
let (sh_id, sh_body_start, sh_body_size) = segment_children(data)
.into_iter()
.find(|(id, _, _)| *id == ebml::SEEK_HEAD)
.expect("seekhead not found");
assert_eq!(sh_id, ebml::SEEK_HEAD);
let sh_body = &data[sh_body_start..sh_body_start + sh_body_size as usize];
let mut cursor = Cursor::new(sh_body);
while (cursor.position() as usize) < sh_body.len() {
let (id, size, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(id, ebml::SEEK);
let seek_end = cursor.position() + size;
let mut seek_id_val: u32 = 0;
let mut seek_pos_val: u64 = 0;
while cursor.position() < seek_end {
let (sid, ssize, _) = ebml::read_element_header(&mut cursor).unwrap();
match sid {
ebml::SEEK_ID => {
let raw = ebml::read_uint_val(&mut cursor, ssize as usize).unwrap();
seek_id_val = raw as u32;
}
ebml::SEEK_POSITION => {
seek_pos_val = ebml::read_uint_val(&mut cursor, ssize as usize).unwrap();
}
_ => {
cursor.seek(io::SeekFrom::Current(ssize as i64)).unwrap();
}
}
}
out.push((seek_id_val, seek_pos_val));
}
out
}
#[test]
fn keyframe_driven_clusters_start_on_video_keyframe() {
let tracks = [make_video_track(), make_audio_track()];
let frames = frames_for(30.0, 1.0);
let (data, _) = mux_to_bytes(&tracks, &[], &frames);
let clusters = find_clusters(&data);
assert!(!clusters.is_empty(), "expected at least one cluster");
for (body_start, body_size, _ts) in clusters {
let body = &data[body_start..body_start + body_size as usize];
let mut cursor = Cursor::new(body);
let (tid, tsize, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
cursor.seek(io::SeekFrom::Current(tsize as i64)).unwrap();
let after_ts = cursor.position() as usize;
let (track_num, flags) = first_simple_block(&body[after_ts..]);
assert_eq!(
track_num, 1,
"first block in cluster must be track 1 (video)"
);
assert_eq!(
flags & 0x80,
0x80,
"first block in cluster must have keyframe flag set, got 0x{:02X}",
flags
);
}
}
#[test]
fn cue_count_equals_cluster_count() {
let tracks = [make_video_track(), make_audio_track()];
let frames = frames_for(30.0, 1.0);
let (data, _) = mux_to_bytes(&tracks, &[], &frames);
let clusters = find_clusters(&data);
let cues = parse_cues(&data);
assert_eq!(
clusters.len(),
cues.len(),
"every cluster must have a cue: cluster count {} != cue count {}",
clusters.len(),
cues.len()
);
assert_eq!(
clusters.len(),
15,
"expected 15 clusters for 30s @ 2s cluster duration"
);
}
#[test]
fn cue_count_equals_cluster_count_blockgroup_vfr() {
use crate::mux::codec::CodecParser;
use crate::mux::codec::mpeg2::Mpeg2Parser;
use crate::mux::ts::PesPacket;
fn seq_header() -> Vec<u8> {
let (w, h, aspect, fr): (u16, u16, u8, u8) = (720, 480, 2, 4);
let mut hdr = vec![0x00, 0x00, 0x01, 0xB3u8];
hdr.push((w >> 4) as u8);
hdr.push((((w & 0x0F) as u8) << 4) | (((h >> 8) & 0x0F) as u8));
hdr.push((h & 0xFF) as u8);
hdr.push((aspect << 4) | (fr & 0x0F));
hdr.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x00]);
hdr
}
fn gop() -> Vec<u8> {
vec![0x00, 0x00, 0x01, 0xB8u8, 0x00, 0x00, 0x00, 0x00]
}
fn pic(coding_type: u8, tr: u16) -> Vec<u8> {
let b4 = ((tr >> 2) & 0xFF) as u8;
let b5 = (((tr & 0x03) as u8) << 6) | ((coding_type & 0x07) << 3);
let mut au = vec![0x00, 0x00, 0x01, 0x00u8, b4, b5, 0x00, 0x00];
au.extend_from_slice(&[0x00, 0x00, 0x01, 0xB5u8, 0x80, 0x00, 0x03, 0x00, 0x80]);
au.extend_from_slice(&[0xAA; 32]);
au
}
let mut parser = Mpeg2Parser::new();
let field_ns = 1_000_000_000i64 * 1001 / 30000 / 2;
let frame_ns = 2 * field_ns;
let mut frames: Vec<crate::mux::codec::Frame> = Vec::new();
let gop_len = 12u16;
for g in 0..80i64 {
let mut es = seq_header();
es.extend_from_slice(&gop());
es.extend_from_slice(&pic(1, 0)); for tr in 1..gop_len {
let ct = if tr % 3 == 0 { 2 } else { 3 };
es.extend_from_slice(&pic(ct, tr));
}
let gop_pts = g * gop_len as i64 * frame_ns * 90_000 / 1_000_000_000;
frames.extend(parser.parse(&PesPacket {
source: None,
pid: 0x1011,
pts: Some(gop_pts),
dts: None,
data: es,
}));
}
frames.extend(parser.flush());
assert!(
frames.iter().all(|f| f.duration_ns.is_some()),
"DVD VFR frames must carry per-frame durations (BlockGroup path)"
);
assert_eq!(
frames.iter().filter(|f| f.keyframe).count(),
80,
"one I-frame keyframe per GOP"
);
let tracks = [make_video_track()];
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, &tracks, None, 0.0, &[]).unwrap();
for f in &frames {
muxer
.write_frame(0, f.pts_ns, f.keyframe, &f.data, f.duration_ns)
.unwrap();
}
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
let clusters = find_clusters(&data);
{
let (c0_start, c0_size, _) = clusters[0];
let mut kinds = Vec::new();
let mut cur = Cursor::new(&data[c0_start..c0_start + c0_size as usize]);
while (cur.position() as usize) < c0_size as usize {
let (id, size, _) = ebml::read_element_header(&mut cur).unwrap();
kinds.push(id);
cur.seek(io::SeekFrom::Current(size as i64)).unwrap();
}
assert!(
kinds.contains(&ebml::BLOCK_GROUP),
"DVD VFR cluster must contain a BlockGroup"
);
assert!(
!kinds.contains(&ebml::SIMPLE_BLOCK),
"DVD VFR cluster must NOT contain a SimpleBlock"
);
}
let cues = parse_cues(&data);
assert!(
clusters.len() > 1,
"expected many clusters for 32 s of video"
);
assert_eq!(
clusters.len(),
cues.len(),
"BlockGroup/VFR (DVD) seek index: every cluster must have a cue — \
cluster count {} != cue count {}",
clusters.len(),
cues.len()
);
let (_, seg_start) = locate_segment(&data);
for (_time, _track, pos) in &cues {
let abs = seg_start + *pos as usize;
let mut cursor = Cursor::new(&data[abs..]);
let (id, _size, _hdr_len) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(
id,
ebml::CLUSTER,
"cue position 0x{:X} did not resolve to a cluster",
pos
);
}
}
#[test]
fn cue_positions_resolve_to_clusters() {
let tracks = [make_video_track(), make_audio_track()];
let frames = frames_for(30.0, 1.0);
let (data, _) = mux_to_bytes(&tracks, &[], &frames);
let (_, seg_start) = locate_segment(&data);
let cues = parse_cues(&data);
assert!(!cues.is_empty());
for (_time, _track, pos) in cues {
let abs = seg_start + pos as usize;
let mut cursor = Cursor::new(&data[abs..]);
let (id, _size, _hdr_len) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(
id,
ebml::CLUSTER,
"cue position 0x{:X} did not resolve to a cluster",
pos
);
}
}
#[test]
fn cue_times_match_cluster_timestamps() {
let tracks = [make_video_track(), make_audio_track()];
let frames = frames_for(30.0, 1.0);
let (data, _) = mux_to_bytes(&tracks, &[], &frames);
let (_, seg_start) = locate_segment(&data);
let cues = parse_cues(&data);
for (time, _track, pos) in cues {
let abs = seg_start + pos as usize;
let mut cursor = Cursor::new(&data[abs..]);
let (id, size, _hdr_len) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(id, ebml::CLUSTER);
let body_start = abs + (cursor.position() as usize);
let body = &data[body_start..body_start + size as usize];
let mut bc = Cursor::new(body);
let (tid, tsize, _) = ebml::read_element_header(&mut bc).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
let cluster_ts = ebml::read_uint_val(&mut bc, tsize as usize).unwrap();
assert_eq!(
cluster_ts, time,
"cluster timestamp {} != cue time {}",
cluster_ts, time
);
}
}
#[test]
fn opening_keyframe_with_nonzero_disc_pts_anchors_base_not_corrupted() {
let tracks = [make_video_track()];
const OPEN_PTS: i64 = 10_000_000_000; let frames = vec![
(0usize, OPEN_PTS, true, vec![0xAAu8; 8]), (0usize, OPEN_PTS + 40_000_000, false, vec![0xBBu8; 8]), ];
let (data, count) = mux_to_bytes(&tracks, &[], &frames);
assert_eq!(
count, 2,
"both opening frames written (none dropped/floored away)"
);
let (_, seg_start) = locate_segment(&data);
let cluster_abs = seg_start
+ segment_children(&data)
.iter()
.find(|(id, _, _)| *id == ebml::CLUSTER)
.map(|(_, off, _)| *off - seg_start)
.expect("a cluster was written");
let mut bc = Cursor::new(&data[cluster_abs..]);
let (tid, tsize, _) = ebml::read_element_header(&mut bc).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
let cluster_ts = ebml::read_uint_val(&mut bc, tsize as usize).unwrap();
assert_eq!(
cluster_ts, 0,
"opening cluster timestamp must be 0 (base anchored on the opening keyframe's real PTS)"
);
let cues = parse_cues(&data);
assert_eq!(cues[0].0, 0, "opening cue at t=0, disc PTS rebased to base");
}
#[test]
fn seekhead_is_first_child_of_segment() {
let tracks = [make_video_track(), make_audio_track()];
let (data, _) = mux_to_bytes(&tracks, &[], &frames_for(10.0, 1.0));
let children = segment_children(&data);
assert!(!children.is_empty());
assert_eq!(
children[0].0,
ebml::SEEK_HEAD,
"first child of segment must be SeekHead, got id 0x{:X}",
children[0].0
);
}
#[test]
fn seekhead_points_to_real_elements() {
let tracks = [make_video_track(), make_audio_track()];
let (data, _) = mux_to_bytes(&tracks, &[], &frames_for(10.0, 1.0));
let (_, seg_start) = locate_segment(&data);
let entries = parse_seekhead(&data);
let required = [ebml::INFO, ebml::TRACKS, ebml::CUES];
for &want_id in &required {
let entry = entries
.iter()
.find(|(id, _)| *id == want_id)
.unwrap_or_else(|| panic!("seekhead missing entry for id 0x{:X}", want_id));
let abs = seg_start + entry.1 as usize;
let mut cursor = Cursor::new(&data[abs..]);
let (got_id, _, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(
got_id, want_id,
"seekhead entry for 0x{:X} resolves to wrong id 0x{:X}",
want_id, got_id
);
}
}
#[test]
fn seekhead_omits_chapters_when_empty() {
let tracks = [make_video_track()];
let (data, _) = mux_to_bytes(&tracks, &[], &frames_for(5.0, 1.0));
let entries = parse_seekhead(&data);
assert_eq!(
entries.len(),
3,
"expected 3 seek entries (Info, Tracks, Cues), got {}",
entries.len()
);
assert!(
entries.iter().all(|(id, _)| *id != ebml::CHAPTERS),
"seekhead should not contain Chapters entry when chapters are empty"
);
}
fn all_block_timestamps(data: &[u8]) -> Vec<(i64, i16, i64)> {
let mut out = Vec::new();
for (body_start, body_size, cluster_ts) in find_clusters(data) {
let body = &data[body_start..body_start + body_size as usize];
let mut cursor = Cursor::new(body);
let (tid, tsize, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
cursor.seek(io::SeekFrom::Current(tsize as i64)).unwrap();
while (cursor.position() as usize) < body.len() {
let (id, sz, _) = ebml::read_element_header(&mut cursor).unwrap();
if id == ebml::SIMPLE_BLOCK {
let bstart = cursor.position() as usize;
let b0 = body[bstart];
let vint_len = if b0 & 0x80 != 0 { 1 } else { 2 };
let ts_pos = bstart + vint_len;
let rel = i16::from_be_bytes([body[ts_pos], body[ts_pos + 1]]);
out.push((cluster_ts as i64, rel, cluster_ts as i64 + rel as i64));
}
cursor.seek(io::SeekFrom::Current(sz as i64)).unwrap();
}
}
out
}
#[test]
fn long_audio_gap_forces_cluster_no_i16_overflow() {
let tracks = [make_video_track(), make_audio_track()];
let mut frames: Vec<(usize, i64, bool, Vec<u8>)> = Vec::new();
frames.push((0, 0, true, vec![0xAB; 16]));
let mut t_ms = 0i64;
while t_ms <= 60_000 {
frames.push((1, t_ms * 1_000_000, true, vec![0xCD; 16]));
t_ms += 100;
}
let (data, _) = mux_to_bytes(&tracks, &[], &frames);
let blocks = all_block_timestamps(&data);
assert!(!blocks.is_empty());
for (cluster_ts, rel, abs) in &blocks {
assert!(
*rel as i64 >= 0 && (*rel as i64) <= MAX_BLOCK_REL,
"block relative_ts {rel} out of [0, i16::MAX] range \
(cluster_ts={cluster_ts}, abs={abs}) — i16 overflow"
);
}
let max_abs = blocks.iter().map(|(_, _, abs)| *abs).max().unwrap();
let tick = |ms: i64| ms * 1_000_000 / TIMESTAMP_SCALE_NS;
assert_eq!(
max_abs,
tick(60_000),
"last block must reconstruct to 600_000 ticks (60_000 ms)"
);
let clusters = find_clusters(&data);
assert!(
clusters.len() >= 2,
"expected the i16 guard to force extra clusters, got {}",
clusters.len()
);
let cues = parse_cues(&data);
assert_eq!(
cues.len(),
clusters.len(),
"every i16-split cluster must emit a cue (cues {} != clusters {})",
cues.len(),
clusters.len()
);
let max_cue = cues.iter().map(|(t, _, _)| *t).max().unwrap() as i64;
assert!(
max_cue > MAX_BLOCK_REL,
"cue coverage must extend past the first i16 boundary, max cue {max_cue}"
);
}
#[test]
fn pre_first_keyframe_frames_dropped() {
let tracks = [make_video_track()];
let frames = vec![
(0usize, 0i64, false, vec![0x11; 16]),
(0usize, 41_000_000i64, true, vec![0x22; 16]),
];
let (data, frame_count) = mux_to_bytes(&tracks, &[], &frames);
assert_eq!(frame_count, 1, "muxer.frame_count must equal 1");
let clusters = find_clusters(&data);
assert_eq!(clusters.len(), 1, "expected exactly one cluster");
let (body_start, body_size, _ts) = clusters[0];
let body = &data[body_start..body_start + body_size as usize];
let mut cursor = Cursor::new(body);
let (tid, tsize, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
cursor.seek(io::SeekFrom::Current(tsize as i64)).unwrap();
let mut sb_count = 0;
while (cursor.position() as usize) < body.len() {
let (id, sz, _) = ebml::read_element_header(&mut cursor).unwrap();
if id == ebml::SIMPLE_BLOCK {
sb_count += 1;
}
cursor.seek(io::SeekFrom::Current(sz as i64)).unwrap();
}
assert_eq!(sb_count, 1, "expected exactly one SimpleBlock in output");
}
#[test]
fn no_track0_keyframe_yields_error_not_empty_file() {
let tracks = [make_video_track(), make_audio_track()];
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, &tracks, None, 0.0, &[]).unwrap();
muxer.write_frame(1, 0, true, &[0xAA; 8], None).unwrap();
muxer
.write_frame(0, 10_000_000, false, &[0xBB; 8], None)
.unwrap();
muxer
.write_frame(1, 20_000_000, true, &[0xCC; 8], None)
.unwrap();
let err = muxer.finish().unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn finish_with_no_frames_errors() {
let buf = Cursor::new(Vec::new());
let tracks = [make_video_track()];
let muxer = MkvMuxer::new(buf, &tracks, None, 60.0, &[]).unwrap();
let err = muxer.finish().unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn backjumped_audio_handled_by_i16_split_no_wrap() {
let tracks = [make_video_track(), make_audio_track()];
let frames = vec![
(0usize, 0i64, true, vec![0x01; 16]),
(0usize, 40_000_000_000i64, true, vec![0x02; 16]), (1usize, 0i64, true, vec![0x03; 16]), ];
let (data, frame_count) = mux_to_bytes(&tracks, &[], &frames);
assert_eq!(frame_count, 3);
let clusters = find_clusters(&data);
let ts: Vec<u64> = clusters.iter().map(|(_, _, t)| *t).collect();
for t in &ts {
assert!(*t <= i64::MAX as u64, "cluster ts must not have wrapped");
}
for (cluster_ts, rel, abs) in all_block_timestamps(&data) {
assert!(
(MIN_BLOCK_REL..=MAX_BLOCK_REL).contains(&(rel as i64)),
"block rel {rel} wrapped i16 (cluster_ts={cluster_ts}, abs={abs})"
);
}
assert_eq!(
parse_cues(&data).len(),
clusters.len(),
"every cluster (incl. the i16-split) must have a cue"
);
}
#[test]
fn negative_pts_audio_after_keyframe_does_not_wrap() {
let tracks = [make_video_track(), make_audio_track()];
let frames_in_order = [
(0usize, 5_000_000_000i64, true, vec![0xBB; 16]), (1usize, 4_000_000_000i64, true, vec![0xAA; 8]), ];
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, &tracks, None, 0.0, &[]).unwrap();
for (t, pts, kf, data) in &frames_in_order {
muxer.write_frame(*t, *pts, *kf, data, None).unwrap();
}
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
let clusters = find_clusters(&data);
assert!(!clusters.is_empty());
for (_, _, ts) in &clusters {
assert!(*ts < 1_000_000_000, "cluster timestamp wrapped: {}", ts);
}
}
#[test]
fn track_vint_encodes_one_and_two_byte_forms() {
let (b, n) = track_vint(1);
assert_eq!(&b[..n], &[0x81]);
let (b, n) = track_vint(0x7F);
assert_eq!(&b[..n], &[0xFF]);
let (b, n) = track_vint(0x80);
assert_eq!(&b[..n], &[0x40, 0x80]);
let (b, n) = track_vint(0x3FFF);
assert_eq!(&b[..n], &[0x7F, 0xFF]);
}
fn first_simple_block_full(data: &[u8]) -> (u64, usize, i16, u8, Vec<u8>) {
let clusters = find_clusters(data);
let (body_start, body_size, _ts) = clusters[0];
let body = &data[body_start..body_start + body_size as usize];
let mut cursor = Cursor::new(body);
let (tid, tsize, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
cursor.seek(io::SeekFrom::Current(tsize as i64)).unwrap();
loop {
let (id, size, _) = ebml::read_element_header(&mut cursor).unwrap();
if id == ebml::SIMPLE_BLOCK {
let p = cursor.position() as usize;
let b0 = body[p];
let vl = if b0 & 0x80 != 0 { 1 } else { 2 };
let rel = i16::from_be_bytes([body[p + vl], body[p + vl + 1]]);
let flags = body[p + vl + 2];
let dat = body[p + vl + 3..p + size as usize].to_vec();
return (size, vl, rel, flags, dat);
}
cursor.seek(io::SeekFrom::Current(size as i64)).unwrap();
}
}
type DurFrame = (usize, i64, bool, Vec<u8>, Option<u64>);
fn mux_with_durations(tracks: &[MkvTrack], frames: &[DurFrame]) -> Vec<u8> {
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, tracks, None, 0.0, &[]).unwrap();
for (t, pts, kf, data, dur) in frames {
muxer.write_frame(*t, *pts, *kf, data, *dur).unwrap();
}
muxer.finish().unwrap();
shared.lock().unwrap().clone().into_inner()
}
#[test]
fn simple_block_declared_size_covers_exactly_the_payload() {
let tracks = [make_video_track()];
let payload = vec![0x11u8, 0x22, 0x33, 0x44, 0x55];
let data = mux_with_durations(&tracks, &[(0, 0, true, payload.clone(), None)]);
let (size, vl, rel, flags, dat) = first_simple_block_full(&data);
assert_eq!(size as usize, vl + 2 + 1 + payload.len());
assert_eq!(rel, 0, "first frame at cluster base → rel ts 0");
assert_eq!(flags & 0x80, 0x80, "keyframe flag set");
assert_eq!(dat, payload, "data must be the exact frame bytes");
}
#[test]
fn simple_block_rel_ts_is_signed_big_endian() {
let tracks = [make_video_track()];
let data = mux_with_durations(
&tracks,
&[
(0, 0, true, vec![0xAA], None),
(0, 1_000_000_000, false, vec![0xBB], None),
],
);
let clusters = find_clusters(&data);
assert_eq!(clusters.len(), 1, "1s < 3s cluster window → one cluster");
let blocks = all_block_timestamps(&data);
let rels: Vec<i16> = blocks.iter().map(|(_, r, _)| *r).collect();
assert!(
rels.contains(&10_000),
"second block rel ts must be 10_000 ticks"
);
}
fn first_block_group(data: &[u8]) -> (Vec<u8>, u64, u8) {
let clusters = find_clusters(data);
for (body_start, body_size, _ts) in clusters {
let body = &data[body_start..body_start + body_size as usize];
let mut cursor = Cursor::new(body);
let (tid, tsize, _) = ebml::read_element_header(&mut cursor).unwrap();
assert_eq!(tid, ebml::CLUSTER_TIMESTAMP);
cursor.seek(io::SeekFrom::Current(tsize as i64)).unwrap();
while (cursor.position() as usize) < body.len() {
let (id, size, _) = ebml::read_element_header(&mut cursor).unwrap();
if id == ebml::BLOCK_GROUP {
let bg_start = cursor.position() as usize;
let bg = &body[bg_start..bg_start + size as usize];
let mut bc = Cursor::new(bg);
let mut data_after = Vec::new();
let mut dur = 0u64;
let mut flags = 0xFFu8;
while (bc.position() as usize) < bg.len() {
let (cid, cs, _) = ebml::read_element_header(&mut bc).unwrap();
let cstart = bc.position() as usize;
if cid == ebml::BLOCK {
let blk = &bg[cstart..cstart + cs as usize];
let vl = if blk[0] & 0x80 != 0 { 1 } else { 2 };
flags = blk[vl + 2];
data_after = blk[vl + 3..].to_vec();
} else if cid == ebml::BLOCK_DURATION {
dur = ebml::read_uint_val(&mut bc, cs as usize).unwrap();
continue;
}
bc.seek(io::SeekFrom::Current(cs as i64)).unwrap();
}
return (data_after, dur, flags);
}
cursor.seek(io::SeekFrom::Current(size as i64)).unwrap();
}
}
panic!("no BlockGroup found");
}
#[test]
fn block_group_emits_block_duration_and_clears_keyframe_flag() {
let tracks = [make_video_track()];
let data = mux_with_durations(
&tracks,
&[
(0, 0, true, vec![0xAA], None),
(0, 40_000_000, true, vec![0xCC, 0xDD], Some(40_000_000)),
],
);
let (block_data, dur_ticks, flags) = first_block_group(&data);
assert_eq!(block_data, vec![0xCC, 0xDD]);
assert_eq!(dur_ticks, 400, "BlockDuration must be 400 ticks (40 ms)");
assert_eq!(
flags & 0x80,
0x00,
"Block inside BlockGroup must clear the keyframe flag (got 0x{flags:02X})"
);
}
#[test]
fn block_duration_floored_to_at_least_one_tick() {
let tracks = [make_video_track()];
let data = mux_with_durations(
&tracks,
&[
(0, 0, true, vec![0xAA], None),
(0, 10_000_000, true, vec![0xBB], Some(50_000)),
],
);
let (_, dur_ticks, _) = first_block_group(&data);
assert_eq!(
dur_ticks, 1,
"sub-tick duration must floor to 1 tick, not 0"
);
}
#[test]
fn keyframe_at_cluster_boundary_opens_new_cluster() {
let tracks = [make_video_track()];
let data = mux_with_durations(
&tracks,
&[
(0, 0, true, vec![0xAA], None),
(0, 3_000_000_000, true, vec![0xBB], None),
],
);
assert_eq!(
find_clusters(&data).len(),
2,
"keyframe at the 3s boundary must open a second cluster"
);
}
#[test]
fn keyframe_just_under_cluster_window_stays_in_cluster() {
let tracks = [make_video_track()];
let data = mux_with_durations(
&tracks,
&[
(0, 0, true, vec![0xAA], None),
(0, 1_999_000_000, true, vec![0xBB], None),
],
);
assert_eq!(
find_clusters(&data).len(),
1,
"keyframe under the cluster window must stay in the open cluster"
);
}
#[test]
fn monotonic_ts_saturates_at_i64_max() {
assert_eq!(monotonic_ts(Some(i64::MAX), i64::MAX), i64::MAX);
assert_eq!(monotonic_ts(Some(10), 100), 100);
}
#[test]
fn dolby_vision_config_packs_level_and_compat_id() {
let c = dolby_vision_config(7, 6, 1);
assert_eq!(c.len(), 24);
assert_eq!(c[2], 7 << 1);
assert_eq!(c[2] & 0x01, 0, "level bit 5 is 0 for level 6");
assert_eq!(c[3], (6 << 3) | 0b111);
assert_eq!(c[4], 1 << 4);
assert!(c[5..].iter().all(|&b| b == 0), "v[5..24] reserved = 0");
}
#[test]
fn dolby_vision_config_high_level_sets_byte2_low_bit() {
let c = dolby_vision_config(7, 0x20, 0);
assert_eq!(c[2] & 0x01, 1, "level bit 5 belongs in byte2 LSB");
assert_eq!(c[3] >> 3, 0);
}
#[test]
fn muxed_frames_round_trip_through_reader() {
use crate::pes::Stream as _;
let tracks = [make_video_track(), make_audio_track()];
let frames = vec![
(0usize, 0i64, true, vec![0x01, 0x02, 0x03]),
(1usize, 0i64, false, vec![0x0B, 0x77, 0x00]),
(0usize, 1_000_000_000i64, false, vec![0x04, 0x05]),
];
let (data, count) = mux_to_bytes(&tracks, &[], &frames);
assert_eq!(count, 3, "all three frames must be written");
let mut stream = super::super::mkvstream::MkvStream::open(Cursor::new(data)).unwrap();
let mut read_back = Vec::new();
while let Some(f) = stream.read().unwrap() {
read_back.push((f.track, f.keyframe, f.data));
}
assert_eq!(read_back.len(), 3, "every muxed frame must read back");
assert!(
read_back
.iter()
.any(|(t, kf, d)| *t == 0 && *kf && d == &[0x01, 0x02, 0x03])
);
assert!(
read_back
.iter()
.any(|(t, _, d)| *t == 1 && d == &[0x0B, 0x77, 0x00])
);
}
#[test]
fn audio_track_emits_sampling_frequency_and_channels() {
let tracks = [make_video_track(), make_audio_track()];
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &tracks, None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::AUDIO).is_some(),
"Audio element present"
);
assert!(
find_id(&data, ebml::SAMPLING_FREQUENCY).is_some(),
"SamplingFrequency present"
);
assert!(find_id(&data, ebml::CHANNELS).is_some(), "Channels present");
}
#[test]
fn video_colour_element_emitted_only_when_hdr_metadata_present() {
let mut hdr_video = make_video_track();
hdr_video.colour_matrix = 9; hdr_video.colour_transfer = 16; let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[hdr_video], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::COLOUR).is_some(),
"Colour element must be emitted for HDR track"
);
let muxer = MkvMuxer::new(
Cursor::new(Vec::new()),
&[make_video_track()],
None,
0.0,
&[],
)
.unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::COLOUR).is_none(),
"no Colour element when colour metadata is all zero"
);
}
#[test]
fn video_emits_hdr10_mastering_and_cll_with_correct_scaling() {
use crate::mux::codec::Hdr10Metadata;
let h = Hdr10Metadata {
display_primaries_x: [8500, 6550, 35400], display_primaries_y: [39850, 2300, 14600],
white_point_x: 15635,
white_point_y: 16450,
max_display_mastering_luminance: 10_000_000, min_display_mastering_luminance: 1, max_content_light_level: 1000,
max_pic_average_light_level: 400,
};
let mut v = make_video_track();
v.colour_matrix = 9;
v.colour_transfer = 16;
v.colour_primaries = 9;
v.colour_range = 1;
v.hdr10 = Some(h);
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[v], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let read_float = |id: u32| -> f64 {
let off = find_id(&data, id).unwrap_or_else(|| panic!("element {id:#x} present"));
assert_eq!(
data[off + 2],
0x88,
"float element {id:#x} declares 8 bytes"
);
let mut b = [0u8; 8];
b.copy_from_slice(&data[off + 3..off + 11]);
f64::from_be_bytes(b)
};
let read_uint = |id: u32| -> u64 {
let off = find_id(&data, id).unwrap_or_else(|| panic!("element {id:#x} present"));
let len = (data[off + 2] & 0x7F) as usize;
let mut val = 0u64;
for &byte in &data[off + 3..off + 3 + len] {
val = (val << 8) | byte as u64;
}
val
};
assert!(
find_id(&data, ebml::MASTERING_METADATA).is_some(),
"MasteringMetadata present"
);
assert_eq!(
read_float(ebml::PRIMARY_R_CHROMATICITY_X),
35400.0 * 0.00002
);
assert_eq!(
read_float(ebml::PRIMARY_R_CHROMATICITY_Y),
14600.0 * 0.00002
);
assert_eq!(read_float(ebml::PRIMARY_G_CHROMATICITY_X), 8500.0 * 0.00002);
assert_eq!(
read_float(ebml::PRIMARY_G_CHROMATICITY_Y),
39850.0 * 0.00002
);
assert_eq!(read_float(ebml::PRIMARY_B_CHROMATICITY_X), 6550.0 * 0.00002);
assert_eq!(read_float(ebml::PRIMARY_B_CHROMATICITY_Y), 2300.0 * 0.00002);
assert_eq!(
read_float(ebml::WHITE_POINT_CHROMATICITY_X),
15635.0 * 0.00002
);
assert_eq!(
read_float(ebml::WHITE_POINT_CHROMATICITY_Y),
16450.0 * 0.00002
);
assert_eq!(read_float(ebml::LUMINANCE_MAX), 1000.0);
assert_eq!(read_float(ebml::LUMINANCE_MIN), 0.0001);
assert_eq!(read_uint(ebml::MAX_CLL), 1000);
assert_eq!(read_uint(ebml::MAX_FALL), 400);
}
#[test]
fn sdr_video_omits_hdr10_metadata() {
let mut v = make_video_track();
v.colour_matrix = 1; v.colour_transfer = 1;
assert!(v.hdr10.is_none());
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[v], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::COLOUR).is_some(),
"Colour still emitted"
);
assert!(
find_id(&data, ebml::MASTERING_METADATA).is_none(),
"no MasteringMetadata for SDR"
);
assert!(find_id(&data, ebml::MAX_CLL).is_none(), "no MaxCLL for SDR");
assert!(
find_id(&data, ebml::MAX_FALL).is_none(),
"no MaxFALL for SDR"
);
}
#[test]
fn video_emits_flag_interlaced_and_field_order() {
let mut interlaced = make_video_track();
interlaced.interlaced = true;
interlaced.field_order = ebml::FIELD_ORDER_TFF;
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[interlaced], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let fi = find_id(&data, ebml::FLAG_INTERLACED).expect("FlagInterlaced present");
assert_eq!(
data[fi + 2],
ebml::INTERLACED_INTERLACED as u8,
"FlagInterlaced must be 1 (interlaced)"
);
let fo = find_id(&data, ebml::FIELD_ORDER).expect("FieldOrder present");
assert_eq!(
data[fo + 2],
ebml::FIELD_ORDER_TFF,
"FieldOrder value must round-trip through the writer"
);
let muxer = MkvMuxer::new(
Cursor::new(Vec::new()),
&[make_video_track()],
None,
0.0,
&[],
)
.unwrap();
let data = muxer.writer.into_inner();
let fi = find_id(&data, ebml::FLAG_INTERLACED).expect("FlagInterlaced present");
assert_eq!(
data[fi + 2],
ebml::INTERLACED_PROGRESSIVE as u8,
"FlagInterlaced must be 2 (progressive)"
);
assert!(
find_id(&data, ebml::FIELD_ORDER).is_none(),
"no FieldOrder for progressive content"
);
}
#[test]
fn video_576i_field_order_undetermined_at_track_build() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Mpeg2,
resolution: Resolution::R576i,
frame_rate: crate::disc::FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt470bg,
display_aspect: Some((16, 9)),
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert!(t.interlaced, "576i is interlaced (FlagInterlaced=1)");
assert_eq!(
t.field_order,
ebml::FIELD_ORDER_UNDETERMINED,
"field order is not known at scan — set later from the measured picture"
);
}
#[test]
fn interlaced_576i_omits_default_decoded_field_duration_keeps_full_frame_duration() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Mpeg2,
resolution: Resolution::R576i,
frame_rate: crate::disc::FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt470bg,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert_eq!(t.default_duration_ns, 40_000_000, "frame duration is 40 ms");
assert_eq!(
t.field_duration_ns, 0,
"field duration must be 0 so the element is suppressed"
);
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let dd = find_id(&data, ebml::DEFAULT_DURATION).expect("DefaultDuration present");
let frame_ns = u32::from_be_bytes([data[dd + 4], data[dd + 5], data[dd + 6], data[dd + 7]]);
assert_eq!(frame_ns, 40_000_000, "DefaultDuration is the full frame");
assert!(
find_id(&data, ebml::DEFAULT_DECODED_FIELD_DURATION).is_none(),
"DefaultDecodedFieldDuration must NOT be written (Windows halves the rate when it is)"
);
let fi = find_id(&data, ebml::FLAG_INTERLACED).expect("FlagInterlaced present");
assert_eq!(
data[fi + 2],
ebml::INTERLACED_INTERLACED as u8,
"FlagInterlaced=1 retained"
);
assert!(
find_id(&data, ebml::FIELD_ORDER).is_none(),
"FieldOrder omitted until measured — no guess at track build"
);
}
#[test]
fn progressive_video_omits_field_duration() {
let t = make_video_track(); let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::DEFAULT_DECODED_FIELD_DURATION).is_none(),
"no field duration for progressive content"
);
}
#[test]
fn field_duration_when_set_is_direct_trackentry_child_not_in_video() {
let mut t = make_video_track();
t.interlaced = true;
t.field_order = ebml::FIELD_ORDER_TFF;
t.field_duration_ns = 20_000_000;
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let (te_start, te_size) = first_track_entry(&data);
let te_children = master_children(&data, te_start, te_size);
assert!(
te_children
.iter()
.any(|(id, _, _)| *id == ebml::DEFAULT_DECODED_FIELD_DURATION),
"DefaultDecodedFieldDuration must be a DIRECT child of TrackEntry"
);
let (_, vid_start, vid_size) = te_children
.iter()
.copied()
.find(|(id, _, _)| *id == ebml::VIDEO)
.expect("Video master present");
let vid_children = master_children(&data, vid_start, vid_size as usize);
assert!(
!vid_children
.iter()
.any(|(id, _, _)| *id == ebml::DEFAULT_DECODED_FIELD_DURATION),
"DefaultDecodedFieldDuration must NOT be nested inside the Video master"
);
assert!(
te_children
.iter()
.any(|(id, _, _)| *id == ebml::DEFAULT_DURATION),
"DefaultDuration must be a direct child of TrackEntry"
);
assert!(
!vid_children
.iter()
.any(|(id, _, _)| *id == ebml::DEFAULT_DURATION),
"DefaultDuration must NOT be nested inside the Video master"
);
assert!(
vid_children
.iter()
.any(|(id, _, _)| *id == ebml::FLAG_INTERLACED),
"FlagInterlaced is a Video child"
);
}
fn video_child_u8(data: &[u8], id: u32) -> Option<u8> {
let (te_start, te_size) = first_track_entry(data);
let (_, vid_start, vid_size) = master_children(data, te_start, te_size)
.into_iter()
.find(|(c, _, _)| *c == ebml::VIDEO)?;
let (_, child_start, child_size) = master_children(data, vid_start, vid_size as usize)
.into_iter()
.find(|(c, _, _)| *c == id)?;
(child_size == 1).then(|| data[child_start])
}
#[test]
fn pal_576i_emits_bt470bg_colour_codes() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Mpeg2,
resolution: Resolution::R576i,
frame_rate: crate::disc::FrameRate::F25,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt470bg,
display_aspect: Some((16, 9)),
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert_eq!(
(
t.colour_matrix,
t.colour_transfer,
t.colour_primaries,
t.colour_range
),
(5, 5, 5, 1),
"PAL SD must map to BT.470BG (5,5,5,1)"
);
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let (te_start, te_size) = first_track_entry(&data);
let (_, vid_start, vid_size) = master_children(&data, te_start, te_size)
.into_iter()
.find(|(id, _, _)| *id == ebml::VIDEO)
.expect("Video master");
let (_, col_start, col_size) = master_children(&data, vid_start, vid_size as usize)
.into_iter()
.find(|(id, _, _)| *id == ebml::COLOUR)
.expect("Colour master present for PAL SD");
let col = master_children(&data, col_start, col_size as usize);
let val = |id: u32| -> u8 {
let (_, off, sz) = col.iter().copied().find(|(c, _, _)| *c == id).unwrap();
assert_eq!(sz, 1, "single-byte CICP value");
data[off]
};
assert_eq!(
val(ebml::MATRIX_COEFFICIENTS),
5,
"PAL matrix = BT.470BG (5)"
);
assert_eq!(
val(ebml::TRANSFER_CHARACTERISTICS),
5,
"PAL transfer = BT.470BG (5)"
);
assert_eq!(val(ebml::PRIMARIES), 5, "PAL primaries = BT.470BG (5)");
assert_eq!(val(ebml::RANGE), 1, "PAL range = limited (1)");
}
#[test]
fn ntsc_480i_emits_smpte170m_colour_codes() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Mpeg2,
resolution: Resolution::R480i,
frame_rate: crate::disc::FrameRate::F29_97,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Smpte170m,
display_aspect: Some((4, 3)),
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert_eq!(
(
t.colour_matrix,
t.colour_transfer,
t.colour_primaries,
t.colour_range
),
(6, 6, 6, 1),
"NTSC SD must map to SMPTE-170M (6,6,6,1)"
);
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let (te_start, te_size) = first_track_entry(&data);
let (_, vid_start, vid_size) = master_children(&data, te_start, te_size)
.into_iter()
.find(|(id, _, _)| *id == ebml::VIDEO)
.expect("Video master");
let (_, col_start, col_size) = master_children(&data, vid_start, vid_size as usize)
.into_iter()
.find(|(id, _, _)| *id == ebml::COLOUR)
.expect("Colour master present for NTSC SD");
let col = master_children(&data, col_start, col_size as usize);
let val = |id: u32| -> u8 {
let (_, off, sz) = col.iter().copied().find(|(c, _, _)| *c == id).unwrap();
assert_eq!(sz, 1, "single-byte CICP value");
data[off]
};
assert_eq!(
val(ebml::MATRIX_COEFFICIENTS),
6,
"NTSC matrix = SMPTE-170M (6)"
);
assert_eq!(
val(ebml::TRANSFER_CHARACTERISTICS),
6,
"NTSC transfer = SMPTE-170M (6)"
);
assert_eq!(val(ebml::PRIMARIES), 6, "NTSC primaries = SMPTE-170M (6)");
assert_eq!(val(ebml::RANGE), 1, "NTSC range = limited (1)");
}
#[test]
fn ntsc_480i_duration_metadata_and_field_order_undetermined_at_build() {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Mpeg2,
resolution: Resolution::R480i,
frame_rate: crate::disc::FrameRate::F29_97,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Smpte170m,
display_aspect: Some((4, 3)),
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
assert!(t.interlaced, "480i is interlaced");
assert_eq!(
t.field_order,
ebml::FIELD_ORDER_UNDETERMINED,
"field order is not known at scan time — never guessed at track build"
);
assert_eq!(
t.default_duration_ns, 33_366_666,
"480i frame duration is ~33.37 ms (29.97 fps, not halved)"
);
assert_eq!(
t.field_duration_ns, 0,
"field duration is suppressed (DefaultDecodedFieldDuration omitted — Windows-fps fix)"
);
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert_eq!(
video_child_u8(&data, ebml::FLAG_INTERLACED),
Some(ebml::INTERLACED_INTERLACED as u8),
"480i must encode FlagInterlaced = 1"
);
assert_eq!(
video_child_u8(&data, ebml::FIELD_ORDER),
None,
"FieldOrder omitted until measured — not a hardcoded guess"
);
}
#[test]
fn finalize_emits_per_track_bps_tags() {
let tracks = [make_video_track(), make_audio_track()];
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, &tracks, None, 10.0, &[]).unwrap();
muxer
.write_frame(0, 0, true, &vec![0xABu8; 1000], None)
.unwrap();
muxer
.write_frame(1, 0, false, &vec![0xCDu8; 500], None)
.unwrap();
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
let children = segment_children(&data);
assert!(
children.iter().any(|(id, _, _)| *id == ebml::TAGS),
"Tags element must be written at finalize"
);
let text = String::from_utf8_lossy(&data);
assert!(text.contains("BPS"), "BPS TagName must be present");
assert!(
text.contains("800"),
"video BPS (1000*8/10) must be present"
);
assert!(text.contains("400"), "audio BPS (500*8/10) must be present");
}
#[test]
fn no_bps_tags_when_duration_unknown() {
let tracks = [make_video_track()];
let frames = vec![(0usize, 0i64, true, vec![0xABu8; 1000])];
let (data, _) = mux_to_bytes(&tracks, &[], &frames);
let children = segment_children(&data);
assert!(
!children.iter().any(|(id, _, _)| *id == ebml::TAGS),
"no Tags element when duration is unknown"
);
}
#[test]
fn ac3_channels_corrected_from_bitstream_acmod() {
let mut audio = make_audio_track(); audio.channels = 6;
let video = make_video_track();
let shared = Arc::new(Mutex::new(Cursor::new(Vec::new())));
let writer = SharedWriter(shared.clone());
let mut muxer = MkvMuxer::new(writer, &[video, audio], None, 0.0, &[]).unwrap();
let ac3 = vec![0x0B, 0x77, 0x00, 0x00, 0x00, 8 << 3, 0x40, 0x00];
muxer.write_frame(0, 0, true, &[0x01, 0x02], None).unwrap();
muxer.write_frame(1, 0, false, &ac3, None).unwrap();
muxer.finish().unwrap();
let data = shared.lock().unwrap().clone().into_inner();
let (tracks_start, tracks_size) = segment_children(&data)
.into_iter()
.find_map(|(id, off, sz)| (id == ebml::TRACKS).then_some((off, sz as usize)))
.expect("Tracks element present");
let tracks_body = &data[tracks_start..tracks_start + tracks_size];
let ch = find_id(tracks_body, ebml::CHANNELS).expect("Channels element present");
assert_eq!(
tracks_body[ch + 2],
2,
"Channels must be corrected to 2 (bitstream acmod), not 6 (IFO)"
);
}
#[test]
fn dolby_vision_track_emits_block_addition_mapping() {
let mut dv = make_video_track();
dv.dv_config = Some(dolby_vision_config(7, 6, 0));
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[dv], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
assert!(
find_id(&data, ebml::BLOCK_ADDITION_MAPPING).is_some(),
"DV track must emit BlockAdditionMapping"
);
let muxer = MkvMuxer::new(
Cursor::new(Vec::new()),
&[make_video_track()],
None,
0.0,
&[],
)
.unwrap();
let data = muxer.writer.into_inner();
assert!(find_id(&data, ebml::BLOCK_ADDITION_MAPPING).is_none());
}
fn video_track_with_codec_private(codec: Codec, cp: Vec<u8>) -> MkvTrack {
let v = VideoStream {
pid: 0xE0,
codec,
resolution: Resolution::R1080p,
frame_rate: crate::disc::FrameRate::F23_976,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
};
let mut t = MkvTrack::video(&v);
t.codec_private = Some(cp);
t
}
fn track_entry_child_body(data: &[u8], id: u32) -> Option<&[u8]> {
let (te_start, te_size) = first_track_entry(data);
let (_, body_start, body_size) = master_children(data, te_start, te_size)
.into_iter()
.find(|(c, _, _)| *c == id)?;
Some(&data[body_start..body_start + body_size as usize])
}
#[test]
fn codec_private_emitted_verbatim_for_each_video_codec() {
let cases: [(Codec, &str, Vec<u8>); 4] = [
(
Codec::H264,
ebml::CODEC_H264,
vec![
0x01, 0x64, 0x00, 0x28, 0xFF, 0xE1, 0x00, 0x04, 0x67, 0x64, 0x00, 0x28,
],
),
(
Codec::Hevc,
ebml::CODEC_HEVC,
vec![
0x01, 0x01, 0x60, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A,
],
),
(
Codec::Vc1,
ebml::CODEC_VC1,
vec![
0x28, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00,
],
),
(
Codec::Mpeg2,
ebml::CODEC_MPEG2,
vec![0x00, 0x00, 0x01, 0xB3, 0x14, 0x00, 0xF0, 0xC4, 0x02],
),
];
for (codec, expected_codec_id, cp) in cases {
let t = video_track_with_codec_private(codec, cp.clone());
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let body = track_entry_child_body(&data, ebml::CODEC_PRIVATE).unwrap_or_else(|| {
panic!("{codec:?}: CodecPrivate must be a direct TrackEntry child")
});
assert_eq!(
body,
&cp[..],
"{codec:?}: emitted CodecPrivate must equal the supplied bytes verbatim"
);
let (te_start, te_size) = first_track_entry(&data);
let (_, vid_start, vid_size) = master_children(&data, te_start, te_size)
.into_iter()
.find(|(c, _, _)| *c == ebml::VIDEO)
.expect("Video master present");
assert!(
!master_children(&data, vid_start, vid_size as usize)
.iter()
.any(|(c, _, _)| *c == ebml::CODEC_PRIVATE),
"{codec:?}: CodecPrivate must not be nested in the Video master"
);
let cid = track_entry_child_body(&data, ebml::CODEC_ID)
.expect("CodecID present")
.to_vec();
assert_eq!(
String::from_utf8_lossy(&cid),
*expected_codec_id,
"{codec:?}: wrong CodecID emitted"
);
}
}
#[test]
fn codec_private_omitted_when_none() {
let muxer = MkvMuxer::new(
Cursor::new(Vec::new()),
&[make_audio_track()],
None,
0.0,
&[],
)
.unwrap();
let data = muxer.writer.into_inner();
assert!(
track_entry_child_body(&data, ebml::CODEC_PRIVATE).is_none(),
"no CodecPrivate element when codec_private is None"
);
}
fn emitted_default_duration_ns(frame_rate: crate::disc::FrameRate) -> u64 {
let v = VideoStream {
pid: 0xE0,
codec: Codec::Hevc,
resolution: Resolution::R1080p,
frame_rate,
hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
};
let t = MkvTrack::video(&v);
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[t], None, 0.0, &[]).unwrap();
let data = muxer.writer.into_inner();
let body = track_entry_child_body(&data, ebml::DEFAULT_DURATION)
.expect("DefaultDuration present for a known frame rate");
body.iter().fold(0u64, |acc, &b| (acc << 8) | b as u64)
}
#[test]
fn default_duration_ns_matches_frame_rate_for_all_rates() {
use crate::disc::FrameRate;
let cases = [
(FrameRate::F23_976, 41_708_333u64),
(FrameRate::F24, 41_666_666),
(FrameRate::F25, 40_000_000),
(FrameRate::F29_97, 33_366_666),
(FrameRate::F30, 33_333_333),
(FrameRate::F50, 20_000_000),
(FrameRate::F59_94, 16_683_333),
(FrameRate::F60, 16_666_666),
];
for (fr, expected) in cases {
assert_eq!(
emitted_default_duration_ns(fr),
expected,
"{fr:?}: emitted DefaultDuration ns mismatch"
);
}
}
}