use std::io::Write;
use oxideav_core::{Error, MediaType, Packet, Result, StreamInfo};
use oxideav_core::{Muxer, WriteSeek};
use crate::codec_id;
use crate::demux::{
AlphaMode, ChromaSitingHorz, ChromaSitingVert, ColourRange, DisplayUnit, FieldOrder,
FlagInterlaced, MatrixCoefficients, Primaries, ProjectionType, StereoMode,
TransferCharacteristics,
};
use crate::ebml::{crc32_ieee, 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,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MkvTrackAudio {
pub sampling_frequency: Option<f64>,
pub output_sampling_frequency: Option<f64>,
pub channels: Option<u64>,
pub bit_depth: Option<u64>,
}
impl MkvTrackAudio {
pub fn sbr(core_sampling_frequency: f64) -> Self {
MkvTrackAudio {
sampling_frequency: Some(core_sampling_frequency),
output_sampling_frequency: Some(core_sampling_frequency * 2.0),
channels: None,
bit_depth: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MkvTrackTiming {
pub default_duration: Option<u64>,
pub default_decoded_field_duration: Option<u64>,
pub track_timestamp_scale: Option<f64>,
}
impl MkvTrackTiming {
pub fn from_frame_rate(fps: f64) -> Result<Self> {
if !fps.is_finite() || fps <= 0.0 {
return Err(Error::invalid(format!(
"MKV muxer: MkvTrackTiming::from_frame_rate fps must be finite and positive (got {fps})"
)));
}
let ns = (1_000_000_000.0_f64 / fps).round();
if !(ns.is_finite() && ns >= 1.0) {
return Err(Error::invalid(
"MKV muxer: MkvTrackTiming::from_frame_rate frame interval rounds to 0 ns",
));
}
Ok(MkvTrackTiming {
default_duration: Some(ns as u64),
default_decoded_field_duration: None,
track_timestamp_scale: None,
})
}
}
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>,
attachments: Vec<MkvAttachment>,
lacing_mode: LacingMode,
lace_pending: Vec<LaceBuffer>,
video_interlacings: Vec<Option<VideoInterlacingMux>>,
video_stereo_modes: Vec<Option<StereoMode>>,
video_alpha_modes: Vec<Option<AlphaMode>>,
video_geometries: Vec<Option<MkvVideoGeometry>>,
video_uncompressed_fourccs: Vec<Option<[u8; 4]>>,
video_aspect_ratio_types: Vec<Option<u64>>,
video_colours: Vec<Option<MkvVideoColour>>,
video_projections: Vec<Option<MkvProjection>>,
track_audience_flags: Vec<Option<MkvTrackAudienceFlags>>,
max_block_addition_ids: Vec<Option<u64>>,
track_audio: Vec<Option<MkvTrackAudio>>,
track_timing: Vec<Option<MkvTrackTiming>>,
last_block_pts_ms: Vec<Option<i64>>,
}
#[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, Debug, Default)]
pub struct MkvAttachment {
pub filename: String,
pub mime_type: String,
pub data: Vec<u8>,
pub uid: Option<u64>,
pub description: Option<String>,
}
impl MkvAttachment {
pub fn new(
filename: impl Into<String>,
mime_type: impl Into<String>,
data: impl Into<Vec<u8>>,
) -> Self {
Self {
filename: filename.into(),
mime_type: mime_type.into(),
data: data.into(),
uid: None,
description: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct VideoInterlacingMux {
flag: FlagInterlaced,
field_order: Option<FieldOrder>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MkvVideoGeometry {
pub crop_top: u64,
pub crop_bottom: u64,
pub crop_left: u64,
pub crop_right: u64,
pub display_width: Option<u64>,
pub display_height: Option<u64>,
pub display_unit: DisplayUnit,
}
impl MkvVideoGeometry {
pub fn cropped(top: u64, bottom: u64, left: u64, right: u64) -> Self {
Self {
crop_top: top,
crop_bottom: bottom,
crop_left: left,
crop_right: right,
display_width: None,
display_height: None,
display_unit: DisplayUnit::Pixels,
}
}
pub fn aspect_ratio(num: u64, den: u64) -> Self {
Self {
crop_top: 0,
crop_bottom: 0,
crop_left: 0,
crop_right: 0,
display_width: Some(num),
display_height: Some(den),
display_unit: DisplayUnit::DisplayAspectRatio,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MkvVideoColour {
pub matrix_coefficients: MatrixCoefficients,
pub bits_per_channel: u64,
pub chroma_subsampling_horz: Option<u64>,
pub chroma_subsampling_vert: Option<u64>,
pub cb_subsampling_horz: Option<u64>,
pub cb_subsampling_vert: Option<u64>,
pub chroma_siting_horz: ChromaSitingHorz,
pub chroma_siting_vert: ChromaSitingVert,
pub range: ColourRange,
pub transfer_characteristics: TransferCharacteristics,
pub primaries: Primaries,
pub max_cll: Option<u64>,
pub max_fall: Option<u64>,
pub mastering_metadata: Option<MkvMasteringMetadata>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct MkvMasteringMetadata {
pub primary_r_chromaticity_x: Option<f64>,
pub primary_r_chromaticity_y: Option<f64>,
pub primary_g_chromaticity_x: Option<f64>,
pub primary_g_chromaticity_y: Option<f64>,
pub primary_b_chromaticity_x: Option<f64>,
pub primary_b_chromaticity_y: Option<f64>,
pub white_point_chromaticity_x: Option<f64>,
pub white_point_chromaticity_y: Option<f64>,
pub luminance_max: Option<f64>,
pub luminance_min: Option<f64>,
}
impl MkvMasteringMetadata {
pub fn bt2020_d65_hdr10() -> Self {
Self {
primary_r_chromaticity_x: Some(0.708),
primary_r_chromaticity_y: Some(0.292),
primary_g_chromaticity_x: Some(0.170),
primary_g_chromaticity_y: Some(0.797),
primary_b_chromaticity_x: Some(0.131),
primary_b_chromaticity_y: Some(0.046),
white_point_chromaticity_x: Some(0.3127),
white_point_chromaticity_y: Some(0.3290),
luminance_max: Some(1000.0),
luminance_min: Some(0.005),
}
}
}
impl Default for MkvVideoColour {
fn default() -> Self {
Self {
matrix_coefficients: MatrixCoefficients::Unspecified,
bits_per_channel: 0,
chroma_subsampling_horz: None,
chroma_subsampling_vert: None,
cb_subsampling_horz: None,
cb_subsampling_vert: None,
chroma_siting_horz: ChromaSitingHorz::Unspecified,
chroma_siting_vert: ChromaSitingVert::Unspecified,
range: ColourRange::Unspecified,
transfer_characteristics: TransferCharacteristics::Unspecified,
primaries: Primaries::Unspecified,
max_cll: None,
max_fall: None,
mastering_metadata: None,
}
}
}
impl MkvVideoColour {
pub fn bt709() -> Self {
Self {
matrix_coefficients: MatrixCoefficients::BT709,
transfer_characteristics: TransferCharacteristics::BT709,
primaries: Primaries::BT709,
range: ColourRange::Broadcast,
..Self::default()
}
}
pub fn bt2020_pq() -> Self {
Self {
matrix_coefficients: MatrixCoefficients::BT2020NonConstantLuminance,
transfer_characteristics: TransferCharacteristics::BT2100Pq,
primaries: Primaries::BT2020,
range: ColourRange::Full,
bits_per_channel: 10,
..Self::default()
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MkvProjection {
pub projection_type: ProjectionType,
pub private: Option<Vec<u8>>,
pub pose_yaw: f64,
pub pose_pitch: f64,
pub pose_roll: f64,
}
impl MkvProjection {
pub fn equirectangular(private: Vec<u8>) -> Self {
Self {
projection_type: ProjectionType::Equirectangular,
private: Some(private),
..Self::default()
}
}
pub fn rotated(roll_degrees: f64) -> Self {
Self {
pose_roll: roll_degrees,
..Self::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MkvTrackAudienceFlags {
pub forced: Option<bool>,
pub hearing_impaired: Option<bool>,
pub visual_impaired: Option<bool>,
pub text_descriptions: Option<bool>,
pub original: Option<bool>,
pub commentary: Option<bool>,
}
impl MkvTrackAudienceFlags {
pub fn forced_subtitle() -> Self {
Self {
forced: Some(true),
..Self::default()
}
}
pub fn hearing_impaired_track() -> Self {
Self {
hearing_impaired: Some(true),
..Self::default()
}
}
pub fn visual_impaired_track() -> Self {
Self {
visual_impaired: Some(true),
..Self::default()
}
}
pub fn commentary_track() -> Self {
Self {
commentary: Some(true),
..Self::default()
}
}
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MkvBlockAddition {
pub id: u64,
pub data: Vec<u8>,
}
impl MkvBlockAddition {
pub fn new(id: u64, data: Vec<u8>) -> Self {
Self { id, data }
}
pub fn codec_defined(data: Vec<u8>) -> Self {
Self { id: 1, data }
}
}
#[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(),
attachments: Vec::new(),
lacing_mode: LacingMode::None,
lace_pending: vec![LaceBuffer::default(); n],
video_interlacings: vec![None; n],
video_stereo_modes: vec![None; n],
video_alpha_modes: vec![None; n],
video_geometries: vec![None; n],
video_uncompressed_fourccs: vec![None; n],
video_aspect_ratio_types: vec![None; n],
video_colours: vec![None; n],
video_projections: vec![None; n],
track_audience_flags: vec![None; n],
max_block_addition_ids: vec![None; n],
track_audio: vec![None; n],
track_timing: vec![None; n],
last_block_pts_ms: vec![None; 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 set_video_interlacing(
&mut self,
stream_index: usize,
flag: FlagInterlaced,
field_order: Option<FieldOrder>,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_interlacing called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_interlacing stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_interlacing on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
if field_order.is_some() && flag != FlagInterlaced::Interlaced {
return Err(Error::invalid(
"MKV muxer: FieldOrder requires FlagInterlaced::Interlaced per RFC 9559 §5.1.4.1.28.2",
));
}
self.video_interlacings[stream_index] = Some(VideoInterlacingMux { flag, field_order });
Ok(self)
}
pub fn video_interlacing(
&self,
stream_index: usize,
) -> Option<(FlagInterlaced, Option<FieldOrder>)> {
self.video_interlacings
.get(stream_index)?
.map(|m| (m.flag, m.field_order))
}
pub fn set_video_stereo_mode(
&mut self,
stream_index: usize,
mode: StereoMode,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_stereo_mode called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_stereo_mode stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_stereo_mode on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
self.video_stereo_modes[stream_index] = Some(mode);
Ok(self)
}
pub fn video_stereo_mode(&self, stream_index: usize) -> Option<StereoMode> {
*self.video_stereo_modes.get(stream_index)?
}
pub fn set_video_alpha_mode(
&mut self,
stream_index: usize,
mode: AlphaMode,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_alpha_mode called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_alpha_mode stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_alpha_mode on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
self.video_alpha_modes[stream_index] = Some(mode);
Ok(self)
}
pub fn video_alpha_mode(&self, stream_index: usize) -> Option<AlphaMode> {
*self.video_alpha_modes.get(stream_index)?
}
pub fn set_video_geometry(
&mut self,
stream_index: usize,
geometry: MkvVideoGeometry,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_geometry called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_geometry stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_geometry on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
if matches!(geometry.display_width, Some(0)) {
return Err(Error::invalid(
"MKV muxer: set_video_geometry display_width == Some(0) violates RFC 9559 §5.1.4.1.28.12 (range: not 0). Use None to omit the element.",
));
}
if matches!(geometry.display_height, Some(0)) {
return Err(Error::invalid(
"MKV muxer: set_video_geometry display_height == Some(0) violates RFC 9559 §5.1.4.1.28.13 (range: not 0). Use None to omit the element.",
));
}
self.video_geometries[stream_index] = Some(geometry);
Ok(self)
}
pub fn video_geometry(&self, stream_index: usize) -> Option<MkvVideoGeometry> {
*self.video_geometries.get(stream_index)?
}
pub fn set_video_uncompressed_fourcc(
&mut self,
stream_index: usize,
fourcc: [u8; 4],
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_uncompressed_fourcc called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_uncompressed_fourcc stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_uncompressed_fourcc on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
self.video_uncompressed_fourccs[stream_index] = Some(fourcc);
Ok(self)
}
pub fn video_uncompressed_fourcc(&self, stream_index: usize) -> Option<[u8; 4]> {
*self.video_uncompressed_fourccs.get(stream_index)?
}
pub fn set_video_aspect_ratio_type(
&mut self,
stream_index: usize,
value: u64,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_aspect_ratio_type called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_aspect_ratio_type stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_aspect_ratio_type on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
self.video_aspect_ratio_types[stream_index] = Some(value);
Ok(self)
}
pub fn video_aspect_ratio_type(&self, stream_index: usize) -> Option<u64> {
*self.video_aspect_ratio_types.get(stream_index)?
}
pub fn set_video_colour(
&mut self,
stream_index: usize,
colour: MkvVideoColour,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_colour called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_colour stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_colour on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
self.video_colours[stream_index] = Some(colour);
Ok(self)
}
pub fn video_colour(&self, stream_index: usize) -> Option<MkvVideoColour> {
*self.video_colours.get(stream_index)?
}
pub fn set_video_projection(
&mut self,
stream_index: usize,
projection: MkvProjection,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_video_projection called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_video_projection stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"MKV muxer: set_video_projection on stream {stream_index} ({}) — only Video tracks carry a Video master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
self.video_projections[stream_index] = Some(projection);
Ok(self)
}
pub fn video_projection(&self, stream_index: usize) -> Option<&MkvProjection> {
self.video_projections.get(stream_index)?.as_ref()
}
pub fn set_track_audience_flags(
&mut self,
stream_index: usize,
flags: MkvTrackAudienceFlags,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_track_audience_flags called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_track_audience_flags stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
self.track_audience_flags[stream_index] = Some(flags);
Ok(self)
}
pub fn track_audience_flags(&self, stream_index: usize) -> Option<MkvTrackAudienceFlags> {
*self.track_audience_flags.get(stream_index)?
}
pub fn set_max_block_addition_id(
&mut self,
stream_index: usize,
max: u64,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_max_block_addition_id called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_max_block_addition_id stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
self.max_block_addition_ids[stream_index] = Some(max);
Ok(self)
}
pub fn max_block_addition_id(&self, stream_index: usize) -> Option<u64> {
*self.max_block_addition_ids.get(stream_index)?
}
pub fn set_track_audio(
&mut self,
stream_index: usize,
audio: MkvTrackAudio,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_track_audio called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_track_audio stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if self.streams[stream_index].params.media_type != MediaType::Audio {
return Err(Error::invalid(format!(
"MKV muxer: set_track_audio on stream {stream_index} ({}) — only Audio tracks carry an Audio master",
self.streams[stream_index].params.codec_id.as_str()
)));
}
for (name, freq) in [
("sampling_frequency", audio.sampling_frequency),
("output_sampling_frequency", audio.output_sampling_frequency),
] {
if let Some(v) = freq {
if !(v.is_finite() && v > 0.0) {
return Err(Error::invalid(format!(
"MKV muxer: set_track_audio {name} {v} out of range (must be finite and > 0)"
)));
}
}
}
if audio.channels == Some(0) {
return Err(Error::invalid(
"MKV muxer: set_track_audio channels 0 out of range (must be not 0)".to_string(),
));
}
if audio.bit_depth == Some(0) {
return Err(Error::invalid(
"MKV muxer: set_track_audio bit_depth 0 out of range (must be not 0)".to_string(),
));
}
self.track_audio[stream_index] = Some(audio);
Ok(self)
}
pub fn track_audio(&self, stream_index: usize) -> Option<MkvTrackAudio> {
*self.track_audio.get(stream_index)?
}
pub fn set_track_timing(
&mut self,
stream_index: usize,
timing: MkvTrackTiming,
) -> Result<&mut Self> {
if self.header_written {
return Err(Error::other(
"MKV muxer: set_track_timing called after write_header",
));
}
if stream_index >= self.streams.len() {
return Err(Error::invalid(format!(
"MKV muxer: set_track_timing stream_index {stream_index} out of range ({} streams)",
self.streams.len()
)));
}
if timing.default_duration == Some(0) {
return Err(Error::invalid(
"MKV muxer: set_track_timing default_duration 0 out of range (must be not 0)"
.to_string(),
));
}
if timing.default_decoded_field_duration == Some(0) {
return Err(Error::invalid(
"MKV muxer: set_track_timing default_decoded_field_duration 0 out of range (must be not 0)"
.to_string(),
));
}
if let Some(v) = timing.track_timestamp_scale {
if !(v.is_finite() && v > 0.0) {
return Err(Error::invalid(format!(
"MKV muxer: set_track_timing track_timestamp_scale {v} out of range (must be finite and > 0)"
)));
}
}
self.track_timing[stream_index] = Some(timing);
Ok(self)
}
pub fn track_timing(&self, stream_index: usize) -> Option<MkvTrackTiming> {
*self.track_timing.get(stream_index)?
}
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 add_attachment(&mut self, attachment: MkvAttachment) -> Result<()> {
if self.header_written {
return Err(Error::other(
"MKV muxer: add_attachment called after write_header",
));
}
if attachment.filename.is_empty() {
return Err(Error::invalid(
"MKV muxer: attachment FileName is mandatory (RFC 9559 §5.1.6.1.2, minOccurs=1)",
));
}
if attachment.mime_type.is_empty() {
return Err(Error::invalid(
"MKV muxer: attachment FileMediaType is mandatory (RFC 9559 §5.1.6.1.3, minOccurs=1)",
));
}
if attachment.uid == Some(0) {
return Err(Error::invalid(
"MKV muxer: attachment FileUID range: not 0 (RFC 9559 §5.1.6.1.5)",
));
}
self.attachments.push(attachment);
Ok(())
}
pub fn attachments(&self) -> &[MkvAttachment] {
&self.attachments
}
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 attachments_seek_entry_in_buf = chapters_seek_entry_in_buf + SEEK_ENTRY_LEN;
let cues_seek_entry_in_buf = attachments_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_with_crc(&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(af) = self.track_audience_flags[i] {
if let Some(v) = af.forced {
write_uint_element(&mut t, ids::FLAG_FORCED, v as u64);
}
if let Some(v) = af.hearing_impaired {
write_uint_element(&mut t, ids::FLAG_HEARING_IMPAIRED, v as u64);
}
if let Some(v) = af.visual_impaired {
write_uint_element(&mut t, ids::FLAG_VISUAL_IMPAIRED, v as u64);
}
if let Some(v) = af.text_descriptions {
write_uint_element(&mut t, ids::FLAG_TEXT_DESCRIPTIONS, v as u64);
}
if let Some(v) = af.original {
write_uint_element(&mut t, ids::FLAG_ORIGINAL, v as u64);
}
if let Some(v) = af.commentary {
write_uint_element(&mut t, ids::FLAG_COMMENTARY, v as u64);
}
}
if let Some(m) = self.max_block_addition_ids[i] {
write_uint_element(&mut t, ids::MAX_BLOCK_ADDITION_ID, m);
}
if let Some(tm) = self.track_timing[i] {
if let Some(v) = tm.default_duration {
write_uint_element(&mut t, ids::DEFAULT_DURATION, v);
}
if let Some(v) = tm.default_decoded_field_duration {
write_uint_element(&mut t, ids::DEFAULT_DECODED_FIELD_DURATION, v);
}
if let Some(v) = tm.track_timestamp_scale {
write_float_element(&mut t, ids::TRACK_TIMESTAMP_SCALE, v);
}
}
if let Some(lang) = s.params.language.as_deref() {
write_string_element(&mut t, ids::LANGUAGE, lang);
}
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();
let hint = self.track_audio[i];
let sampling_frequency = hint
.and_then(|h| h.sampling_frequency)
.or_else(|| s.params.sample_rate.map(|sr| sr as f64));
if let Some(sf) = sampling_frequency {
write_float_element(&mut audio, ids::SAMPLING_FREQUENCY, sf);
}
if let Some(osf) = hint.and_then(|h| h.output_sampling_frequency) {
write_float_element(&mut audio, ids::OUTPUT_SAMPLING_FREQUENCY, osf);
}
let channels = hint
.and_then(|h| h.channels)
.or_else(|| s.params.channels.map(|ch| ch as u64));
if let Some(ch) = channels {
write_uint_element(&mut audio, ids::CHANNELS, ch);
}
let bit_depth = hint.and_then(|h| h.bit_depth).or_else(|| {
s.params
.sample_format
.map(|fmt| (fmt.bytes_per_sample() * 8) as u64)
});
if let Some(bd) = bit_depth {
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(vi) = self.video_interlacings[i] {
write_uint_element(&mut video, ids::FLAG_INTERLACED, vi.flag.to_raw());
if let Some(fo) = vi.field_order {
write_uint_element(&mut video, ids::FIELD_ORDER, fo.to_raw());
}
}
if let Some(sm) = self.video_stereo_modes[i] {
write_uint_element(&mut video, ids::STEREO_MODE, sm.to_raw());
}
if let Some(am) = self.video_alpha_modes[i] {
write_uint_element(&mut video, ids::ALPHA_MODE, am.to_raw());
}
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);
}
if let Some(g) = self.video_geometries[i] {
if g.crop_top != 0 {
write_uint_element(&mut video, ids::PIXEL_CROP_TOP, g.crop_top);
}
if g.crop_bottom != 0 {
write_uint_element(&mut video, ids::PIXEL_CROP_BOTTOM, g.crop_bottom);
}
if g.crop_left != 0 {
write_uint_element(&mut video, ids::PIXEL_CROP_LEFT, g.crop_left);
}
if g.crop_right != 0 {
write_uint_element(&mut video, ids::PIXEL_CROP_RIGHT, g.crop_right);
}
if let Some(dw) = g.display_width {
write_uint_element(&mut video, ids::DISPLAY_WIDTH, dw);
}
if let Some(dh) = g.display_height {
write_uint_element(&mut video, ids::DISPLAY_HEIGHT, dh);
}
if g.display_unit != DisplayUnit::Pixels {
write_uint_element(&mut video, ids::DISPLAY_UNIT, g.display_unit.to_raw());
}
}
if let Some(art) = self.video_aspect_ratio_types[i] {
write_uint_element(&mut video, ids::ASPECT_RATIO_TYPE, art);
}
if let Some(fourcc) = self.video_uncompressed_fourccs[i] {
write_bytes_element(&mut video, ids::UNCOMPRESSED_FOURCC, &fourcc);
}
if let Some(c) = self.video_colours[i] {
let mut colour = Vec::new();
if c.matrix_coefficients != MatrixCoefficients::Unspecified {
write_uint_element(
&mut colour,
ids::MATRIX_COEFFICIENTS,
c.matrix_coefficients.to_raw(),
);
}
if c.bits_per_channel != 0 {
write_uint_element(&mut colour, ids::BITS_PER_CHANNEL, c.bits_per_channel);
}
if let Some(v) = c.chroma_subsampling_horz {
write_uint_element(&mut colour, ids::CHROMA_SUBSAMPLING_HORZ, v);
}
if let Some(v) = c.chroma_subsampling_vert {
write_uint_element(&mut colour, ids::CHROMA_SUBSAMPLING_VERT, v);
}
if let Some(v) = c.cb_subsampling_horz {
write_uint_element(&mut colour, ids::CB_SUBSAMPLING_HORZ, v);
}
if let Some(v) = c.cb_subsampling_vert {
write_uint_element(&mut colour, ids::CB_SUBSAMPLING_VERT, v);
}
if c.chroma_siting_horz != ChromaSitingHorz::Unspecified {
write_uint_element(
&mut colour,
ids::CHROMA_SITING_HORZ,
c.chroma_siting_horz.to_raw(),
);
}
if c.chroma_siting_vert != ChromaSitingVert::Unspecified {
write_uint_element(
&mut colour,
ids::CHROMA_SITING_VERT,
c.chroma_siting_vert.to_raw(),
);
}
if c.range != ColourRange::Unspecified {
write_uint_element(&mut colour, ids::COLOUR_RANGE, c.range.to_raw());
}
if c.transfer_characteristics != TransferCharacteristics::Unspecified {
write_uint_element(
&mut colour,
ids::TRANSFER_CHARACTERISTICS,
c.transfer_characteristics.to_raw(),
);
}
if c.primaries != Primaries::Unspecified {
write_uint_element(&mut colour, ids::PRIMARIES, c.primaries.to_raw());
}
if let Some(v) = c.max_cll {
write_uint_element(&mut colour, ids::MAX_CLL, v);
}
if let Some(v) = c.max_fall {
write_uint_element(&mut colour, ids::MAX_FALL, v);
}
if let Some(mm) = c.mastering_metadata {
let mut mast = Vec::new();
if let Some(v) = mm.primary_r_chromaticity_x {
write_float_element(&mut mast, ids::PRIMARY_R_CHROMATICITY_X, v);
}
if let Some(v) = mm.primary_r_chromaticity_y {
write_float_element(&mut mast, ids::PRIMARY_R_CHROMATICITY_Y, v);
}
if let Some(v) = mm.primary_g_chromaticity_x {
write_float_element(&mut mast, ids::PRIMARY_G_CHROMATICITY_X, v);
}
if let Some(v) = mm.primary_g_chromaticity_y {
write_float_element(&mut mast, ids::PRIMARY_G_CHROMATICITY_Y, v);
}
if let Some(v) = mm.primary_b_chromaticity_x {
write_float_element(&mut mast, ids::PRIMARY_B_CHROMATICITY_X, v);
}
if let Some(v) = mm.primary_b_chromaticity_y {
write_float_element(&mut mast, ids::PRIMARY_B_CHROMATICITY_Y, v);
}
if let Some(v) = mm.white_point_chromaticity_x {
write_float_element(&mut mast, ids::WHITE_POINT_CHROMATICITY_X, v);
}
if let Some(v) = mm.white_point_chromaticity_y {
write_float_element(&mut mast, ids::WHITE_POINT_CHROMATICITY_Y, v);
}
if let Some(v) = mm.luminance_max {
write_float_element(&mut mast, ids::LUMINANCE_MAX, v);
}
if let Some(v) = mm.luminance_min {
write_float_element(&mut mast, ids::LUMINANCE_MIN, v);
}
write_master_element(&mut colour, ids::MASTERING_METADATA, &mast);
}
write_master_element(&mut video, ids::COLOUR, &colour);
}
if let Some(p) = &self.video_projections[i] {
let mut proj = Vec::new();
if p.projection_type != ProjectionType::Rectangular {
write_uint_element(
&mut proj,
ids::PROJECTION_TYPE,
p.projection_type.to_raw(),
);
}
if let Some(private) = &p.private {
write_bytes_element(&mut proj, ids::PROJECTION_PRIVATE, private);
}
if p.pose_yaw != 0.0 {
write_float_element(&mut proj, ids::PROJECTION_POSE_YAW, p.pose_yaw);
}
if p.pose_pitch != 0.0 {
write_float_element(&mut proj, ids::PROJECTION_POSE_PITCH, p.pose_pitch);
}
if p.pose_roll != 0.0 {
write_float_element(&mut proj, ids::PROJECTION_POSE_ROLL, p.pose_roll);
}
write_master_element(&mut video, ids::PROJECTION, &proj);
}
write_master_element(&mut t, ids::VIDEO, &video);
}
write_master_element(&mut tracks_body, ids::TRACK_ENTRY, &t);
}
write_master_element_with_crc(&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)
};
let attachments_offset_opt: Option<u64> = if self.attachments.is_empty() {
None
} else {
let attachments_offset_in_buf = all.len() as u64 - segment_data_start_in_buf;
let attachments_bytes = build_attachments_element(&self.attachments);
all.extend_from_slice(&attachments_bytes);
Some(attachments_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);
}
}
match attachments_offset_opt {
Some(off) => write_u64_be_at(
&mut all,
attachments_seek_entry_in_buf + SEEK_POS_PAYLOAD_OFFSET,
off,
),
None => {
let void = void_seek_entry();
all[attachments_seek_entry_in_buf..attachments_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<()> {
self.write_packet_inner(packet, None)
}
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 write_packet_inner(
&mut self,
packet: &Packet,
additions: Option<&[MkvBlockAddition]>,
) -> 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 || additions.is_some())
&& !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 || additions.is_some()) {
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 let Some(adds) = additions {
let reference_block = if packet.flags.keyframe {
None
} else {
Some(
self.last_block_pts_ms[stream_idx]
.map(|prev| prev - pts_ms)
.unwrap_or(0),
)
};
let duration_ms = derived_duration
.map(|d| pts_to_ms(d, stream_time_base))
.filter(|d| *d >= 0)
.map(|d| d as u64);
let group = build_block_group(
track_number,
timecode_offset as i16,
&packet.data,
adds,
duration_ms,
reference_block,
);
self.output.write_all(&group)?;
} else 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)?;
}
self.last_block_pts_ms[stream_idx] = Some(pts_ms);
Ok(())
}
pub fn write_packet_with_additions(
&mut self,
packet: &Packet,
additions: &[MkvBlockAddition],
) -> Result<()> {
if additions.is_empty() {
return self.write_packet_inner(packet, None);
}
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 declared = self.max_block_addition_ids[stream_idx].unwrap_or(0);
if declared == 0 {
return Err(Error::invalid(format!(
"MKV muxer: stream {stream_idx} has MaxBlockAdditionID 0 — RFC 9559 §5.1.4.1.16 \
means no BlockAdditions for this track; declare a non-zero maximum via \
set_max_block_addition_id before write_header"
)));
}
for (i, a) in additions.iter().enumerate() {
if a.id == 0 {
return Err(Error::invalid(
"MKV muxer: BlockAddID 0 is out of range — RFC 9559 §5.1.3.5.2.3 ranges the \
element as \"not 0\"",
));
}
if a.id > declared {
return Err(Error::invalid(format!(
"MKV muxer: BlockAddID {} exceeds the track's declared MaxBlockAdditionID {} \
(RFC 9559 §5.1.4.1.16)",
a.id, declared
)));
}
if additions[..i].iter().any(|b| b.id == a.id) {
return Err(Error::invalid(format!(
"MKV muxer: duplicate BlockAddID {} — RFC 9559 §5.1.3.5.2.3 requires each \
value to be unique between the BlockMore elements of one BlockAdditions",
a.id
)));
}
}
self.write_packet_inner(packet, Some(additions))
}
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 + CRC32_CHILD_LEN);
write_master_element_with_crc(&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 build_block_group(
track: u64,
tc_offset: i16,
frame: &[u8],
additions: &[MkvBlockAddition],
duration_ticks: Option<u64>,
reference_block: Option<i64>,
) -> Vec<u8> {
let mut block_body = Vec::with_capacity(4 + frame.len());
block_body.extend_from_slice(&write_vint(track, 0));
block_body.extend_from_slice(&tc_offset.to_be_bytes());
block_body.push(0x00);
block_body.extend_from_slice(frame);
let mut group_body = Vec::new();
write_bytes_element(&mut group_body, ids::BLOCK, &block_body);
let mut additions_body = Vec::new();
for a in additions {
let mut more = Vec::with_capacity(8 + a.data.len());
write_bytes_element(&mut more, ids::BLOCK_ADDITIONAL, &a.data);
if a.id != 1 {
write_uint_element(&mut more, ids::BLOCK_ADD_ID, a.id);
}
write_master_element(&mut additions_body, ids::BLOCK_MORE, &more);
}
write_master_element(&mut group_body, ids::BLOCK_ADDITIONS, &additions_body);
if let Some(d) = duration_ticks {
write_uint_element(&mut group_body, ids::BLOCK_DURATION, d);
}
if let Some(r) = reference_block {
write_int_element(&mut group_body, ids::REFERENCE_BLOCK, r);
}
let mut out = Vec::with_capacity(8 + group_body.len());
write_master_element(&mut out, ids::BLOCK_GROUP, &group_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_int_element(buf: &mut Vec<u8>, id: u32, value: i64) {
let mut n = 1usize;
while n < 8 {
let min = -(1i64 << (8 * n - 1));
let max = (1i64 << (8 * n - 1)) - 1;
if value >= min && value <= max {
break;
}
n += 1;
}
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 CRC32_CHILD_LEN: usize = 6;
fn build_crc32_child(data: &[u8]) -> [u8; CRC32_CHILD_LEN] {
let crc = crc32_ieee(data);
let bytes = crc.to_le_bytes();
[
ids::CRC32 as u8, 0x84, bytes[0],
bytes[1],
bytes[2],
bytes[3],
]
}
fn write_master_element_with_crc(buf: &mut Vec<u8>, id: u32, body: &[u8]) {
let crc = build_crc32_child(body);
let inner_len = CRC32_CHILD_LEN + body.len();
buf.extend_from_slice(&write_element_id(id));
buf.extend_from_slice(&write_vint(inner_len as u64, 0));
buf.extend_from_slice(&crc);
buf.extend_from_slice(body);
}
const SEEK_HEAD_HEADER_LEN: usize = 5;
const SEEK_HEAD_ENTRY_COUNT: usize = 5;
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::ATTACHMENTS, 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 + CRC32_CHILD_LEN);
write_master_element_with_crc(&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
}
fn build_attachments_element(attachments: &[MkvAttachment]) -> Vec<u8> {
let mut attachments_body = Vec::new();
for (i, att) in attachments.iter().enumerate() {
let index = i as u64 + 1;
let file_body = build_attached_file(index, att);
write_master_element(&mut attachments_body, ids::ATTACHED_FILE, &file_body);
}
let mut out = Vec::with_capacity(attachments_body.len() + 8 + CRC32_CHILD_LEN);
write_master_element_with_crc(&mut out, ids::ATTACHMENTS, &attachments_body);
out
}
fn build_attached_file(index: u64, att: &MkvAttachment) -> Vec<u8> {
let mut body = Vec::new();
if let Some(desc) = att.description.as_deref() {
if !desc.is_empty() {
write_string_element(&mut body, ids::FILE_DESCRIPTION, desc);
}
}
write_string_element(&mut body, ids::FILE_NAME, &att.filename);
write_string_element(&mut body, ids::FILE_MIME_TYPE, &att.mime_type);
write_bytes_element(&mut body, ids::FILE_DATA, &att.data);
let uid = att.uid.unwrap_or(index);
write_uint_element(&mut body, ids::FILE_UID, uid);
body
}