use crate::error::{Error, Result};
use crate::matroska::{DisplayUnit, MatroskaAudio, MatroskaVideo, Projection};
use crate::mkv::mux::bytes::{invalid, refuse};
use crate::mkv::mux::mp4_config::{audio_specific_config, opus_head};
use crate::mkv::mux::mp4_tags::tags_from_mp4;
use crate::mkv::mux::spec::TrackSpec;
use crate::track::{CodecConfig, Language, PixelAspect, SampleEntry, Track, TrackKind};
const OPUS_PRE_ROLL_NS: u64 = 80_000_000;
const UNIT: i32 = 0x10000;
pub(crate) fn spec_from_mp4(track: &Track) -> Result<TrackSpec> {
let entry = match track.sample_entries.as_slice() {
[entry] => entry,
[] => {
return Err(invalid(format!(
"track {} has no sample description and no Matroska facts. Pass a demuxed track",
track.id
)));
}
entries => {
return Err(Error::SampleDescriptionChange {
track_id: track.id,
descriptions: entries.len() as u64,
});
}
};
if track.timebase.num != 1 || track.timebase.den == 0 {
return Err(invalid(format!(
"track {} has timebase {}/{}. An MP4 track counts in 1/timescale seconds",
track.id, track.timebase.num, track.timebase.den
)));
}
let config = entry.codec_config.as_ref();
let missing = |name: &str| {
invalid(format!(
"track {}: sample entry `{}` has no `{name}` box. Remux the source with \
`ffmpeg -c copy`",
track.id, entry.fourcc
))
};
let mut spec = match (&entry.fourcc.0, config) {
(b"avc1" | b"avc3", Some(CodecConfig::Avc(bytes))) => {
video_spec(track, entry, "V_MPEG4/ISO/AVC", Some(bytes.clone()))?
}
(b"avc1" | b"avc3", _) => return Err(missing("avcC")),
(b"hvc1" | b"hev1", Some(CodecConfig::Hevc(bytes))) => {
video_spec(track, entry, "V_MPEGH/ISO/HEVC", Some(bytes.clone()))?
}
(b"hvc1" | b"hev1", _) => return Err(missing("hvcC")),
(b"av01", Some(CodecConfig::Av1(bytes))) => {
video_spec(track, entry, "V_AV1", Some(bytes.clone()))?
}
(b"av01", _) => return Err(missing("av1C")),
(b"vp09", _) => video_spec(track, entry, "V_VP9", None)?,
(b"mp4a", Some(CodecConfig::Esds(bytes))) => {
let private = audio_specific_config(bytes, track.id)?.to_vec();
audio_spec(track, entry, "A_AAC", private)?
}
(b"mp4a", _) => return Err(missing("esds")),
(b"Opus", Some(CodecConfig::Opus(bytes))) => {
let private = opus_head(bytes, track.id)?;
let mut spec = audio_spec(track, entry, "A_OPUS", private)?;
spec.matroska.seek_pre_roll = OPUS_PRE_ROLL_NS;
spec
}
(b"Opus", _) => return Err(missing("dOps")),
(b"tx3g", _) => TrackSpec::new(TrackKind::Subtitle, "S_TEXT/UTF8", track.timebase),
_ => {
return Err(refuse(format!(
"track {} with sample entry `{}`: this muxer maps avc1, avc3, hvc1, hev1, av01, \
vp09, mp4a, Opus and tx3g",
track.id, entry.fourcc
)));
}
};
spec.matroska.language = language_of(track.language);
spec.metadata = tags_from_mp4(&track.metadata)?;
Ok(spec)
}
pub(crate) fn is_tx3g(track: &Track) -> bool {
track.codec().is_some_and(|fourcc| &fourcc.0 == b"tx3g")
}
fn language_of(language: Language) -> String {
language.matroska_code().to_owned()
}
fn video_spec(
track: &Track,
entry: &SampleEntry,
codec_id: &str,
private: Option<Vec<u8>>,
) -> Result<TrackSpec> {
let Some(info) = entry.video else {
return Err(invalid(format!(
"track {}: sample entry `{}` has no visual facts, so its size is unknown. Pass a \
demuxed video track",
track.id, entry.fourcc
)));
};
let width = u64::from(info.width);
let height = u64::from(info.height);
let (display_width, display_height, display_unit) =
display_of(width, height, info.pixel_aspect);
let projection = roll_of(track)?.map(|pose_roll| Projection {
projection_type: 0,
private: None,
pose_yaw: 0.0,
pose_pitch: 0.0,
pose_roll,
});
let mut spec = TrackSpec::new(TrackKind::Video, codec_id, track.timebase);
spec.matroska.codec_private = private;
spec.matroska.video = Some(MatroskaVideo {
pixel_width: width,
pixel_height: height,
display_width,
display_height,
display_unit,
crop: [0; 4],
interlaced: 0,
projection,
colour: info.colour,
extra: Vec::new(),
});
Ok(spec)
}
fn display_of(
width: u64,
height: u64,
aspect: Option<PixelAspect>,
) -> (Option<u64>, Option<u64>, DisplayUnit) {
let Some(aspect) = aspect else {
return (None, None, DisplayUnit::Pixels);
};
let (h, v) = (u64::from(aspect.h_spacing), u64::from(aspect.v_spacing));
if h == v || h == 0 || v == 0 {
return (None, None, DisplayUnit::Pixels);
}
let scaled = width * h;
if scaled.is_multiple_of(v) {
(Some(scaled / v), Some(height), DisplayUnit::Pixels)
} else {
(Some(scaled), Some(height * v), DisplayUnit::AspectRatio)
}
}
fn roll_of(track: &Track) -> Result<Option<f64>> {
let [a, b, _, c, d, ..] = track.matrix;
match (a, b, c, d) {
(UNIT, 0, 0, UNIT) => Ok(None),
(0, b, c, 0) if b == -UNIT && c == UNIT => Ok(Some(90.0)),
(a, 0, 0, d) if a == -UNIT && d == -UNIT => Ok(Some(180.0)),
(0, UNIT, c, 0) if c == -UNIT => Ok(Some(-90.0)),
_ => Err(refuse(format!(
"track {} with `tkhd` matrix {:?}: `ProjectionPoseRoll` holds right-angle rotations \
in this muxer",
track.id, track.matrix
))),
}
}
fn audio_spec(
track: &Track,
entry: &SampleEntry,
codec_id: &str,
private: Vec<u8>,
) -> Result<TrackSpec> {
let Some(info) = entry.audio else {
return Err(invalid(format!(
"track {}: sample entry `{}` has no audio facts. Pass a demuxed audio track",
track.id, entry.fourcc
)));
};
let mut spec = TrackSpec::new(TrackKind::Audio, codec_id, track.timebase);
spec.matroska.codec_private = Some(private);
spec.matroska.audio = Some(MatroskaAudio {
sampling_frequency: f64::from(info.sample_rate),
output_sampling_frequency: None,
channels: u64::from(info.channels),
bit_depth: None,
extra: Vec::new(),
});
Ok(spec)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::edit::EditList;
use crate::track::{Timebase, VideoInfo};
fn track(fourcc: &[u8; 4], kind: TrackKind, timescale: u32) -> Track {
Track {
id: 1,
kind,
handler: None,
timebase: Timebase::from_timescale(timescale),
duration: None,
language: Language::Iso639(*b"und"),
matrix: [UNIT, 0, 0, 0, UNIT, 0, 0, 0, 0x4000_0000],
display_width: 0,
display_height: 0,
sample_entries: vec![SampleEntry {
fourcc: crate::track::FourCc(*fourcc),
raw: Vec::new(),
data_reference_index: 1,
codec_config: None,
video: Some(VideoInfo {
width: 96,
height: 64,
pixel_aspect: None,
colour: None,
}),
audio: None,
}],
edit_list: EditList::default(),
references: Vec::new(),
chapter_track: false,
metadata: Vec::new(),
sample_count: 0,
matroska: None,
}
}
#[test]
fn languages_map_through_the_macintosh_table() {
assert_eq!(language_of(Language::Other(0)), "eng");
assert_eq!(language_of(Language::Other(2)), "ger");
assert_eq!(language_of(Language::Iso639(*b"fra")), "fre");
assert_eq!(language_of(Language::Iso639(*b"fre")), "fre");
assert_eq!(language_of(Language::Other(95)), "und");
assert_eq!(language_of(Language::Other(0x7FFF)), "und");
}
#[test]
fn rotation_maps_right_angles_only() {
let mut video = track(b"vp09", TrackKind::Video, 12_800);
assert_eq!(roll_of(&video).ok(), Some(None));
video.matrix = [0, -UNIT, 0, UNIT, 0, 0, 640 * UNIT, 0, 0x4000_0000];
assert_eq!(roll_of(&video).ok(), Some(Some(90.0)));
video.matrix = [-UNIT, 0, 0, 0, -UNIT, 0, 0, 0, 0x4000_0000];
assert_eq!(roll_of(&video).ok(), Some(Some(180.0)));
video.matrix = [0, UNIT, 0, -UNIT, 0, 0, 0, 0, 0x4000_0000];
assert_eq!(roll_of(&video).ok(), Some(Some(-90.0)));
video.matrix = [UNIT, UNIT, 0, 0, UNIT, 0, 0, 0, 0x4000_0000];
assert!(roll_of(&video).is_err());
}
#[test]
fn pixel_aspect_keeps_an_exact_display_size() {
let pasp = |h_spacing, v_spacing| {
Some(PixelAspect {
h_spacing,
v_spacing,
})
};
assert_eq!(
display_of(96, 64, pasp(4, 3)),
(Some(128), Some(64), DisplayUnit::Pixels)
);
assert_eq!(
display_of(100, 64, pasp(4, 3)),
(Some(400), Some(192), DisplayUnit::AspectRatio)
);
assert_eq!(
display_of(96, 64, pasp(1, 1)),
(None, None, DisplayUnit::Pixels)
);
}
}