use std::io::Write;
use oxideav_core::{Error, MediaType, Packet, Result, StreamInfo};
use oxideav_core::{Muxer, WriteSeek};
use crate::codec_id;
use crate::ebml::{write_element_id, write_vint, VINT_UNKNOWN_SIZE};
use crate::ids;
const CLUSTER_DURATION_MS: i64 = 5_000;
pub fn open(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
MkvMuxer::new(output, streams, DocType::Matroska).map(|m| Box::new(m) as Box<dyn Muxer>)
}
pub fn open_webm(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
MkvMuxer::new(output, streams, DocType::Webm).map(|m| Box::new(m) as Box<dyn Muxer>)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DocType {
Matroska,
Webm,
}
impl DocType {
fn as_str(self) -> &'static str {
match self {
DocType::Matroska => "matroska",
DocType::Webm => "webm",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LacingMode {
#[default]
None,
Xiph,
Ebml,
FixedSize,
}
impl LacingMode {
fn flag_bits(self) -> u8 {
match self {
LacingMode::None => 0b00,
LacingMode::Xiph => 0b01,
LacingMode::FixedSize => 0b10,
LacingMode::Ebml => 0b11,
}
}
}
pub struct MkvMuxer {
output: Box<dyn WriteSeek>,
streams: Vec<StreamInfo>,
track_numbers: Vec<u64>,
stream_pts: Vec<i64>,
cluster_open: bool,
cluster_timecode_ms: i64,
cluster_offset_rel: u64,
cluster_body_start_abs: u64,
segment_data_start: u64,
cues: Vec<CueRecord>,
cue_seen_in_cluster: Vec<bool>,
seek_cues_entry_offset: u64,
seek_head_written: bool,
header_written: bool,
trailer_written: bool,
doc_type: DocType,
chapters: Vec<MkvChapter>,
lacing_mode: LacingMode,
lace_pending: Vec<LaceBuffer>,
}
#[derive(Clone, Debug, Default)]
struct LaceBuffer {
frames: Vec<Vec<u8>>,
first_timecode_offset: i16,
keyframe: bool,
}
const MAX_FRAMES_PER_LACE: usize = 8;
#[derive(Clone, Debug, Default)]
pub struct MkvChapter {
pub time_start_ns: u64,
pub time_end_ns: Option<u64>,
pub display: Vec<ChapterDisplay>,
}
#[derive(Clone, Debug)]
pub struct ChapterDisplay {
pub title: String,
pub language: String,
pub country: Option<String>,
}
impl ChapterDisplay {
pub fn untitled_in(language: impl Into<String>) -> Self {
Self {
title: String::new(),
language: language.into(),
country: None,
}
}
}
#[derive(Clone, Copy, Debug)]
struct CueRecord {
track: u64,
time_ms: u64,
cluster_offset: u64,
relative_position: u64,
}
impl MkvMuxer {
fn new(output: Box<dyn WriteSeek>, streams: &[StreamInfo], doc_type: DocType) -> Result<Self> {
if streams.is_empty() {
return Err(Error::invalid("MKV muxer: need at least one stream"));
}
if doc_type == DocType::Webm {
for (i, s) in streams.iter().enumerate() {
if !codec_id::is_webm_codec(&s.params.codec_id) {
return Err(Error::unsupported(format!(
"WebM muxer: stream {i} uses codec '{}' which is not in the WebM whitelist (allowed: vp8, vp9, av1, vorbis, opus)",
s.params.codec_id.as_str()
)));
}
}
}
let stream_track_numbers: Vec<u64> = (0..streams.len() as u64).map(|i| i + 1).collect();
let n = streams.len();
Ok(MkvMuxer {
output,
streams: streams.to_vec(),
track_numbers: stream_track_numbers,
stream_pts: vec![0i64; n],
cluster_open: false,
cluster_timecode_ms: 0,
cluster_offset_rel: 0,
cluster_body_start_abs: 0,
segment_data_start: 0,
cues: Vec::new(),
cue_seen_in_cluster: vec![false; n],
seek_cues_entry_offset: 0,
seek_head_written: false,
header_written: false,
trailer_written: false,
doc_type,
chapters: Vec::new(),
lacing_mode: LacingMode::None,
lace_pending: vec![LaceBuffer::default(); n],
})
}
pub fn with_block_lacing(&mut self, mode: LacingMode) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: with_block_lacing called after write_header",
));
}
self.lacing_mode = mode;
Ok(self)
}
pub fn block_lacing_mode(&self) -> LacingMode {
self.lacing_mode
}
pub fn add_chapter(
&mut self,
start_time_ns: u64,
end_time_ns: Option<u64>,
title: impl Into<String>,
) -> Result<()> {
self.add_chapter_full(MkvChapter {
time_start_ns: start_time_ns,
time_end_ns: end_time_ns,
display: vec![ChapterDisplay {
title: title.into(),
language: "eng".into(),
country: None,
}],
})
}
pub fn add_chapter_full(&mut self, chapter: MkvChapter) -> Result<()> {
if self.header_written {
return Err(Error::other(
"MKV muxer: add_chapter_full called after write_header",
));
}
if let Some(end) = chapter.time_end_ns {
if end < chapter.time_start_ns {
return Err(Error::invalid(format!(
"MKV muxer: chapter end_time_ns ({end}) < start_time_ns ({})",
chapter.time_start_ns
)));
}
}
self.chapters.push(chapter);
Ok(())
}
pub fn chapters(&self) -> &[MkvChapter] {
&self.chapters
}
pub fn new_matroska(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Self> {
Self::new(output, streams, DocType::Matroska)
}
pub fn new_webm(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Self> {
Self::new(output, streams, DocType::Webm)
}
}
impl Muxer for MkvMuxer {
fn format_name(&self) -> &str {
self.doc_type.as_str()
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("MKV muxer: write_header called twice"));
}
let base_pos = self.output.stream_position().unwrap_or(0);
let mut ebml_body = Vec::new();
write_uint_element(&mut ebml_body, ids::EBML_VERSION, 1);
write_uint_element(&mut ebml_body, ids::EBML_READ_VERSION, 1);
write_uint_element(&mut ebml_body, ids::EBML_MAX_ID_LENGTH, 4);
write_uint_element(&mut ebml_body, ids::EBML_MAX_SIZE_LENGTH, 8);
write_string_element(&mut ebml_body, ids::EBML_DOC_TYPE, self.doc_type.as_str());
write_uint_element(&mut ebml_body, ids::EBML_DOC_TYPE_VERSION, 4);
write_uint_element(&mut ebml_body, ids::EBML_DOC_TYPE_READ_VERSION, 2);
let mut all = Vec::new();
write_master_element(&mut all, ids::EBML_HEADER, &ebml_body);
all.extend_from_slice(&write_element_id(ids::SEGMENT));
all.extend_from_slice(&write_vint(VINT_UNKNOWN_SIZE, 0));
let segment_data_start_in_buf = all.len() as u64;
let seek_head_offset_in_buf = all.len() as u64 - segment_data_start_in_buf;
let seek_head_bytes = build_initial_seek_head();
let seek_head_start_in_buf = all.len();
all.extend_from_slice(&seek_head_bytes);
let info_seek_entry_in_buf = seek_head_start_in_buf + SEEK_HEAD_HEADER_LEN;
let tracks_seek_entry_in_buf = info_seek_entry_in_buf + SEEK_ENTRY_LEN;
let chapters_seek_entry_in_buf = tracks_seek_entry_in_buf + SEEK_ENTRY_LEN;
let cues_seek_entry_in_buf = chapters_seek_entry_in_buf + SEEK_ENTRY_LEN;
debug_assert_eq!(seek_head_bytes.len(), SEEK_HEAD_TOTAL_LEN);
let _ = seek_head_offset_in_buf;
let info_offset_in_buf = all.len() as u64 - segment_data_start_in_buf;
let mut info_body = Vec::new();
write_uint_element(&mut info_body, ids::TIMECODE_SCALE, 1_000_000); write_string_element(&mut info_body, ids::MUXING_APP, "oxideav");
write_string_element(&mut info_body, ids::WRITING_APP, "oxideav");
write_master_element(&mut all, ids::INFO, &info_body);
let tracks_offset_in_buf = all.len() as u64 - segment_data_start_in_buf;
let mut tracks_body = Vec::new();
for (i, s) in self.streams.iter().enumerate() {
let track_number = self.track_numbers[i];
let mut t = Vec::new();
write_uint_element(&mut t, ids::TRACK_NUMBER, track_number);
write_uint_element(&mut t, ids::TRACK_UID, track_number);
let track_type = match s.params.media_type {
MediaType::Audio => ids::TRACK_TYPE_AUDIO,
MediaType::Video => ids::TRACK_TYPE_VIDEO,
MediaType::Subtitle => ids::TRACK_TYPE_SUBTITLE,
_ => 17, };
write_uint_element(&mut t, ids::TRACK_TYPE, track_type);
let flag_lacing = if self.lacing_mode == LacingMode::None {
0
} else {
1
};
write_uint_element(&mut t, ids::FLAG_LACING, flag_lacing);
if let Some(name) = codec_id::to_matroska(&s.params.codec_id) {
write_string_element(&mut t, ids::CODEC_ID, name);
} else {
let raw = format!("X_{}", s.params.codec_id);
write_string_element(&mut t, ids::CODEC_ID, &raw);
}
let cp = encode_codec_private(&s.params.codec_id, &s.params.extradata);
if !cp.is_empty() {
write_bytes_element(&mut t, ids::CODEC_PRIVATE, &cp);
}
if s.params.codec_id.as_str() == "opus" {
let pre_skip_samples = parse_opus_pre_skip(&s.params.extradata);
let codec_delay_ns = pre_skip_samples as u64 * 1_000_000_000 / 48_000;
write_uint_element(&mut t, ids::CODEC_DELAY, codec_delay_ns);
write_uint_element(&mut t, ids::SEEK_PRE_ROLL, 80_000_000);
}
if s.params.media_type == MediaType::Audio {
let mut audio = Vec::new();
if let Some(sr) = s.params.sample_rate {
write_float_element(&mut audio, ids::SAMPLING_FREQUENCY, sr as f64);
}
if let Some(ch) = s.params.channels {
write_uint_element(&mut audio, ids::CHANNELS, ch as u64);
}
if let Some(fmt) = s.params.sample_format {
let bd = (fmt.bytes_per_sample() * 8) as u64;
write_uint_element(&mut audio, ids::BIT_DEPTH, bd);
}
write_master_element(&mut t, ids::AUDIO, &audio);
}
if s.params.media_type == MediaType::Video {
let mut video = Vec::new();
if let Some(w) = s.params.width {
write_uint_element(&mut video, ids::PIXEL_WIDTH, w as u64);
}
if let Some(h) = s.params.height {
write_uint_element(&mut video, ids::PIXEL_HEIGHT, h as u64);
}
write_master_element(&mut t, ids::VIDEO, &video);
}
write_master_element(&mut tracks_body, ids::TRACK_ENTRY, &t);
}
write_master_element(&mut all, ids::TRACKS, &tracks_body);
let chapters_offset_opt: Option<u64> = if self.chapters.is_empty() {
None
} else {
let chapters_offset_in_buf = all.len() as u64 - segment_data_start_in_buf;
let chapters_bytes = build_chapters_element(&self.chapters);
all.extend_from_slice(&chapters_bytes);
Some(chapters_offset_in_buf)
};
write_u64_be_at(
&mut all,
info_seek_entry_in_buf + SEEK_POS_PAYLOAD_OFFSET,
info_offset_in_buf,
);
write_u64_be_at(
&mut all,
tracks_seek_entry_in_buf + SEEK_POS_PAYLOAD_OFFSET,
tracks_offset_in_buf,
);
match chapters_offset_opt {
Some(off) => write_u64_be_at(
&mut all,
chapters_seek_entry_in_buf + SEEK_POS_PAYLOAD_OFFSET,
off,
),
None => {
let void = void_seek_entry();
all[chapters_seek_entry_in_buf..chapters_seek_entry_in_buf + SEEK_ENTRY_LEN]
.copy_from_slice(&void);
}
}
self.segment_data_start = base_pos + segment_data_start_in_buf;
self.seek_cues_entry_offset = base_pos + cues_seek_entry_in_buf as u64;
self.seek_head_written = true;
self.output.write_all(&all)?;
self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
if !self.header_written {
return Err(Error::other("MKV muxer: write_header not called"));
}
let stream_idx = packet.stream_index as usize;
if stream_idx >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: unknown stream index {}",
stream_idx
)));
}
let track_number = self.track_numbers[stream_idx];
let stream_time_base = self.streams[stream_idx].time_base;
let media_type = self.streams[stream_idx].params.media_type;
let codec = self.streams[stream_idx].params.codec_id.as_str().to_owned();
let derived_duration: Option<i64> = match codec.as_str() {
"opus" => opus_packet_duration_samples(&packet.data).map(|s| s as i64),
_ => packet.duration,
};
let effective_pts = match packet.pts {
Some(v) => v,
None => self.stream_pts[stream_idx],
};
if let Some(d) = derived_duration {
self.stream_pts[stream_idx] = effective_pts + d;
} else if packet.pts.is_some() {
self.stream_pts[stream_idx] = effective_pts;
}
let pts_ms = pts_to_ms(effective_pts, stream_time_base);
if self.lacing_mode != LacingMode::None {
for other_idx in 0..self.lace_pending.len() {
if other_idx != stream_idx && !self.lace_pending[other_idx].frames.is_empty() {
self.flush_lace(other_idx)?;
}
}
}
let needs_new_cluster = !self.cluster_open
|| pts_ms - self.cluster_timecode_ms > CLUSTER_DURATION_MS
|| pts_ms - self.cluster_timecode_ms > i16::MAX as i64
|| pts_ms - self.cluster_timecode_ms < 0;
if needs_new_cluster {
if self.lacing_mode != LacingMode::None
&& !self.lace_pending[stream_idx].frames.is_empty()
{
self.flush_lace(stream_idx)?;
}
self.start_cluster(pts_ms)?;
}
let timecode_offset = pts_ms - self.cluster_timecode_ms;
if timecode_offset < i16::MIN as i64 || timecode_offset > i16::MAX as i64 {
return Err(Error::other(
"MKV muxer: packet timecode delta exceeds i16 range",
));
}
let pre_block_pos = self.output.stream_position().unwrap_or(0);
let pre_block_rel = pre_block_pos.saturating_sub(self.cluster_body_start_abs);
if !self.cue_seen_in_cluster[stream_idx] {
let indexable = match media_type {
MediaType::Video => packet.flags.keyframe,
_ => true,
};
if indexable && self.lacing_mode == LacingMode::None {
self.cues.push(CueRecord {
track: track_number,
time_ms: pts_ms.max(0) as u64,
cluster_offset: self.cluster_offset_rel,
relative_position: pre_block_rel,
});
self.cue_seen_in_cluster[stream_idx] = true;
}
}
if self.lacing_mode == LacingMode::None {
let block_bytes = build_simple_block(
track_number,
timecode_offset as i16,
packet.flags.keyframe,
LacingMode::None,
std::slice::from_ref(&packet.data),
);
self.output.write_all(&block_bytes)?;
} else {
self.append_to_lace(stream_idx, timecode_offset as i16, packet)?;
}
Ok(())
}
fn write_trailer(&mut self) -> Result<()> {
if self.trailer_written {
return Ok(());
}
if self.lacing_mode != LacingMode::None {
for i in 0..self.lace_pending.len() {
if !self.lace_pending[i].frames.is_empty() {
self.flush_lace(i)?;
}
}
}
let cues_offset_rel = self.write_cues()?;
if self.seek_head_written {
self.patch_cues_seek_entry(cues_offset_rel)?;
}
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
impl MkvMuxer {
fn start_cluster(&mut self, timecode_ms: i64) -> Result<()> {
let cluster_abs = self.output.stream_position().unwrap_or(0);
self.cluster_offset_rel = cluster_abs.saturating_sub(self.segment_data_start);
self.output.write_all(&write_element_id(ids::CLUSTER))?;
self.output.write_all(&write_vint(VINT_UNKNOWN_SIZE, 0))?;
self.cluster_body_start_abs = self.output.stream_position().unwrap_or(0);
let mut tc = Vec::new();
write_uint_element(&mut tc, ids::TIMECODE, timecode_ms.max(0) as u64);
self.output.write_all(&tc)?;
self.cluster_timecode_ms = timecode_ms.max(0);
self.cluster_open = true;
for s in self.cue_seen_in_cluster.iter_mut() {
*s = false;
}
Ok(())
}
fn write_cues(&mut self) -> Result<Option<u64>> {
if self.cues.is_empty() {
return Ok(None);
}
let mut by_time: std::collections::BTreeMap<u64, Vec<CueRecord>> =
std::collections::BTreeMap::new();
for c in &self.cues {
by_time.entry(c.time_ms).or_default().push(*c);
}
let mut body = Vec::new();
for (time, entries) in by_time {
let mut cp = Vec::new();
write_uint_element(&mut cp, ids::CUE_TIME, time);
for e in entries {
let mut ctp = Vec::new();
write_uint_element(&mut ctp, ids::CUE_TRACK, e.track);
write_uint_element(&mut ctp, ids::CUE_CLUSTER_POSITION, e.cluster_offset);
write_uint_element(&mut ctp, ids::CUE_RELATIVE_POSITION, e.relative_position);
write_master_element(&mut cp, ids::CUE_TRACK_POSITIONS, &ctp);
}
write_master_element(&mut body, ids::CUE_POINT, &cp);
}
let mut out = Vec::with_capacity(body.len() + 8);
write_master_element(&mut out, ids::CUES, &body);
let cues_abs = self.output.stream_position().unwrap_or(0);
self.output.write_all(&out)?;
Ok(Some(cues_abs.saturating_sub(self.segment_data_start)))
}
fn append_to_lace(
&mut self,
stream_idx: usize,
timecode_offset: i16,
packet: &Packet,
) -> Result<()> {
let must_flush = {
let buf = &self.lace_pending[stream_idx];
if buf.frames.is_empty() {
false
} else {
buf.keyframe != packet.flags.keyframe
|| buf.frames.len() >= MAX_FRAMES_PER_LACE
|| (self.lacing_mode == LacingMode::FixedSize
&& buf.frames[0].len() != packet.data.len())
}
};
if must_flush {
self.flush_lace(stream_idx)?;
}
let buf = &mut self.lace_pending[stream_idx];
if buf.frames.is_empty() {
buf.first_timecode_offset = timecode_offset;
buf.keyframe = packet.flags.keyframe;
}
buf.frames.push(packet.data.clone());
Ok(())
}
fn flush_lace(&mut self, stream_idx: usize) -> Result<()> {
let frames = std::mem::take(&mut self.lace_pending[stream_idx].frames);
if frames.is_empty() {
return Ok(());
}
let track_number = self.track_numbers[stream_idx];
let tc_offset = self.lace_pending[stream_idx].first_timecode_offset;
let keyframe = self.lace_pending[stream_idx].keyframe;
let media_type = self.streams[stream_idx].params.media_type;
let mode = if frames.len() == 1 {
LacingMode::None
} else {
self.lacing_mode
};
if !self.cue_seen_in_cluster[stream_idx] {
let indexable = match media_type {
MediaType::Video => keyframe,
_ => true,
};
if indexable {
let pre_block_pos = self.output.stream_position().unwrap_or(0);
let pre_block_rel = pre_block_pos.saturating_sub(self.cluster_body_start_abs);
let pts_ms = (self.cluster_timecode_ms + tc_offset as i64).max(0) as u64;
self.cues.push(CueRecord {
track: track_number,
time_ms: pts_ms,
cluster_offset: self.cluster_offset_rel,
relative_position: pre_block_rel,
});
self.cue_seen_in_cluster[stream_idx] = true;
}
}
let block_bytes = build_simple_block(track_number, tc_offset, keyframe, mode, &frames);
self.output.write_all(&block_bytes)?;
Ok(())
}
fn patch_cues_seek_entry(&mut self, cues_offset_rel: Option<u64>) -> Result<()> {
use std::io::SeekFrom;
let resume_pos = self.output.stream_position().unwrap_or(0);
match cues_offset_rel {
Some(off) => {
let payload_pos = self.seek_cues_entry_offset + SEEK_POS_PAYLOAD_OFFSET as u64;
self.output.seek(SeekFrom::Start(payload_pos))?;
self.output.write_all(&off.to_be_bytes())?;
}
None => {
self.output
.seek(SeekFrom::Start(self.seek_cues_entry_offset))?;
self.output.write_all(&void_seek_entry())?;
}
}
self.output.seek(SeekFrom::Start(resume_pos))?;
Ok(())
}
}
fn build_simple_block(
track: u64,
tc_offset: i16,
keyframe: bool,
mode: LacingMode,
frames: &[Vec<u8>],
) -> Vec<u8> {
let payload_total: usize = frames.iter().map(|f| f.len()).sum();
let mut body = Vec::with_capacity(4 + payload_total + 8 * frames.len());
body.extend_from_slice(&write_vint(track, 0));
body.extend_from_slice(&tc_offset.to_be_bytes());
let mut flags: u8 = 0;
if keyframe {
flags |= 0x80;
}
flags |= mode.flag_bits() << 1;
body.push(flags);
match mode {
LacingMode::None => {
debug_assert_eq!(
frames.len(),
1,
"no-lacing Block must carry exactly 1 frame"
);
body.extend_from_slice(&frames[0]);
}
LacingMode::Xiph => {
emit_xiph_lacing(&mut body, frames);
}
LacingMode::Ebml => {
emit_ebml_lacing(&mut body, frames);
}
LacingMode::FixedSize => {
emit_fixed_lacing(&mut body, frames);
}
}
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(&write_element_id(ids::SIMPLE_BLOCK));
out.extend_from_slice(&write_vint(body.len() as u64, 0));
out.extend_from_slice(&body);
out
}
fn emit_xiph_lacing(body: &mut Vec<u8>, frames: &[Vec<u8>]) {
debug_assert!(frames.len() >= 2 && frames.len() <= 256);
body.push((frames.len() - 1) as u8);
for f in &frames[..frames.len() - 1] {
let mut remaining = f.len();
while remaining >= 255 {
body.push(0xFF);
remaining -= 255;
}
body.push(remaining as u8);
}
for f in frames {
body.extend_from_slice(f);
}
}
fn emit_fixed_lacing(body: &mut Vec<u8>, frames: &[Vec<u8>]) {
debug_assert!(frames.len() >= 2 && frames.len() <= 256);
debug_assert!(
frames.iter().all(|f| f.len() == frames[0].len()),
"fixed-size lacing requires equal-size frames"
);
body.push((frames.len() - 1) as u8);
for f in frames {
body.extend_from_slice(f);
}
}
fn emit_ebml_lacing(body: &mut Vec<u8>, frames: &[Vec<u8>]) {
debug_assert!(frames.len() >= 2 && frames.len() <= 256);
body.push((frames.len() - 1) as u8);
body.extend_from_slice(&write_vint(frames[0].len() as u64, 0));
let mut prev = frames[0].len() as i64;
for f in &frames[1..frames.len() - 1] {
let cur = f.len() as i64;
let delta = cur - prev;
body.extend_from_slice(&write_signed_vint(delta));
prev = cur;
}
for f in frames {
body.extend_from_slice(f);
}
}
fn write_signed_vint(value: i64) -> Vec<u8> {
for width in 1u8..=8 {
let bias = (1i64 << (7 * width as i64 - 1)) - 1;
let max_pos = 1i64 << (7 * width as i64 - 1);
let min_neg = -(max_pos - 1);
if value >= min_neg && value <= max_pos {
let unsigned = (value + bias) as u64;
return write_vint_fixed(unsigned, width);
}
}
panic!("EBML signed VINT: value {value} out of range");
}
fn write_vint_fixed(value: u64, width: u8) -> Vec<u8> {
assert!((1..=8).contains(&width), "VINT width must be 1..=8");
let payload_bits = 7u32 * width as u32;
if payload_bits < 64 && value >= (1u64 << payload_bits) {
panic!("write_vint_fixed: value {value} exceeds {width}-byte VINT range");
}
let mut out = vec![0u8; width as usize];
out[0] = 1u8 << (8 - width);
let mut v = value;
for i in (0..width as usize).rev() {
out[i] |= (v & 0xFF) as u8;
v >>= 8;
}
out
}
fn pts_to_ms(value: i64, tb: oxideav_core::TimeBase) -> i64 {
let r = tb.as_rational();
if r.den == 0 {
return value;
}
let v = value as i128 * r.num as i128 * 1000;
(v / r.den as i128) as i64
}
fn opus_packet_duration_samples(packet: &[u8]) -> Option<u32> {
if packet.is_empty() {
return None;
}
let toc = packet[0];
let config = toc >> 3;
let frame_size_48k: u32 = match config {
0 | 4 | 8 => 480,
1 | 5 | 9 => 960,
2 | 6 | 10 => 1920,
3 | 7 | 11 => 2880,
12 | 14 => 480,
13 | 15 => 960,
16 | 20 | 24 | 28 => 120,
17 | 21 | 25 | 29 => 240,
18 | 22 | 26 | 30 => 480,
19 | 23 | 27 | 31 => 960,
_ => return None,
};
let n_frames: u32 = match toc & 0x03 {
0 => 1,
1 | 2 => 2,
3 => {
if packet.len() < 2 {
return None;
}
(packet[1] & 0x3F) as u32
}
_ => unreachable!(),
};
Some(frame_size_48k * n_frames)
}
fn parse_opus_pre_skip(extradata: &[u8]) -> u16 {
if extradata.len() < 12 || &extradata[0..8] != b"OpusHead" {
return 0;
}
u16::from_le_bytes([extradata[10], extradata[11]])
}
fn encode_codec_private(codec_id: &oxideav_core::CodecId, extradata: &[u8]) -> Vec<u8> {
match codec_id.as_str() {
"flac" => {
let mut out = Vec::with_capacity(4 + extradata.len());
out.extend_from_slice(b"fLaC");
out.extend_from_slice(extradata);
out
}
_ => extradata.to_vec(),
}
}
fn write_uint_element(buf: &mut Vec<u8>, id: u32, value: u64) {
let n = if value == 0 {
1
} else {
(64 - value.leading_zeros()).div_ceil(8) as usize
};
buf.extend_from_slice(&write_element_id(id));
buf.extend_from_slice(&write_vint(n as u64, 0));
for i in (0..n).rev() {
buf.push(((value >> (i * 8)) & 0xFF) as u8);
}
}
fn write_string_element(buf: &mut Vec<u8>, id: u32, value: &str) {
buf.extend_from_slice(&write_element_id(id));
buf.extend_from_slice(&write_vint(value.len() as u64, 0));
buf.extend_from_slice(value.as_bytes());
}
fn write_bytes_element(buf: &mut Vec<u8>, id: u32, value: &[u8]) {
buf.extend_from_slice(&write_element_id(id));
buf.extend_from_slice(&write_vint(value.len() as u64, 0));
buf.extend_from_slice(value);
}
fn write_float_element(buf: &mut Vec<u8>, id: u32, value: f64) {
buf.extend_from_slice(&write_element_id(id));
buf.extend_from_slice(&write_vint(8, 0));
buf.extend_from_slice(&value.to_be_bytes());
}
fn write_master_element(buf: &mut Vec<u8>, id: u32, body: &[u8]) {
buf.extend_from_slice(&write_element_id(id));
buf.extend_from_slice(&write_vint(body.len() as u64, 0));
buf.extend_from_slice(body);
}
const SEEK_HEAD_HEADER_LEN: usize = 5;
const SEEK_HEAD_ENTRY_COUNT: usize = 4;
const SEEK_HEAD_TOTAL_LEN: usize = SEEK_HEAD_HEADER_LEN + SEEK_HEAD_ENTRY_COUNT * SEEK_ENTRY_LEN;
const SEEK_ENTRY_LEN: usize = 21;
const SEEK_POS_PAYLOAD_OFFSET: usize = 13;
fn build_initial_seek_head() -> Vec<u8> {
let mut body = Vec::with_capacity(SEEK_HEAD_ENTRY_COUNT * SEEK_ENTRY_LEN);
body.extend_from_slice(&seek_entry(ids::INFO, 0));
body.extend_from_slice(&seek_entry(ids::TRACKS, 0));
body.extend_from_slice(&seek_entry(ids::CHAPTERS, 0));
body.extend_from_slice(&seek_entry(ids::CUES, 0));
debug_assert_eq!(body.len(), SEEK_HEAD_ENTRY_COUNT * SEEK_ENTRY_LEN);
let mut out = Vec::with_capacity(SEEK_HEAD_TOTAL_LEN);
write_master_element(&mut out, ids::SEEK_HEAD, &body);
debug_assert_eq!(out.len(), SEEK_HEAD_TOTAL_LEN);
out
}
fn seek_entry(target_id: u32, position: u64) -> Vec<u8> {
let mut body = Vec::with_capacity(SEEK_ENTRY_LEN - 3);
body.extend_from_slice(&write_element_id(ids::SEEK_ID));
body.extend_from_slice(&write_vint(4, 0));
body.extend_from_slice(&target_id.to_be_bytes());
body.extend_from_slice(&write_element_id(ids::SEEK_POSITION));
body.extend_from_slice(&write_vint(8, 0));
body.extend_from_slice(&position.to_be_bytes());
debug_assert_eq!(body.len(), SEEK_ENTRY_LEN - 3);
let mut entry = Vec::with_capacity(SEEK_ENTRY_LEN);
write_master_element(&mut entry, ids::SEEK, &body);
debug_assert_eq!(entry.len(), SEEK_ENTRY_LEN);
entry
}
fn void_seek_entry() -> Vec<u8> {
let mut out = Vec::with_capacity(SEEK_ENTRY_LEN);
out.push(ids::VOID as u8); out.push(0x93); out.resize(SEEK_ENTRY_LEN, 0u8);
debug_assert_eq!(out.len(), SEEK_ENTRY_LEN);
out
}
fn write_u64_be_at(buf: &mut [u8], pos: usize, value: u64) {
buf[pos..pos + 8].copy_from_slice(&value.to_be_bytes());
}
const EDITION_UID_DEFAULT: u64 = 1;
fn build_chapters_element(chapters: &[MkvChapter]) -> Vec<u8> {
let mut edition_body = Vec::new();
write_uint_element(&mut edition_body, ids::EDITION_UID, EDITION_UID_DEFAULT);
for (i, ch) in chapters.iter().enumerate() {
let atom = build_chapter_atom(i as u64 + 1, ch);
write_master_element(&mut edition_body, ids::CHAPTER_ATOM, &atom);
}
let mut chapters_body = Vec::new();
write_master_element(&mut chapters_body, ids::EDITION_ENTRY, &edition_body);
let mut out = Vec::with_capacity(chapters_body.len() + 8);
write_master_element(&mut out, ids::CHAPTERS, &chapters_body);
out
}
fn build_chapter_atom(uid: u64, ch: &MkvChapter) -> Vec<u8> {
let mut body = Vec::new();
write_uint_element(&mut body, ids::CHAPTER_UID, uid);
write_uint_element(&mut body, ids::CHAPTER_TIME_START, ch.time_start_ns);
if let Some(end) = ch.time_end_ns {
write_uint_element(&mut body, ids::CHAPTER_TIME_END, end);
}
for disp in &ch.display {
let mut display_body = Vec::new();
write_string_element(&mut display_body, ids::CHAP_STRING, &disp.title);
write_string_element(&mut display_body, ids::CHAP_LANGUAGE, &disp.language);
if let Some(country) = &disp.country {
write_string_element(&mut display_body, ids::CHAP_COUNTRY, country);
}
write_master_element(&mut body, ids::CHAPTER_DISPLAY, &display_body);
}
body
}