use super::ebml;
use crate::disc::{
AudioChannels, AudioStream, Chapter, Codec, ColorSpace, HdrFormat, Resolution, SampleRate,
SubtitleStream, VideoStream,
};
use std::io::{self, Seek, Write};
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 sample_rate: f64,
pub channels: u8,
pub bit_depth: u8,
pub dv_config: Option<Vec<u8>>,
}
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
}
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) = match v.color_space {
ColorSpace::Bt2020 => (9, 16, 9, 1), ColorSpace::Bt709 => (1, 1, 1, 1), ColorSpace::Unknown => (0, 0, 0, 0),
};
let transfer = match v.hdr {
HdrFormat::Hdr10 | HdrFormat::Hdr10Plus | HdrFormat::DolbyVision => 16, HdrFormat::Hlg => 18,
_ => transfer,
};
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: w,
display_height: h,
colour_matrix: matrix,
colour_transfer: transfer,
colour_primaries: primaries,
colour_range: range,
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
},
}
}
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,
sample_rate: sr,
channels: ch,
bit_depth: 0,
dv_config: 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,
sample_rate: 0.0,
channels: 0,
bit_depth: 0,
dv_config: 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>,
continuity: TimelineContinuity,
cues: Vec<CuePoint>,
frame_count: u64,
dropped_pre_cluster: u64,
seek_fixups: Vec<SeekPositionFixup>,
info_offset: u64,
tracks_offset: u64,
chapters_offset: Option<u64>,
}
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;
const DISCONTINUITY_BACKSTEP_NS: i64 = 3_000_000_000;
const DISCONTINUITY_GAP_NS: i64 = 1_000_000;
struct TimelineContinuity {
offset_ns: i64,
prev_offset_ns: i64,
high_ns: Option<i64>,
}
impl TimelineContinuity {
fn new() -> Self {
Self {
offset_ns: 0,
prev_offset_ns: 0,
high_ns: None,
}
}
fn adjust(&mut self, raw_pts_ns: i64, drives_epoch: bool) -> i64 {
if !drives_epoch {
let mapped = raw_pts_ns.saturating_add(self.offset_ns);
if let Some(high) = self.high_ns {
if mapped > high + DISCONTINUITY_BACKSTEP_NS {
let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns);
if prev_mapped <= high {
return prev_mapped;
}
}
}
return mapped;
}
let Some(high) = self.high_ns else {
let adj = raw_pts_ns.saturating_add(self.offset_ns);
self.high_ns = Some(adj);
return adj;
};
let adj = raw_pts_ns.saturating_add(self.offset_ns);
if adj < high - DISCONTINUITY_BACKSTEP_NS {
self.prev_offset_ns = self.offset_ns;
let bump = (high - adj).saturating_add(DISCONTINUITY_GAP_NS);
self.offset_ns = self.offset_ns.saturating_add(bump);
let adj2 = raw_pts_ns.saturating_add(self.offset_ns);
self.high_ns = Some(high.max(adj2));
adj2
} else {
self.high_ns = Some(high.max(adj));
adj
}
}
}
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();
for target_id in &targets {
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)?;
}
const FREEMKV_MUX_APP: &str = concat!("freemkv ", env!("CARGO_PKG_VERSION"));
ebml::write_string(&mut writer, ebml::MUXING_APP, FREEMKV_MUX_APP)?;
ebml::write_string(&mut writer, ebml::WRITING_APP, FREEMKV_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)?;
for (i, track) in tracks.iter().enumerate() {
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, (i + 1) as u64 | 0x100_0000)?;
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.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)?;
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 {
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)?;
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, 0x6476_6343)?;
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_uint(&mut writer, ebml::CHANNELS, track.channels as u64)?;
}
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(),
continuity: TimelineContinuity::new(),
cues: Vec::new(),
frame_count: 0,
dropped_pre_cluster: 0,
seek_fixups,
info_offset,
tracks_offset,
chapters_offset,
})
}
pub fn write_frame(
&mut self,
track_idx: usize,
pts_ns: i64,
keyframe: bool,
data: &[u8],
duration_ns: Option<u64>,
) -> io::Result<()> {
let is_video = self.track_is_video.get(track_idx).copied().unwrap_or(false);
let drives_epoch = track_idx == 0;
let pts_ns = self.continuity.adjust(pts_ns, drives_epoch);
let raw_ticks = pts_ns / TIMESTAMP_SCALE_NS;
let is_video_key = keyframe && track_idx == 0;
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;
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;
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)?;
}
for fixup in &self.seek_fixups {
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())?;
}
self.writer.seek(std::io::SeekFrom::End(0))?;
self.writer.flush()?;
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;
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,
sample_rate: 0.0,
channels: 0,
bit_depth: 0,
dv_config: 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,
sample_rate: 48000.0,
channels: 6,
bit_depth: 0,
dv_config: 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 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);
}
const S: i64 = 1_000_000_000;
fn adj_video(tc: &mut TimelineContinuity, p: i64) -> i64 {
tc.adjust(p, true)
}
fn adj_other(tc: &mut TimelineContinuity, p: i64) -> i64 {
tc.adjust(p, false)
}
#[test]
fn continuity_rebases_clip_boundary_reset() {
let clip1: Vec<i64> = (0..=10).map(|i| i * S).collect(); let clip2: Vec<i64> = (0..=10).map(|i| i * S).collect(); let raw: Vec<i64> = clip1.iter().chain(clip2.iter()).copied().collect();
assert!(
raw.windows(2).any(|w| w[1] < w[0]),
"precondition: raw clip-reset sequence is non-monotonic"
);
let mut tc = TimelineContinuity::new();
let out: Vec<i64> = raw.iter().map(|&p| adj_video(&mut tc, p)).collect();
assert!(
out.windows(2).all(|w| w[1] >= w[0]),
"corrected timeline must be monotonic non-decreasing, got {out:?}"
);
assert_eq!(out[11], 10 * S + DISCONTINUITY_GAP_NS);
assert!(out[21] > 19 * S);
}
#[test]
fn continuity_preserves_bframe_reorder() {
let mut tc = TimelineContinuity::new();
let raw = [0i64, 125_000_000, 42_000_000, 83_000_000, 250_000_000];
let out: Vec<i64> = raw.iter().map(|&p| adj_video(&mut tc, p)).collect();
assert_eq!(out, raw, "B-frame reorder must pass through unchanged");
assert_eq!(tc.offset_ns, 0, "no rebase for sub-threshold reorder");
}
#[test]
fn continuity_preserves_forward_gap() {
let mut tc = TimelineContinuity::new();
let raw = [0i64, S, 2 * S + 500_000_000, 4 * S]; let out: Vec<i64> = raw.iter().map(|&p| adj_video(&mut tc, p)).collect();
assert_eq!(out, raw, "forward gap preserved verbatim");
assert_eq!(tc.offset_ns, 0, "no rebase on forward progression");
}
#[test]
fn single_clip_late_subtitle_does_not_inflate_offset() {
let mut tc = TimelineContinuity::new();
let mut max_out = i64::MIN;
for sec in 0..=60 {
let v = adj_video(&mut tc, sec * S);
max_out = max_out.max(v);
if sec % 7 == 0 && sec >= 7 {
let sub_raw = (sec - 5) * S;
let s = adj_other(&mut tc, sub_raw);
assert_eq!(s, sub_raw, "subtitle rides the current offset");
}
}
assert_eq!(
tc.offset_ns, 0,
"single-clip interleave must not ratchet offset (was {})",
tc.offset_ns
);
assert_eq!(tc.high_ns, Some(60 * S), "frontier tracks video only");
assert!(max_out <= 60 * S, "no timeline inflation, max={max_out}");
}
#[test]
fn dv_enhancement_layer_does_not_drive_epochs() {
let mut tc = TimelineContinuity::new();
let mut max_out = i64::MIN;
for sec in 0..=60 {
let bl = adj_video(&mut tc, sec * S);
let el_raw = if sec > 0 { (sec - 1) * S } else { 0 };
let el = adj_other(&mut tc, el_raw);
assert_eq!(el, el_raw, "EL rides current offset, true PTS preserved");
max_out = max_out.max(bl).max(el);
}
assert_eq!(
tc.offset_ns, 0,
"DV EL interleave must not ratchet offset (was {})",
tc.offset_ns
);
assert_eq!(tc.high_ns, Some(60 * S), "frontier tracks base video only");
assert!(max_out <= 60 * S, "no timeline inflation, max={max_out}");
}
#[test]
fn non_video_never_advances_frontier() {
let mut tc = TimelineContinuity::new();
adj_video(&mut tc, 0);
adj_video(&mut tc, 5 * S);
let frontier = tc.high_ns.unwrap();
let s = adj_other(&mut tc, 25 * S);
assert_eq!(s, 25 * S, "non-video maps under current offset");
assert_eq!(
tc.high_ns.unwrap(),
frontier,
"non-video must NOT advance the frontier"
);
let v = adj_video(&mut tc, 6 * S);
assert_eq!(v, 6 * S, "video continues normally, no false boundary");
assert_eq!(
tc.offset_ns, 0,
"no rebase triggered by the leading subtitle"
);
}
#[test]
fn continuity_large_clip_boundary_backjump_rebased() {
let mut tc = TimelineContinuity::new();
let clip1: Vec<i64> = (0..=780).map(|i| i * S).collect();
let clip2: Vec<i64> = (0..=120).map(|i| i * S).collect();
let mut last = i64::MIN;
let mut max = i64::MIN;
for &p in clip1.iter().chain(clip2.iter()) {
let a = adj_video(&mut tc, p);
assert!(
a >= last,
"rebased timeline must be monotonic, got {a} < {last}"
);
last = a;
max = max.max(a);
}
assert_eq!(tc.offset_ns, 780 * S + DISCONTINUITY_GAP_NS);
assert!(
(900 * S..901 * S).contains(&max),
"timeline must span ~900s (clip1+clip2), got {max}"
);
}
#[test]
fn non_video_straggler_remapped_to_seam_at_boundary() {
let mut tc = TimelineContinuity::new();
for i in 0..=600 {
adj_video(&mut tc, i * S);
}
let frontier = tc.high_ns.unwrap();
assert_eq!(frontier, 600 * S);
let c2 = adj_video(&mut tc, 0);
assert_eq!(c2, 600 * S + DISCONTINUITY_GAP_NS);
let straggler_raw = 599 * S + 500_000_000;
let straggler = adj_other(&mut tc, straggler_raw);
assert_eq!(
straggler, straggler_raw,
"straggler must remap to its seam position via the previous offset"
);
assert!(
straggler <= frontier,
"straggler must land at/below the frontier, got {straggler}"
);
assert_eq!(
tc.high_ns.unwrap(),
c2,
"straggler must not move the frontier"
);
let normal = adj_other(&mut tc, S);
assert_eq!(normal, S + 600 * S + DISCONTINUITY_GAP_NS);
}
#[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 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 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_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 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 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());
}
}