use alloc::vec;
use alloc::vec::Vec;
use broadcast_common::{Package, Serialize};
use crate::error::{Error, Result};
use crate::media::{Media, Track};
use crate::opus::OpusSpecificBox;
use crate::pipeline::CodecConfig;
const ID_EBML: [u8; 4] = [0x1A, 0x45, 0xDF, 0xA3];
const ID_EBML_VERSION: [u8; 2] = [0x42, 0x86];
const ID_EBML_READ_VERSION: [u8; 2] = [0x42, 0xF7];
const ID_EBML_MAX_ID_LENGTH: [u8; 2] = [0x42, 0xF2];
const ID_EBML_MAX_SIZE_LENGTH: [u8; 2] = [0x42, 0xF3];
const ID_DOC_TYPE: [u8; 2] = [0x42, 0x82];
const ID_DOC_TYPE_VERSION: [u8; 2] = [0x42, 0x87];
const ID_DOC_TYPE_READ_VERSION: [u8; 2] = [0x42, 0x85];
const EBML_VERSION_VALUE: u64 = 1;
const EBML_READ_VERSION_VALUE: u64 = 1;
const EBML_MAX_ID_LENGTH_VALUE: u64 = 4;
const EBML_MAX_SIZE_LENGTH_VALUE: u64 = 8;
const DOC_TYPE: &[u8] = b"matroska";
const DOC_TYPE_VERSION_VALUE: u64 = 4;
const DOC_TYPE_READ_VERSION_VALUE: u64 = 2;
const ID_SEGMENT: [u8; 4] = [0x18, 0x53, 0x80, 0x67];
const ID_SEEK_HEAD: [u8; 4] = [0x11, 0x4D, 0x9B, 0x74];
const ID_SEEK: [u8; 2] = [0x4D, 0xBB];
const ID_SEEK_ID: [u8; 2] = [0x53, 0xAB];
const ID_SEEK_POSITION: [u8; 2] = [0x53, 0xAC];
const ID_INFO: [u8; 4] = [0x15, 0x49, 0xA9, 0x66];
const ID_TIMESTAMP_SCALE: [u8; 3] = [0x2A, 0xD7, 0xB1];
const ID_DURATION: [u8; 2] = [0x44, 0x89];
const ID_MUXING_APP: [u8; 2] = [0x4D, 0x80];
const ID_WRITING_APP: [u8; 2] = [0x57, 0x41];
const ID_TRACKS: [u8; 4] = [0x16, 0x54, 0xAE, 0x6B];
const ID_TRACK_ENTRY: [u8; 1] = [0xAE];
const ID_TRACK_NUMBER: [u8; 1] = [0xD7];
const ID_TRACK_UID: [u8; 2] = [0x73, 0xC5];
const ID_TRACK_TYPE: [u8; 1] = [0x83];
const ID_CODEC_ID: [u8; 1] = [0x86];
const ID_CODEC_PRIVATE: [u8; 2] = [0x63, 0xA2];
const ID_VIDEO: [u8; 1] = [0xE0];
const ID_PIXEL_WIDTH: [u8; 1] = [0xB0];
const ID_PIXEL_HEIGHT: [u8; 1] = [0xBA];
const ID_AUDIO: [u8; 1] = [0xE1];
const ID_SAMPLING_FREQUENCY: [u8; 1] = [0xB5];
const ID_CHANNELS: [u8; 1] = [0x9F];
const ID_CLUSTER: [u8; 4] = [0x1F, 0x43, 0xB6, 0x75];
const ID_TIMESTAMP: [u8; 1] = [0xE7];
const ID_SIMPLE_BLOCK: [u8; 1] = [0xA3];
const ID_CUES: [u8; 4] = [0x1C, 0x53, 0xBB, 0x6B];
const ID_CUE_POINT: [u8; 1] = [0xBB];
const ID_CUE_TIME: [u8; 1] = [0xB3];
const ID_CUE_TRACK_POSITIONS: [u8; 1] = [0xB7];
const ID_CUE_TRACK: [u8; 1] = [0xF7];
const ID_CUE_CLUSTER_POSITION: [u8; 1] = [0xF1];
const TRACK_TYPE_VIDEO: u64 = 1;
const TRACK_TYPE_AUDIO: u64 = 2;
const BLOCK_FLAG_KEYFRAME: u8 = 0x80;
const SIMPLE_BLOCK_FIXED_LEN: usize = 2 + 1;
const TIMESTAMP_SCALE_NS: u64 = 1_000_000;
const NS_PER_SECOND: u64 = 1_000_000_000;
const APP_NAME: &[u8] = b"transmux";
const MAX_CLUSTER_SPAN_WITH_VIDEO_TICKS: i64 = 5_000;
const MAX_CLUSTER_SPAN_AUDIO_ONLY_TICKS: i64 = 1_000;
const MIN_BLOCK_REL_TICKS: i64 = i16::MIN as i64;
const MAX_BLOCK_REL_TICKS: i64 = i16::MAX as i64;
const SEEK_POSITION_FIXED_WIDTH: usize = 4;
const CODEC_V_AVC: &[u8] = b"V_MPEG4/ISO/AVC";
const CODEC_V_HEVC: &[u8] = b"V_MPEGH/ISO/HEVC";
const CODEC_V_VP9: &[u8] = b"V_VP9";
const CODEC_V_VP8: &[u8] = b"V_VP8";
const CODEC_V_AV1: &[u8] = b"V_AV1";
const CODEC_A_AAC: &[u8] = b"A_AAC";
const CODEC_A_OPUS: &[u8] = b"A_OPUS";
const CODEC_A_VORBIS: &[u8] = b"A_VORBIS";
const CODEC_A_AC3: &[u8] = b"A_AC3";
const CODEC_A_EAC3: &[u8] = b"A_EAC3";
const OPUS_HEAD_MAGIC: &[u8; 8] = b"OpusHead";
fn encode_vint_size(value: u64) -> Vec<u8> {
let width: usize = if value <= 0x7E {
1
} else if value <= 0x3FFE {
2
} else if value <= 0x1F_FFFE {
3
} else if value <= 0x0FFF_FFFE {
4
} else if value <= 0x07_FFFF_FFFE {
5
} else {
8
};
let marker = 1u64 << (width * 7);
let encoded = marker | value;
encoded.to_be_bytes()[8 - width..].to_vec()
}
fn len_vint(len: usize) -> Vec<u8> {
encode_vint_size(u64::try_from(len).unwrap_or(u64::MAX))
}
fn element(id: &[u8], body: &[u8]) -> Vec<u8> {
let size = encode_vint_size(body.len() as u64);
let mut out = Vec::with_capacity(id.len() + size.len() + body.len());
out.extend_from_slice(id);
out.extend_from_slice(&size);
out.extend_from_slice(body);
out
}
fn master(id: &[u8], children: &[Vec<u8>]) -> Vec<u8> {
let body_len: usize = children.iter().map(Vec::len).sum();
let size = len_vint(body_len);
let mut out = Vec::with_capacity(id.len() + size.len() + body_len);
out.extend_from_slice(id);
out.extend_from_slice(&size);
for child in children {
out.extend_from_slice(child);
}
out
}
fn uint_bytes(value: u64) -> Vec<u8> {
if value == 0 {
return vec![0u8];
}
let full = value.to_be_bytes();
let first_nonzero = full.iter().position(|&b| b != 0).unwrap_or(7);
full[first_nonzero..].to_vec()
}
fn uint_elem(id: &[u8], value: u64) -> Vec<u8> {
element(id, &uint_bytes(value))
}
fn fixed_uint_elem(id: &[u8], value: u64, width: usize) -> Vec<u8> {
let full = value.to_be_bytes();
element(id, &full[8 - width..])
}
fn string_elem(id: &[u8], s: &[u8]) -> Vec<u8> {
element(id, s)
}
fn binary_elem(id: &[u8], data: &[u8]) -> Vec<u8> {
element(id, data)
}
fn float64_elem(id: &[u8], value: f64) -> Vec<u8> {
element(id, &value.to_be_bytes())
}
fn build_ebml_header() -> Vec<u8> {
master(
&ID_EBML,
&[
uint_elem(&ID_EBML_VERSION, EBML_VERSION_VALUE),
uint_elem(&ID_EBML_READ_VERSION, EBML_READ_VERSION_VALUE),
uint_elem(&ID_EBML_MAX_ID_LENGTH, EBML_MAX_ID_LENGTH_VALUE),
uint_elem(&ID_EBML_MAX_SIZE_LENGTH, EBML_MAX_SIZE_LENGTH_VALUE),
string_elem(&ID_DOC_TYPE, DOC_TYPE),
uint_elem(&ID_DOC_TYPE_VERSION, DOC_TYPE_VERSION_VALUE),
uint_elem(&ID_DOC_TYPE_READ_VERSION, DOC_TYPE_READ_VERSION_VALUE),
],
)
}
fn build_info(duration_ticks: f64) -> Vec<u8> {
master(
&ID_INFO,
&[
uint_elem(&ID_TIMESTAMP_SCALE, TIMESTAMP_SCALE_NS),
float64_elem(&ID_DURATION, duration_ticks),
string_elem(&ID_MUXING_APP, APP_NAME),
string_elem(&ID_WRITING_APP, APP_NAME),
],
)
}
fn seek_entry(target_id: &[u8], position: u64) -> Vec<u8> {
master(
&ID_SEEK,
&[
binary_elem(&ID_SEEK_ID, target_id),
fixed_uint_elem(&ID_SEEK_POSITION, position, SEEK_POSITION_FIXED_WIDTH),
],
)
}
fn build_seek_head(pos_info: u64, pos_tracks: u64) -> Vec<u8> {
master(
&ID_SEEK_HEAD,
&[
seek_entry(&ID_INFO, pos_info),
seek_entry(&ID_TRACKS, pos_tracks),
],
)
}
fn video_elem(width: u16, height: u16) -> Vec<u8> {
master(
&ID_VIDEO,
&[
uint_elem(&ID_PIXEL_WIDTH, width as u64),
uint_elem(&ID_PIXEL_HEIGHT, height as u64),
],
)
}
fn audio_elem(channels: u16, sample_rate: u32) -> Vec<u8> {
master(
&ID_AUDIO,
&[
uint_elem(&ID_CHANNELS, channels as u64),
float64_elem(&ID_SAMPLING_FREQUENCY, sample_rate as f64),
],
)
}
fn build_opus_head(dops: &OpusSpecificBox) -> Vec<u8> {
let mut out = Vec::with_capacity(19);
out.extend_from_slice(OPUS_HEAD_MAGIC);
out.push(dops.version);
out.push(dops.output_channel_count);
out.extend_from_slice(&dops.pre_skip.to_le_bytes());
out.extend_from_slice(&dops.input_sample_rate.to_le_bytes());
out.extend_from_slice(&dops.output_gain.to_le_bytes());
out.push(dops.channel_mapping_family);
if let Some(map) = &dops.channel_mapping {
out.push(map.stream_count);
out.push(map.coupled_count);
out.extend_from_slice(&map.channel_mapping);
}
out
}
fn codec_debug_name(config: &CodecConfig) -> &'static str {
match config {
CodecConfig::Avc { .. } => "AVC",
CodecConfig::Hevc { .. } => "HEVC",
CodecConfig::Vvc { .. } => "VVC",
CodecConfig::Aac { .. } => "AAC",
CodecConfig::Ac3 { .. } => "AC-3",
CodecConfig::Eac3 { .. } => "E-AC-3",
CodecConfig::Av1 { .. } => "AV1",
CodecConfig::Vp9 { .. } => "VP9",
CodecConfig::Vp8 { .. } => "VP8",
CodecConfig::Opus { .. } => "Opus",
CodecConfig::Flac { .. } => "FLAC",
CodecConfig::Ac4 { .. } => "AC-4",
CodecConfig::MpegH { .. } => "MPEG-H 3D Audio",
CodecConfig::Mpeg2Video { .. } => "MPEG-2 Video",
CodecConfig::MpegAudio { .. } => "MPEG-1/2 Audio",
CodecConfig::Dts { .. } => "DTS",
CodecConfig::Vorbis { .. } => "Vorbis",
CodecConfig::Subtitle { .. } => "Subtitle",
CodecConfig::Data { .. } => "Data",
}
}
fn build_track_entry(
track_number: u64,
track_id: u32,
config: &CodecConfig,
) -> Result<(Vec<u8>, bool)> {
let is_video = config.is_video();
let track_type = if is_video {
TRACK_TYPE_VIDEO
} else {
TRACK_TYPE_AUDIO
};
let (codec_id, codec_private, sub_elem): (&'static [u8], Option<Vec<u8>>, Vec<u8>) =
match config {
CodecConfig::Avc {
config,
width,
height,
} => (
CODEC_V_AVC,
Some(config.config.to_bytes()),
video_elem(*width, *height),
),
CodecConfig::Hevc {
config,
width,
height,
} => (
CODEC_V_HEVC,
Some(config.config.to_bytes()),
video_elem(*width, *height),
),
CodecConfig::Vp9 { width, height, .. } => {
(CODEC_V_VP9, None, video_elem(*width, *height))
}
CodecConfig::Vp8 { width, height } => (CODEC_V_VP8, None, video_elem(*width, *height)),
CodecConfig::Av1 {
config,
width,
height,
} => (
CODEC_V_AV1,
Some(config.to_bytes()),
video_elem(*width, *height),
),
CodecConfig::Aac {
esds,
channel_count,
sample_rate,
..
} => {
let asc = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.map(|dsi| dsi.data.clone())
.ok_or(Error::InvalidInput(
"AAC esds has no DecoderSpecificInfo (AudioSpecificConfig) to carry as \
Matroska CodecPrivate",
))?;
(
CODEC_A_AAC,
Some(asc),
audio_elem(*channel_count, *sample_rate),
)
}
CodecConfig::Opus {
config,
channel_count,
sample_rate,
..
} => (
CODEC_A_OPUS,
Some(build_opus_head(config)),
audio_elem(*channel_count, *sample_rate),
),
CodecConfig::Vorbis {
codec_private,
channels,
sample_rate,
} => (
CODEC_A_VORBIS,
Some(codec_private.clone()),
audio_elem(*channels, *sample_rate),
),
CodecConfig::Ac3 {
channel_count,
sample_rate,
..
} => (CODEC_A_AC3, None, audio_elem(*channel_count, *sample_rate)),
CodecConfig::Eac3 {
channel_count,
sample_rate,
..
} => (CODEC_A_EAC3, None, audio_elem(*channel_count, *sample_rate)),
other => {
return Err(Error::UnsupportedMkvCodec {
codec: codec_debug_name(other),
});
}
};
let mut children = vec![
uint_elem(&ID_TRACK_NUMBER, track_number),
uint_elem(&ID_TRACK_UID, track_id as u64),
uint_elem(&ID_TRACK_TYPE, track_type),
string_elem(&ID_CODEC_ID, codec_id),
sub_elem,
];
if let Some(priv_bytes) = codec_private {
children.push(binary_elem(&ID_CODEC_PRIVATE, &priv_bytes));
}
Ok((master(&ID_TRACK_ENTRY, &children), is_video))
}
#[derive(Debug, Clone, Copy)]
struct BlockEvent {
track_index: usize,
track_number: u64,
sample_index: usize,
pts_ticks: i64,
is_keyframe: bool,
is_video: bool,
}
fn to_matroska_ticks(ticks: i64, track_timescale: u32, timestamp_scale_ns: u64) -> i64 {
if track_timescale == 0 {
return 0;
}
let ns = (ticks as i128) * (NS_PER_SECOND as i128) / (track_timescale as i128);
(ns / timestamp_scale_ns as i128) as i64
}
fn simple_block_body_len(track_number: u64, data_len: usize) -> usize {
encode_vint_size(track_number).len() + SIMPLE_BLOCK_FIXED_LEN + data_len
}
fn simple_block_len(track_number: u64, data_len: usize) -> usize {
let body = simple_block_body_len(track_number, data_len);
ID_SIMPLE_BLOCK.len() + len_vint(body).len() + body
}
fn write_simple_block(
out: &mut Vec<u8>,
track_number: u64,
rel_ts: i16,
is_keyframe: bool,
data: &[u8],
) {
let body = simple_block_body_len(track_number, data.len());
out.extend_from_slice(&ID_SIMPLE_BLOCK);
out.extend_from_slice(&len_vint(body));
out.extend_from_slice(&encode_vint_size(track_number));
out.extend_from_slice(&rel_ts.to_be_bytes());
out.push(if is_keyframe { BLOCK_FLAG_KEYFRAME } else { 0 });
out.extend_from_slice(data);
}
fn build_clusters(events: &[BlockEvent], has_video: bool) -> Vec<Vec<BlockEvent>> {
let max_span = if has_video {
MAX_CLUSTER_SPAN_WITH_VIDEO_TICKS
} else {
MAX_CLUSTER_SPAN_AUDIO_ONLY_TICKS
};
let mut clusters: Vec<Vec<BlockEvent>> = Vec::new();
let mut current: Vec<BlockEvent> = Vec::new();
let mut cluster_start: i64 = 0;
for &ev in events {
let rel = ev.pts_ticks - cluster_start;
let start_new = current.is_empty()
|| (has_video && ev.is_video && ev.is_keyframe)
|| rel >= max_span
|| !(MIN_BLOCK_REL_TICKS..=MAX_BLOCK_REL_TICKS).contains(&rel);
if start_new && !current.is_empty() {
clusters.push(core::mem::take(&mut current));
}
if current.is_empty() {
cluster_start = ev.pts_ticks.max(0);
}
current.push(ev);
}
if !current.is_empty() {
clusters.push(current);
}
clusters
}
fn build_cluster(events: &[BlockEvent], tracks: &[Track]) -> Result<Vec<u8>> {
let first_ticks = events.first().map(|e| e.pts_ticks).unwrap_or(0);
let base = first_ticks.max(0);
let timestamp = uint_elem(&ID_TIMESTAMP, base as u64);
let mut rels: Vec<i16> = Vec::with_capacity(events.len());
let mut body_len = timestamp.len();
for ev in events {
let rel_i64 = ev.pts_ticks - base;
let rel = i16::try_from(rel_i64).map_err(|_| {
Error::InvalidInput(
"mkv_mux: cluster relative timestamp exceeds the SimpleBlock i16 range",
)
})?;
rels.push(rel);
let data_len = tracks[ev.track_index].samples[ev.sample_index].data.len();
body_len += simple_block_len(ev.track_number, data_len);
}
let size = len_vint(body_len);
let mut out = Vec::with_capacity(ID_CLUSTER.len() + size.len() + body_len);
out.extend_from_slice(&ID_CLUSTER);
out.extend_from_slice(&size);
out.extend_from_slice(×tamp);
for (ev, &rel) in events.iter().zip(&rels) {
let data = &tracks[ev.track_index].samples[ev.sample_index].data;
write_simple_block(&mut out, ev.track_number, rel, ev.is_keyframe, data);
}
Ok(out)
}
fn build_cues(clusters: &[Vec<BlockEvent>], cluster_offsets: &[u64]) -> Vec<u8> {
let mut cue_points: Vec<Vec<u8>> = Vec::new();
let mut last_cue_time: Option<i64> = None;
for (cluster, &offset) in clusters.iter().zip(cluster_offsets) {
for ev in cluster {
if ev.is_video && ev.is_keyframe {
let raw = ev.pts_ticks.max(0);
let cue_time = last_cue_time.map_or(raw, |last| raw.max(last));
last_cue_time = Some(cue_time);
let track_positions = master(
&ID_CUE_TRACK_POSITIONS,
&[
uint_elem(&ID_CUE_TRACK, ev.track_number),
uint_elem(&ID_CUE_CLUSTER_POSITION, offset),
],
);
cue_points.push(master(
&ID_CUE_POINT,
&[uint_elem(&ID_CUE_TIME, cue_time as u64), track_positions],
));
}
}
}
if cue_points.is_empty() {
return Vec::new();
}
master(&ID_CUES, &cue_points)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MkvMux {
_private: (),
}
impl MkvMux {
pub fn new() -> Self {
Self::default()
}
}
impl Package for MkvMux {
type Media = Media;
type Output = Vec<u8>;
type Error = Error;
fn package(&mut self, media: &Media) -> Result<Vec<u8>> {
if media.tracks.is_empty() {
return Err(Error::InvalidInput("cannot package a Media with no tracks"));
}
let mut track_entries: Vec<Vec<u8>> = Vec::with_capacity(media.tracks.len());
let mut is_video_track: Vec<bool> = Vec::with_capacity(media.tracks.len());
for (i, track) in media.tracks.iter().enumerate() {
let track_number = (i + 1) as u64;
let (entry, is_video) =
build_track_entry(track_number, track.spec.track_id, &track.spec.config)?;
track_entries.push(entry);
is_video_track.push(is_video);
}
let has_video = is_video_track.iter().any(|&v| v);
let per_track_ticks: Vec<Vec<i64>> = media
.tracks
.iter()
.map(|track| {
let timescale = track.timescale();
track
.samples
.iter()
.map(|s| {
to_matroska_ticks(
s.pts.or(s.dts).unwrap_or(0),
timescale,
TIMESTAMP_SCALE_NS,
)
})
.collect()
})
.collect();
let mut duration_ticks: i64 = 0;
for track in &media.tracks {
let timescale = track.timescale();
for sample in &track.samples {
let pts = sample.pts.or(sample.dts).unwrap_or(0);
let end_ticks = to_matroska_ticks(
pts + sample.duration.unwrap_or(0) as i64,
timescale,
TIMESTAMP_SCALE_NS,
);
if end_ticks > duration_ticks {
duration_ticks = end_ticks;
}
}
}
let mut cursors = vec![0usize; media.tracks.len()];
let mut events: Vec<BlockEvent> = Vec::new();
loop {
let mut best: Option<(usize, i64)> = None;
for (i, track) in media.tracks.iter().enumerate() {
if cursors[i] < track.samples.len() {
let pts_ticks = per_track_ticks[i][cursors[i]];
if best.is_none_or(|(_, best_ticks)| pts_ticks < best_ticks) {
best = Some((i, pts_ticks));
}
}
}
let Some((i, pts_ticks)) = best else { break };
let si = cursors[i];
cursors[i] += 1;
events.push(BlockEvent {
track_index: i,
track_number: (i + 1) as u64,
sample_index: si,
pts_ticks,
is_keyframe: media.tracks[i].samples[si].flags.is_sync,
is_video: is_video_track[i],
});
}
let info_bytes = build_info(duration_ticks as f64);
let tracks_bytes = master(&ID_TRACKS, &track_entries);
let seekhead_placeholder = build_seek_head(0, 0);
let pos_info = seekhead_placeholder.len() as u64;
let pos_tracks = pos_info + info_bytes.len() as u64;
let seekhead_bytes = build_seek_head(pos_info, pos_tracks);
debug_assert_eq!(
seekhead_bytes.len(),
seekhead_placeholder.len(),
"SeekHead's serialized size must be independent of its SeekPosition values"
);
let clusters = build_clusters(&events, has_video);
let mut cluster_bytes: Vec<Vec<u8>> = Vec::with_capacity(clusters.len());
let mut cluster_offsets: Vec<u64> = Vec::with_capacity(clusters.len());
let mut running_offset = pos_tracks + tracks_bytes.len() as u64;
for cluster in &clusters {
cluster_offsets.push(running_offset);
let bytes = build_cluster(cluster, &media.tracks)?;
running_offset += bytes.len() as u64;
cluster_bytes.push(bytes);
}
let cues_bytes = build_cues(&clusters, &cluster_offsets);
let parts: Vec<&[u8]> = [
seekhead_bytes.as_slice(),
info_bytes.as_slice(),
tracks_bytes.as_slice(),
]
.into_iter()
.chain(cluster_bytes.iter().map(Vec::as_slice))
.chain(core::iter::once(cues_bytes.as_slice()))
.collect();
let payload_len: usize = parts.iter().map(|p| p.len()).sum();
let size = len_vint(payload_len);
let mut out = build_ebml_header();
out.reserve_exact(ID_SEGMENT.len() + size.len() + payload_len);
out.extend_from_slice(&ID_SEGMENT);
out.extend_from_slice(&size);
for part in parts {
out.extend_from_slice(part);
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vint_size_widths() {
assert_eq!(encode_vint_size(0), vec![0x80]);
assert_eq!(encode_vint_size(1), vec![0x81]);
assert_eq!(encode_vint_size(0x7E), vec![0xFE]);
assert_eq!(encode_vint_size(0x7F), vec![0x40, 0x7F]);
assert_eq!(encode_vint_size(2), vec![0x82]);
}
#[test]
fn uint_bytes_widths() {
assert_eq!(uint_bytes(0), vec![0u8]);
assert_eq!(uint_bytes(1), vec![1u8]);
assert_eq!(uint_bytes(1_000_000), vec![0x0F, 0x42, 0x40]);
}
#[test]
fn opus_head_round_trips_via_field_reconstruction() {
let dops = OpusSpecificBox {
version: 1,
output_channel_count: 1,
pre_skip: 312,
input_sample_rate: 48_000,
output_gain: 0,
channel_mapping_family: 0,
channel_mapping: None,
};
let head = build_opus_head(&dops);
assert_eq!(&head[0..8], OPUS_HEAD_MAGIC);
assert_eq!(head[8], 1); assert_eq!(head[9], 1); assert_eq!(u16::from_le_bytes([head[10], head[11]]), 312);
assert_eq!(
u32::from_le_bytes([head[12], head[13], head[14], head[15]]),
48_000
);
}
#[test]
fn backward_step_starts_a_new_cluster_instead_of_overflowing_the_block_timestamp() {
let ev = |pts: i64, video: bool, key: bool| BlockEvent {
track_index: 0,
sample_index: 0,
pts_ticks: pts,
track_number: 1,
is_keyframe: key,
is_video: video,
};
let events = alloc::vec![ev(0, false, false), ev(-40_000, false, false)];
let clusters = build_clusters(&events, false);
assert_eq!(
clusters.len(),
2,
"a backward jump must start a new Cluster"
);
for cluster in &clusters {
let base = cluster[0].pts_ticks;
for e in cluster {
let rel = e.pts_ticks - base;
assert!(
(MIN_BLOCK_REL_TICKS..=MAX_BLOCK_REL_TICKS).contains(&rel),
"relative timestamp {rel} left the i16 range"
);
}
}
}
#[test]
fn negative_first_pts_does_not_shift_the_cluster() {
let file = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/mkv/vp9_opus.mkv");
let Ok(bytes) = std::fs::read(file) else {
eprintln!("SKIP negative_first_pts_does_not_shift_the_cluster: fixture missing");
return;
};
let mut media = crate::webm_demux::WebmDemux::new()
.demux(&bytes)
.expect("demux fixture");
let shift: i64 = -30_000;
for track in &mut media.tracks {
for s in &mut track.samples {
s.dts = s.dts.map(|v| v.saturating_add(shift));
s.pts = s.pts.map(|v| v.saturating_add(shift));
}
track.start_decode_time = track.start_decode_time.saturating_sub(shift.unsigned_abs());
}
let out = MkvMux::new().package(&media).expect("package");
let (cluster_at, id_len, size_len, body_len) =
find_element(&out, &ID_CLUSTER).expect("a Cluster");
let cluster = &out[cluster_at..cluster_at + id_len + size_len + body_len];
let (ts, rels) = cluster_timestamps(cluster);
assert!(ts >= 0, "Cluster Timestamp is unsigned; got {ts}");
assert!(!rels.is_empty(), "the first cluster carries blocks");
assert_eq!(rels[0], -30_000, "the first block keeps its real offset");
assert!(
rels.windows(2).all(|w| w[1] >= w[0] - 1000),
"blocks keep their relative spacing: {rels:?}"
);
let span = rels.iter().max().unwrap() - rels.iter().min().unwrap();
assert!(span > 0, "the cluster must span real time: {rels:?}");
}
fn find_element(data: &[u8], id: &[u8]) -> Option<(usize, usize, usize, usize)> {
for c in 0..data.len().saturating_sub(id.len()) {
if &data[c..c + id.len()] != id {
continue;
}
let Some((size, size_len)) = read_vint_size(data, c + id.len()) else {
continue;
};
if c + id.len() + size_len + size > data.len() {
continue;
}
return Some((c, id.len(), size_len, size));
}
None
}
fn vint_id_len(b: u8) -> Option<usize> {
if b == 0 {
return None;
}
Some(b.leading_zeros() as usize + 1)
}
fn read_vint_size(data: &[u8], off: usize) -> Option<(usize, usize)> {
let width = vint_id_len(*data.get(off)?)?;
if off + width > data.len() {
return None;
}
let mut value = u64::from(data[off]) & (0xFF >> width);
for i in 1..width {
value = (value << 8) | u64::from(data[off + i]);
}
Some((value as usize, width))
}
fn cluster_timestamps(cluster: &[u8]) -> (i64, alloc::vec::Vec<i64>) {
let id_len = vint_id_len(cluster[0]).expect("Cluster id");
let (_, size_len) = read_vint_size(cluster, id_len).expect("Cluster size");
let mut c = id_len + size_len;
let mut timestamp = None;
let mut rels = alloc::vec::Vec::new();
while c < cluster.len() {
let cid_len = vint_id_len(cluster[c]).expect("child id");
let (size, csize_len) = read_vint_size(cluster, c + cid_len).expect("child size");
let body_start = c + cid_len + csize_len;
let body = &cluster[body_start..body_start + size];
c = body_start + size;
if cluster[c - size - csize_len - cid_len..c - size - csize_len] == ID_TIMESTAMP {
let mut v = 0u64;
for &b in body {
v = (v << 8) | u64::from(b);
}
timestamp = Some(v as i64);
} else if cluster[c - size - csize_len - cid_len..c - size - csize_len]
== ID_SIMPLE_BLOCK
{
let tn_len = vint_id_len(body[0]).expect("track number");
let rel = i16::from_be_bytes([body[tn_len], body[tn_len + 1]]) as i64;
rels.push(rel);
}
}
(timestamp.expect("Cluster has a Timestamp"), rels)
}
#[test]
fn negative_timeline_and_backward_jump_are_ffprobe_readable() {
if std::process::Command::new("ffprobe")
.arg("-version")
.output()
.is_err()
{
eprintln!(
"SKIP negative_timeline_and_backward_jump_are_ffprobe_readable: ffprobe not on PATH"
);
return;
}
let file = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/mkv/vp9_opus.mkv");
let Ok(bytes) = std::fs::read(file) else {
eprintln!(
"SKIP negative_timeline_and_backward_jump_are_ffprobe_readable: fixture missing"
);
return;
};
let base = crate::webm_demux::WebmDemux::new()
.demux(&bytes)
.expect("demux fixture");
let source_counts: Vec<usize> = base.tracks.iter().map(|t| t.samples.len()).collect();
for (label, mutate) in [
(
"negative timeline",
&(|m: &mut crate::media::Media| {
for track in &mut m.tracks {
for s in &mut track.samples {
s.dts = s.dts.map(|v| v.saturating_sub(500));
s.pts = s.pts.map(|v| v.saturating_sub(500));
}
}
}) as &dyn Fn(&mut crate::media::Media),
),
(
"backward jump",
&(|m: &mut crate::media::Media| {
let half = m.tracks[0].samples.len() / 2;
for s in &mut m.tracks[0].samples[half..] {
s.dts = s.dts.map(|v| v.saturating_sub(10_000));
s.pts = s.pts.map(|v| v.saturating_sub(10_000));
}
}) as &dyn Fn(&mut crate::media::Media),
),
] {
let mut media = base.clone();
mutate(&mut media);
let out = MkvMux::new().package(&media).expect(label);
let path = std::env::temp_dir().join(format!(
"transmux-h7-{}-{}.mkv",
std::process::id(),
label.replace(' ', "_")
));
std::fs::write(&path, &out).expect("write");
let probe = std::process::Command::new("ffprobe")
.args([
"-v",
"error",
"-show_packets",
"-show_data_hash",
"sha256",
"-show_entries",
"packet=stream_index,pts,data_hash",
"-of",
"csv",
path.to_str().expect("utf-8 path"),
])
.output()
.expect("run ffprobe");
let stdout = String::from_utf8_lossy(&probe.stdout).into_owned();
let stderr = String::from_utf8_lossy(&probe.stderr).into_owned();
let _ = std::fs::remove_file(&path);
assert!(
stderr.is_empty(),
"{label}: ffprobe -v error must be silent, got: {stderr}"
);
let mut counts: alloc::collections::BTreeMap<usize, usize> = Default::default();
let mut last: alloc::collections::BTreeMap<usize, i64> = Default::default();
let mut monotonic = alloc::collections::BTreeMap::new();
let mut hashes: alloc::collections::BTreeMap<usize, alloc::vec::Vec<String>> =
Default::default();
for line in stdout.lines().filter(|l| l.starts_with("packet,")) {
let parts: Vec<&str> = line.split(',').collect();
let stream: usize = parts[1].parse().unwrap_or(usize::MAX);
*counts.entry(stream).or_default() += 1;
if let Some(hash) = parts
.get(3)
.filter(|h| h.to_ascii_uppercase().starts_with("SHA256:"))
{
hashes
.entry(stream)
.or_default()
.push(hash.to_ascii_uppercase());
}
let Some(pts) = parts.get(2).and_then(|p| p.parse::<i64>().ok()) else {
continue;
};
if let Some(prev) = last.get(&stream)
&& pts < *prev
{
*monotonic.entry(stream).or_insert(0usize) += 1;
}
last.insert(stream, pts);
}
for (i, &expect) in source_counts.iter().enumerate() {
assert_eq!(
counts.get(&i).copied().unwrap_or(0),
expect,
"{label}: stream {i} packet count"
);
}
assert!(!counts.is_empty(), "{label}: ffprobe read packets");
let (source_hashes, source_error) = probe_packet_hashes(&bytes);
assert!(
source_error.is_empty(),
"{label}: ffprobe on the fixture must be silent, got: {source_error}"
);
for (stream, got) in &hashes {
let Some(want) = source_hashes.get(stream) else {
continue;
};
assert_eq!(
got.len(),
want.len(),
"{label}: stream {stream} hashed packet count"
);
assert_eq!(
got, want,
"{label}: stream {stream} packet payloads differ from the source"
);
}
let backwards_total: usize = monotonic.values().sum();
assert!(
backwards_total <= 4,
"{label}: {backwards_total} backwards steps is more than the input had"
);
}
}
fn probe_packet_hashes(
data: &[u8],
) -> (
alloc::collections::BTreeMap<usize, alloc::vec::Vec<alloc::string::String>>,
alloc::string::String,
) {
let path = std::env::temp_dir().join(format!(
"transmux-h8-probe-{}-{}.mkv",
std::process::id(),
data.as_ptr() as usize
));
std::fs::write(&path, data).expect("write probe input");
let out = std::process::Command::new("ffprobe")
.args([
"-v",
"error",
"-show_packets",
"-show_data_hash",
"sha256",
"-show_entries",
"packet=stream_index,data_hash",
"-of",
"csv",
path.to_str().expect("utf-8 path"),
])
.output()
.expect("run ffprobe");
let _ = std::fs::remove_file(&path);
let mut by_stream: alloc::collections::BTreeMap<usize, alloc::vec::Vec<_>> =
Default::default();
for line in String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| l.starts_with("packet,"))
{
let parts: Vec<&str> = line.split(',').collect();
let stream: usize = parts[1].parse().unwrap_or(usize::MAX);
if let Some(hash) = parts
.get(2)
.filter(|h| h.to_ascii_uppercase().starts_with("SHA256:"))
{
by_stream
.entry(stream)
.or_default()
.push(hash.to_ascii_uppercase());
}
}
(by_stream, String::from_utf8_lossy(&out.stderr).into_owned())
}
#[test]
fn cluster_with_an_out_of_range_block_is_rejected() {
let file = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/mkv/vp9_opus.mkv");
let Ok(bytes) = std::fs::read(file) else {
eprintln!("SKIP cluster_with_an_out_of_range_block_is_rejected: fixture missing");
return;
};
let media = crate::webm_demux::WebmDemux::new()
.demux(&bytes)
.expect("demux fixture");
let events = alloc::vec![
BlockEvent {
track_index: 0,
sample_index: 0,
pts_ticks: 0,
track_number: 1,
is_keyframe: true,
is_video: true,
},
BlockEvent {
track_index: 0,
sample_index: 0,
pts_ticks: 40_000,
track_number: 1,
is_keyframe: false,
is_video: true,
},
];
let err = build_cluster(&events, &media.tracks)
.expect_err("40 s relative timestamp cannot fit an i16");
assert!(
matches!(err, Error::InvalidInput(_)),
"expected InvalidInput, got {err:?}"
);
}
#[test]
fn multiple_backward_steps_are_ffprobe_readable() {
if std::process::Command::new("ffprobe")
.arg("-version")
.output()
.is_err()
{
eprintln!("SKIP multiple_backward_steps_are_ffprobe_readable: ffprobe not on PATH");
return;
}
let file = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/mkv/vp9_opus.mkv");
let Ok(bytes) = std::fs::read(file) else {
eprintln!("SKIP multiple_backward_steps_are_ffprobe_readable: fixture missing");
return;
};
let base = crate::webm_demux::WebmDemux::new()
.demux(&bytes)
.expect("demux fixture");
let mut media = base.clone();
let n = media.tracks[0].samples.len();
for cut in [n / 4, n / 2, 3 * n / 4] {
if cut < n {
for s in &mut media.tracks[0].samples[cut..] {
s.dts = s.dts.map(|v| v.saturating_sub(5_000));
s.pts = s.pts.map(|v| v.saturating_sub(5_000));
}
}
}
let out = MkvMux::new().package(&media).expect("package");
let (hashes, stderr) = probe_packet_hashes(&out);
assert!(
stderr.is_empty(),
"ffprobe -v error must be silent: {stderr}"
);
let (source, source_err) = probe_packet_hashes(&bytes);
assert!(
source_err.is_empty(),
"ffprobe on the fixture must be silent: {source_err}"
);
for (stream, got) in &hashes {
if let Some(want) = source.get(stream) {
assert_eq!(
got, want,
"stream {stream}: payloads must match the source across several jumps"
);
}
}
}
#[test]
fn cues_stay_monotonic_across_a_backward_jump() {
let ev = |pts: i64, key: bool| BlockEvent {
track_index: 0,
sample_index: 0,
pts_ticks: pts,
track_number: 1,
is_keyframe: key,
is_video: true,
};
let clusters = alloc::vec![
alloc::vec![ev(0, true)],
alloc::vec![ev(5_000, true)],
alloc::vec![ev(2_000, true)],
alloc::vec![ev(9_000, true)],
];
let offsets: alloc::vec::Vec<u64> = alloc::vec![100, 200, 300, 400];
let cues = build_cues(&clusters, &offsets);
let id_len = vint_id_len(cues[0]).expect("Cues id");
let (_, size_len) = read_vint_size(&cues, id_len).expect("Cues size");
let body = &cues[id_len + size_len..];
let mut times: alloc::vec::Vec<u64> = alloc::vec::Vec::new();
let mut c = 0usize;
while c < body.len() {
let cid_len = vint_id_len(body[c]).expect("CuePoint id");
let (size, csize_len) = read_vint_size(body, c + cid_len).expect("CuePoint size");
let bs = c + cid_len + csize_len;
if body[c..c + cid_len] == ID_CUE_POINT {
let inner = &body[bs..bs + size];
let tid_len = vint_id_len(inner[0]).expect("CueTime id");
let (tsize, tsize_len) = read_vint_size(inner, tid_len).expect("CueTime size");
assert_eq!(
&inner[..tid_len],
ID_CUE_TIME,
"a CuePoint must start with its CueTime (RFC 9559 §20)"
);
let mut v = 0u64;
for &b in &inner[tid_len + tsize_len..tid_len + tsize_len + tsize] {
v = (v << 8) | u64::from(b);
}
times.push(v);
}
c = bs + size;
}
assert_eq!(times, alloc::vec![0, 5_000, 5_000, 9_000], "got {times:?}");
assert!(
times.windows(2).all(|w| w[1] >= w[0]),
"CueTimes must be non-decreasing, got {times:?}"
);
}
#[test]
fn empty_media_rejected() {
let media = Media::new(Vec::new(), 1000);
let mut mux = MkvMux::new();
assert!(mux.package(&media).is_err());
}
}