use anyhow::{Context, Result, bail};
use codec::frame::{
ColorMetadata, ColorSpace, ContentLightLevel, MasteringDisplay, PixelFormat, StreamInfo,
TransferFn,
};
use matroska_demuxer::{
Colour as MkvColour, Frame as MkvFrame, MasteringMetadata as MkvMastering, MatrixCoefficients,
MatroskaFile, Primaries, Range as MkvRange, TrackType as MkvTrackType, TransferCharacteristics,
};
use mp4::Mp4Reader;
use std::io::Cursor;
use crate::mp4_sanitize::sanitize_isobmff_box_sizes;
use crate::annexb::{
AvcConfig, HevcConfig, NaluCodec, ParamSetTracker, length_prefixed_to_annexb_tracked,
parse_avcc, parse_hvcc,
};
use crate::avi::demux_avi;
use crate::streaming::{DemuxHeader, Sample, StreamingDemuxer};
use crate::ts::demux_ts;
use crate::{MkvColorInfo, MkvMasteringMetadata};
pub struct DemuxResult {
pub codec: String,
pub info: StreamInfo,
pub samples: Vec<Vec<u8>>,
pub audio: Option<AudioTrack>,
}
#[derive(Debug, Clone)]
pub struct AudioTrack {
pub codec: String,
pub samples: Vec<Vec<u8>>,
pub sample_rate: u32,
pub channels: u16,
pub asc: Vec<u8>,
pub codec_private: Vec<u8>,
pub timescale: u32,
pub durations: Vec<u32>,
}
pub fn demux(data: &[u8]) -> Result<DemuxResult> {
match detect_container(data) {
"mp4" => demux_mp4(data),
"mkv" => demux_mkv(data),
"avi" => demux_avi(data),
"ts" => demux_ts(data),
other => bail!("unsupported container: {other}"),
}
}
fn detect_container(data: &[u8]) -> &'static str {
if data.len() < 12 {
return "unknown";
}
if &data[4..8] == b"ftyp" || &data[4..8] == b"moov" || &data[4..8] == b"mdat" {
return "mp4";
}
if data[0] == 0x1A && data[1] == 0x45 && data[2] == 0xDF && data[3] == 0xA3 {
return "mkv";
}
if &data[..4] == b"RIFF" && &data[8..12] == b"AVI " {
return "avi";
}
if data[0] == 0x47
&& data.len() > 188
&& data[188] == 0x47
&& (data.len() <= 376 || data[376] == 0x47)
{
return "ts";
}
"unknown"
}
pub fn demux_mp4(data: &[u8]) -> Result<DemuxResult> {
let sanitized = sanitize_isobmff_box_sizes(data);
let data: &[u8] = &sanitized;
let size = data.len() as u64;
let cursor = Cursor::new(data);
let reader = Mp4Reader::read_header(cursor, size).context("reading MP4 header")?;
let video_track = reader
.tracks()
.values()
.find(|t| t.track_type().ok() == Some(mp4::TrackType::Video))
.context("no video track in MP4")?;
let track_id = video_track.track_id();
let codec_from_mp4 = format_codec(video_track);
let codec = if codec_from_mp4 == "unknown" && has_av01_sample_entry(data) {
"av1".to_string()
} else if codec_from_mp4 == "unknown" && hevc_sample_entry_fourcc(data).is_some() {
"h265".to_string()
} else if codec_from_mp4 == "unknown" && prores_sample_entry_fourcc(data).is_some() {
"prores".to_string()
} else {
codec_from_mp4
};
let width = video_track.width() as u32;
let height = video_track.height() as u32;
let sample_count = video_track.sample_count();
let duration = video_track.duration().as_secs_f64();
let frame_rate = if duration > 0.0 {
sample_count as f64 / duration
} else {
30.0
};
let bitrate = video_track.bitrate() as u64;
let mp4_color = extract_mp4_visual_color_metadata(data);
let initial_color_metadata = ColorMetadata {
mastering_display: mp4_color.mastering_display,
content_light_level: mp4_color.content_light_level,
..Default::default()
};
let info = StreamInfo {
codec: codec.clone(),
width,
height,
frame_rate,
duration,
pixel_format: PixelFormat::Yuv420p,
color_space: ColorSpace::Bt709,
total_frames: sample_count as u64,
bitrate,
color_metadata: initial_color_metadata,
};
let cursor = Cursor::new(data);
let mut reader = Mp4Reader::read_header(cursor, size).context("re-reading MP4 for samples")?;
let mut samples = Vec::with_capacity(sample_count as usize);
let needs_annexb = matches!(codec.as_str(), "h264" | "h265");
let (sps_pps, length_size) = if needs_annexb {
if codec == "h264" {
match extract_avc_config(data) {
Some(cfg) => (cfg.parameter_sets, cfg.length_size),
None => (extract_sps_pps(&reader, track_id), 4u8),
}
} else {
match extract_hevc_config(data) {
Some(cfg) => (cfg.parameter_sets, cfg.length_size),
None => (Vec::new(), 4u8),
}
}
} else {
(Vec::new(), 4u8)
};
let mut avc_tracker = if needs_annexb {
Some(ParamSetTracker::new(if codec == "h264" {
NaluCodec::Avc
} else {
NaluCodec::Hevc
}))
} else {
None
};
for sample_idx in 1..=sample_count {
let sample = reader
.read_sample(track_id, sample_idx)
.context("reading sample")?;
if let Some(sample) = sample {
let sample_data = sample.bytes.to_vec();
if let Some(tracker) = avc_tracker.as_mut() {
let annexb =
length_prefixed_to_annexb_tracked(&sample_data, length_size, tracker, &sps_pps);
samples.push(annexb);
} else {
samples.push(sample_data);
}
}
}
let detected_pf = codec::pixel_format::detect(&codec, &samples);
let info = StreamInfo {
pixel_format: detected_pf,
..info
};
let audio = extract_mp4_audio(data);
Ok(DemuxResult {
codec,
info,
samples,
audio,
})
}
fn extract_mp4_audio(data: &[u8]) -> Option<AudioTrack> {
let size = data.len() as u64;
let cursor = Cursor::new(data);
let reader = Mp4Reader::read_header(cursor, size).ok()?;
let track = reader
.tracks()
.values()
.find(|t| t.track_type().ok() == Some(mp4::TrackType::Audio))?;
let track_id = track.track_id();
let opus_dops = extract_mp4_opus_dops_body(data);
let ac3_cfg = extract_mp4_ac3_dac3_body(data);
let eac3_cfg = extract_mp4_eac3_dec3_body(data);
let media_type = track.media_type();
let crate_says_aac = media_type
.as_ref()
.map(|mt| matches!(mt, mp4::MediaType::AAC))
.unwrap_or(false);
let manual_says_aac = mp4_has_aac_sample_entry(data);
let is_aac = crate_says_aac || manual_says_aac;
if !is_aac && opus_dops.is_none() && ac3_cfg.is_none() && eac3_cfg.is_none() {
match media_type {
Ok(mt) => tracing::warn!(
codec = ?mt,
"audio passthrough skipped: only AAC / Opus / AC-3 / E-AC-3 are supported"
),
Err(e) => tracing::warn!(
error = ?e,
"audio passthrough skipped: mp4 crate could not classify audio sample entry, \
and manual stsd walk found no recognized 4cc"
),
}
return None;
}
let timescale = track.timescale();
let sample_count = track.sample_count();
if is_aac {
let asc = match extract_aac_asc(data) {
Some(a) => a,
None => return None,
};
if asc.is_empty() {
tracing::warn!(
"AAC track found but AudioSpecificConfig is empty; dropping. \
Source has an esds box but its DecoderSpecificInfo descriptor is \
zero-length."
);
return None;
}
let parsed = crate::aac_asc::parse_aac_asc(&asc);
let sample_rate = match parsed
.as_ref()
.and_then(|p| p.sbr_sample_rate.or(Some(p.sample_rate)))
.or_else(|| decode_asc_sample_rate(&asc))
{
Some(sr) => sr,
None => {
tracing::warn!(
asc_hex = %hex_prefix(&asc, 16),
"AAC ASC sample rate could not be decoded; dropping audio. \
Likely an extended sampling-frequency-index escape (0x0F) \
pointing at unsupported bytes, or a malformed ASC."
);
return None;
}
};
let channels = parsed
.as_ref()
.map(crate::aac_asc::effective_output_channels)
.or_else(|| decode_asc_channels(&asc))
.unwrap_or(2);
let mut samples = Vec::with_capacity(sample_count as usize);
let mut durations = Vec::with_capacity(sample_count as usize);
const AAC_LC_CORE_FRAME_SIZE_TICKS: u32 = 1024;
if let Some(frag) = build_fragmented_sample_table(data, track_id, 0, 0) {
tracing::info!(
track_id,
sample_count = frag.len(),
"fragmented MP4 audio: built sample table from moof/traf/trun"
);
for s in &frag {
let off = s.offset as usize;
let sz = s.size as usize;
let end = match off.checked_add(sz) {
Some(e) if e <= data.len() => e,
_ => {
tracing::warn!(
track_id,
offset = s.offset,
size = s.size,
data_len = data.len(),
"fragmented audio sample range out of bounds; truncating track"
);
break;
}
};
let dur = if is_aac {
AAC_LC_CORE_FRAME_SIZE_TICKS
} else {
s.duration_ticks
};
durations.push(dur);
samples.push(data[off..end].to_vec());
}
} else {
let mut cursor = Cursor::new(data);
let mut reader = match Mp4Reader::read_header(&mut cursor, size) {
Ok(r) => r,
Err(e) => {
tracing::warn!(error = %e, "audio passthrough: re-opening MP4 for sample read failed; dropping audio");
return None;
}
};
for idx in 1..=sample_count {
match reader.read_sample(track_id, idx) {
Ok(Some(sample)) => {
let dur = if is_aac && sample.duration == 0 {
AAC_LC_CORE_FRAME_SIZE_TICKS
} else {
sample.duration
};
durations.push(dur);
samples.push(sample.bytes.to_vec());
}
Ok(None) => break,
Err(e) => {
tracing::warn!(
track_id,
idx,
error = %e,
"audio passthrough: read_sample error mid-track; \
keeping samples read so far ({} of {}) and continuing",
samples.len(),
sample_count
);
break;
}
}
}
}
if samples.is_empty() {
tracing::warn!(
track_id,
sample_count,
"AAC track parsed (ASC + sample table) but read_sample returned 0 \
samples — possible mp4 crate stsd / stco parse failure on the source"
);
return None;
}
return Some(AudioTrack {
codec: "aac".into(),
samples,
sample_rate,
channels,
asc,
codec_private: Vec::new(),
timescale,
durations,
});
}
if let Some(dac3_body) = ac3_cfg {
if dac3_body.len() < 3 {
tracing::warn!("MP4 AC-3 dac3 body shorter than 3 bytes — dropping audio");
return None;
}
let (sr, ch) = ac3_sample_rate_channels_from_dac3(&dac3_body)?;
let mut cursor = Cursor::new(data);
let mut reader = Mp4Reader::read_header(&mut cursor, size).ok()?;
let mut samples = Vec::with_capacity(sample_count as usize);
let mut durations = Vec::with_capacity(sample_count as usize);
for idx in 1..=sample_count {
match reader.read_sample(track_id, idx).ok()? {
Some(sample) => {
durations.push(sample.duration);
samples.push(sample.bytes.to_vec());
}
None => break,
}
}
if samples.is_empty() {
return None;
}
return Some(AudioTrack {
codec: "ac3".into(),
samples,
sample_rate: sr,
channels: ch,
asc: Vec::new(),
codec_private: dac3_body[..3].to_vec(),
timescale,
durations,
});
}
if let Some(dec3_body) = eac3_cfg {
if dec3_body.len() < 5 {
tracing::warn!("MP4 E-AC-3 dec3 body shorter than 5 bytes — dropping audio");
return None;
}
let (sr, ch) = eac3_sample_rate_channels_from_dec3(&dec3_body)?;
let mut cursor = Cursor::new(data);
let mut reader = Mp4Reader::read_header(&mut cursor, size).ok()?;
let mut samples = Vec::with_capacity(sample_count as usize);
let mut durations = Vec::with_capacity(sample_count as usize);
for idx in 1..=sample_count {
match reader.read_sample(track_id, idx).ok()? {
Some(sample) => {
durations.push(sample.duration);
samples.push(sample.bytes.to_vec());
}
None => break,
}
}
if samples.is_empty() {
return None;
}
return Some(AudioTrack {
codec: "eac3".into(),
samples,
sample_rate: sr,
channels: ch,
asc: Vec::new(),
codec_private: dec3_body,
timescale,
durations,
});
}
let dops_body = opus_dops?; let opus_head = dops_to_opus_head(&dops_body)?;
let input_sample_rate =
u32::from_le_bytes([opus_head[4], opus_head[5], opus_head[6], opus_head[7]]);
let channels = opus_head[1] as u16;
let mut cursor = Cursor::new(data);
let mut reader = Mp4Reader::read_header(&mut cursor, size).ok()?;
let mut samples = Vec::with_capacity(sample_count as usize);
let mut durations = Vec::with_capacity(sample_count as usize);
for idx in 1..=sample_count {
match reader.read_sample(track_id, idx).ok()? {
Some(sample) => {
durations.push(sample.duration);
samples.push(sample.bytes.to_vec());
}
None => break,
}
}
if samples.is_empty() {
return None;
}
Some(AudioTrack {
codec: "opus".into(),
samples,
sample_rate: input_sample_rate,
channels,
asc: Vec::new(),
codec_private: opus_head,
timescale,
durations,
})
}
fn extract_mp4_ac3_dac3_body(data: &[u8]) -> Option<Vec<u8>> {
extract_mp4_audio_config_body(data, b"ac-3", b"dac3")
}
fn extract_mp4_eac3_dec3_body(data: &[u8]) -> Option<Vec<u8>> {
extract_mp4_audio_config_body(data, b"ec-3", b"dec3")
}
fn extract_mp4_audio_config_body(
data: &[u8],
entry_fourcc: &[u8; 4],
cfg_fourcc: &[u8; 4],
) -> Option<Vec<u8>> {
let moov = find_direct_child(data, b"moov")?;
let mut pos = 0;
while pos + 8 <= moov.len() {
let size =
u32::from_be_bytes([moov[pos], moov[pos + 1], moov[pos + 2], moov[pos + 3]]) as usize;
let btype = &moov[pos + 4..pos + 8];
if size < 8 || pos.checked_add(size).is_none_or(|end| end > moov.len()) {
break;
}
if btype == b"trak" {
let trak_body = &moov[pos + 8..pos + size];
if let Some(cfg) = extract_audio_cfg_from_trak(trak_body, entry_fourcc, cfg_fourcc) {
return Some(cfg);
}
}
pos += size;
}
None
}
fn extract_audio_cfg_from_trak(
trak: &[u8],
entry_fourcc: &[u8; 4],
cfg_fourcc: &[u8; 4],
) -> Option<Vec<u8>> {
let stsd = find_box_body(trak, &[b"mdia", b"minf", b"stbl", b"stsd"])?;
if stsd.len() < 16 {
return None;
}
let mut pos = 8; while pos + 8 <= stsd.len() {
let entry_size =
u32::from_be_bytes([stsd[pos], stsd[pos + 1], stsd[pos + 2], stsd[pos + 3]]) as usize;
let entry_type: [u8; 4] = stsd[pos + 4..pos + 8].try_into().ok()?;
if entry_size < 8 || pos.saturating_add(entry_size) > stsd.len() {
break;
}
if &entry_type == entry_fourcc {
let end = pos + entry_size;
let child_start = pos + 8 + 28;
if child_start >= end {
return None;
}
return find_direct_child(&stsd[child_start..end], cfg_fourcc).map(|b| b.to_vec());
}
pos += entry_size;
}
None
}
fn ac3_sample_rate_channels_from_dac3(dac3: &[u8]) -> Option<(u32, u16)> {
if dac3.len() < 3 {
return None;
}
let raw = ((dac3[0] as u32) << 16) | ((dac3[1] as u32) << 8) | dac3[2] as u32;
let fscod = ((raw >> 22) & 0x03) as u8;
let acmod = ((raw >> 11) & 0x07) as u8;
let lfeon = ((raw >> 10) & 0x01) == 1;
let sr = match fscod {
0 => 48_000,
1 => 44_100,
2 => 32_000,
_ => return None,
};
Some((sr, crate::ac3_sync::channel_count(acmod, lfeon)))
}
fn eac3_sample_rate_channels_from_dec3(dec3: &[u8]) -> Option<(u32, u16)> {
if dec3.len() < 5 {
return None;
}
let raw_be = u64::from(dec3[0]) << 32
| u64::from(dec3[1]) << 24
| u64::from(dec3[2]) << 16
| u64::from(dec3[3]) << 8
| u64::from(dec3[4]);
let fscod = ((raw_be >> 22) & 0x03) as u8;
let acmod = ((raw_be >> 9) & 0x07) as u8;
let lfeon = ((raw_be >> 8) & 0x01) == 1;
let sr = crate::ac3_sync::eac3_sample_rate_hz(fscod, 0);
if sr == 0 {
return None;
}
Some((sr, crate::ac3_sync::channel_count(acmod, lfeon)))
}
fn extract_mp4_opus_dops_body(data: &[u8]) -> Option<Vec<u8>> {
let moov = find_direct_child(data, b"moov")?;
let mut pos = 0;
while pos + 8 <= moov.len() {
let size =
u32::from_be_bytes([moov[pos], moov[pos + 1], moov[pos + 2], moov[pos + 3]]) as usize;
let btype = &moov[pos + 4..pos + 8];
if size < 8 || pos.checked_add(size).is_none_or(|end| end > moov.len()) {
break;
}
if btype == b"trak" {
let trak_body = &moov[pos + 8..pos + size];
if let Some(dops) = extract_dops_from_trak(trak_body) {
return Some(dops);
}
}
pos += size;
}
None
}
fn extract_dops_from_trak(trak: &[u8]) -> Option<Vec<u8>> {
let stsd = find_box_body(trak, &[b"mdia", b"minf", b"stbl", b"stsd"])?;
if stsd.len() < 16 {
return None;
}
let mut pos = 8; while pos + 8 <= stsd.len() {
let entry_size =
u32::from_be_bytes([stsd[pos], stsd[pos + 1], stsd[pos + 2], stsd[pos + 3]]) as usize;
let entry_type: [u8; 4] = stsd[pos + 4..pos + 8].try_into().ok()?;
if entry_size < 8 || pos.saturating_add(entry_size) > stsd.len() {
break;
}
if &entry_type == b"Opus" {
let end = pos + entry_size;
let child_start = pos + 8 + 28;
if child_start >= end {
return None;
}
return find_direct_child(&stsd[child_start..end], b"dOps").map(|b| b.to_vec());
}
pos += entry_size;
}
None
}
fn dops_to_opus_head(dops: &[u8]) -> Option<Vec<u8>> {
if dops.len() < 11 {
return None;
}
let output_channels = dops[1];
let pre_skip = u16::from_be_bytes([dops[2], dops[3]]);
let input_sample_rate = u32::from_be_bytes([dops[4], dops[5], dops[6], dops[7]]);
let output_gain = i16::from_be_bytes([dops[8], dops[9]]);
let channel_mapping_family = dops[10];
let extra_tail = if channel_mapping_family != 0 {
if dops.len() < 13 {
return None;
}
let tail_len = 2 + dops[12] as usize;
if dops.len() < 11 + tail_len {
return None;
}
dops[11..11 + tail_len].to_vec()
} else {
Vec::new()
};
let mut head = Vec::with_capacity(11 + extra_tail.len());
head.push(1u8); head.push(output_channels);
head.extend_from_slice(&pre_skip.to_le_bytes());
head.extend_from_slice(&input_sample_rate.to_le_bytes());
head.extend_from_slice(&(output_gain as u16).to_le_bytes());
head.push(channel_mapping_family);
head.extend_from_slice(&extra_tail);
Some(head)
}
fn extract_aac_asc(data: &[u8]) -> Option<Vec<u8>> {
let moov = find_direct_child(data, b"moov")?;
let mut pos = 0;
let mut saw_audio_trak = false;
while pos + 8 <= moov.len() {
let size =
u32::from_be_bytes([moov[pos], moov[pos + 1], moov[pos + 2], moov[pos + 3]]) as usize;
let btype = &moov[pos + 4..pos + 8];
if size < 8 || pos.checked_add(size).is_none_or(|end| end > moov.len()) {
break;
}
if btype == b"trak" {
let trak_body = &moov[pos + 8..pos + size];
if trak_is_audio(trak_body) {
saw_audio_trak = true;
if let Some(asc) = extract_asc_from_trak(trak_body) {
return Some(asc);
}
if let Some(asc) = brute_force_find_asc_in_trak(trak_body) {
tracing::warn!(
asc_len = asc.len(),
"audio passthrough recovered ASC via brute-force esds scan; \
the trak's stsd shape is not in our structured handler. \
Capture this file and add coverage so the structured walk \
finds it next time."
);
return Some(asc);
}
}
}
pos += size;
}
if saw_audio_trak {
tracing::warn!(
"audio passthrough skipped: identified an audio trak via smhd, but no \
stsd entry yielded an AudioSpecificConfig. Possible causes: enca with \
unsupported scheme, sample entry fourcc we don't recognise, esds box \
missing or corrupt, mp4 sanitizer mis-aligned a wave-wrapped esds."
);
} else {
tracing::warn!(
"audio passthrough skipped: no trak had a Sound Media Header (smhd). \
Source may be video-only, or its track headers do not conform to ISOBMFF \
§8.4.5.3 (smhd is required for audio traks)."
);
}
None
}
fn hex_prefix(bytes: &[u8], n: usize) -> String {
let mut out = String::with_capacity(n * 2);
for b in bytes.iter().take(n) {
out.push_str(&format!("{b:02x}"));
}
out
}
const AAC_AUDIO_SAMPLE_ENTRIES: &[&[u8; 4]] = &[b"mp4a", b"enca"];
fn trak_is_audio(trak: &[u8]) -> bool {
find_box_body(trak, &[b"mdia", b"minf", b"smhd"]).is_some()
}
fn extract_asc_from_trak(trak: &[u8]) -> Option<Vec<u8>> {
let stsd = find_box_body(trak, &[b"mdia", b"minf", b"stbl", b"stsd"])?;
if stsd.len() < 8 {
tracing::warn!(
stsd_len = stsd.len(),
"audio passthrough: stsd shorter than its 8-byte FullBox preamble"
);
return None;
}
let entries = &stsd[8..];
let mut cursor = 0;
while cursor + 8 <= entries.len() {
let entry_size = u32::from_be_bytes([
entries[cursor],
entries[cursor + 1],
entries[cursor + 2],
entries[cursor + 3],
]) as usize;
let entry_type: &[u8; 4] = entries[cursor + 4..cursor + 8].try_into().unwrap();
if entry_size < 8 || cursor + entry_size > entries.len() {
break;
}
if AAC_AUDIO_SAMPLE_ENTRIES.contains(&entry_type) {
if entry_size >= 36 {
let body = &entries[cursor + 8 + 28..cursor + entry_size];
if let Some(asc) = find_esds_recursive(body) {
return Some(asc);
}
}
}
cursor += entry_size;
}
None
}
fn brute_force_find_asc_in_trak(trak: &[u8]) -> Option<Vec<u8>> {
let mut pos = 0;
while pos + 8 <= trak.len() {
if &trak[pos + 4..pos + 8] == b"esds" {
let size = u32::from_be_bytes([trak[pos], trak[pos + 1], trak[pos + 2], trak[pos + 3]])
as usize;
if size >= 12 && pos + size <= trak.len() {
let esds_body = &trak[pos + 12..pos + size];
if let Some(asc) = extract_asc_from_esds(esds_body) {
if !asc.is_empty() {
return Some(asc);
}
}
}
}
pos += 1;
}
None
}
fn find_esds_recursive(body: &[u8]) -> Option<Vec<u8>> {
let mut pos = 0;
while pos + 8 <= body.len() {
let sub_size =
u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]) as usize;
let sub_type = &body[pos + 4..pos + 8];
if sub_size < 8 || pos + sub_size > body.len() {
break;
}
if sub_type == b"esds" {
let esds_body = &body[pos + 8 + 4..pos + sub_size];
return extract_asc_from_esds(esds_body);
}
if sub_type == b"wave" {
if let Some(asc) = find_esds_recursive(&body[pos + 8..pos + sub_size]) {
return Some(asc);
}
}
pos += sub_size;
}
None
}
fn mp4_has_aac_sample_entry(data: &[u8]) -> bool {
let Some(moov) = find_direct_child(data, b"moov") else {
return false;
};
let mut pos = 0;
while pos + 8 <= moov.len() {
let size =
u32::from_be_bytes([moov[pos], moov[pos + 1], moov[pos + 2], moov[pos + 3]]) as usize;
let btype = &moov[pos + 4..pos + 8];
if size < 8 || pos + size > moov.len() {
break;
}
if btype == b"trak" {
let trak_body = &moov[pos + 8..pos + size];
if !trak_is_audio(trak_body) {
pos += size;
continue;
}
if let Some(stsd) = find_box_body(trak_body, &[b"mdia", b"minf", b"stbl", b"stsd"])
&& stsd.len() >= 8
{
let entries = &stsd[8..];
let mut cursor = 0;
while cursor + 8 <= entries.len() {
let entry_size = u32::from_be_bytes([
entries[cursor],
entries[cursor + 1],
entries[cursor + 2],
entries[cursor + 3],
]) as usize;
if entry_size < 8 || cursor + entry_size > entries.len() {
break;
}
let entry_type: &[u8; 4] = entries[cursor + 4..cursor + 8].try_into().unwrap();
if AAC_AUDIO_SAMPLE_ENTRIES.contains(&entry_type) {
return true;
}
cursor += entry_size;
}
}
}
pos += size;
}
false
}
fn extract_asc_from_esds(body: &[u8]) -> Option<Vec<u8>> {
let (tag, payload, _rest) = read_descriptor(body)?;
if tag != 0x03 {
return None;
}
if payload.len() < 3 {
return None;
}
let flags = payload[2];
let mut off = 3;
if flags & 0x80 != 0 {
off += 2;
} if flags & 0x40 != 0 {
if off >= payload.len() {
return None;
}
let url_len = payload[off] as usize;
off += 1 + url_len;
}
if flags & 0x20 != 0 {
off += 2;
} if off > payload.len() {
return None;
}
let mut cursor = &payload[off..];
while !cursor.is_empty() {
let (tag, child, rest) = read_descriptor(cursor)?;
cursor = rest;
if tag != 0x04 {
continue;
}
if child.len() < 13 {
return None;
}
let inner = &child[13..];
let mut inner_cursor = inner;
while !inner_cursor.is_empty() {
let (t, dsi_payload, r) = read_descriptor(inner_cursor)?;
inner_cursor = r;
if t == 0x05 {
return Some(dsi_payload.to_vec());
}
}
return None;
}
None
}
fn read_descriptor(data: &[u8]) -> Option<(u8, &[u8], &[u8])> {
if data.is_empty() {
return None;
}
let tag = data[0];
let mut pos = 1;
let mut length: usize = 0;
for _ in 0..4 {
if pos >= data.len() {
return None;
}
let b = data[pos];
pos += 1;
length = (length << 7) | (b & 0x7F) as usize;
if b & 0x80 == 0 {
break;
}
}
if pos + length > data.len() {
return None;
}
let payload = &data[pos..pos + length];
let rest = &data[pos + length..];
Some((tag, payload, rest))
}
fn decode_asc_sample_rate(asc: &[u8]) -> Option<u32> {
if asc.len() < 2 {
return None;
}
let mut br = AscBitReader::new(asc);
let aot = br.bits(5)?;
let _extended_aot = if aot == 31 { br.bits(6)? + 32 } else { aot };
let freq_idx = br.bits(4)? as usize;
if freq_idx == 0xF {
let sr = br.bits(24)?;
Some(sr as u32)
} else {
const FREQS: [u32; 13] = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
];
FREQS.get(freq_idx).copied()
}
}
fn decode_asc_channels(asc: &[u8]) -> Option<u16> {
if asc.len() < 2 {
return None;
}
let mut br = AscBitReader::new(asc);
let aot = br.bits(5)?;
let _ext = if aot == 31 { br.bits(6)? + 32 } else { aot };
let freq_idx = br.bits(4)? as usize;
if freq_idx == 0xF {
let _ = br.bits(24)?;
}
let chan_cfg = br.bits(4)? as u16;
if chan_cfg == 0 {
Some(2)
} else {
Some(chan_cfg)
}
}
struct AscBitReader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> AscBitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn bits(&mut self, n: u32) -> Option<u64> {
let mut v: u64 = 0;
for _ in 0..n {
let byte = *self.data.get(self.pos / 8)?;
let bit = (byte >> (7 - (self.pos % 8))) & 1;
v = (v << 1) | bit as u64;
self.pos += 1;
}
Some(v)
}
}
pub fn demux_mkv(data: &[u8]) -> Result<DemuxResult> {
let cursor = Cursor::new(data);
let mut mkv =
MatroskaFile::open(cursor).map_err(|e| anyhow::anyhow!("reading MKV header: {e}"))?;
let (
track_number,
track_uid,
codec_id,
width,
height,
annexb_prepend,
length_size,
color_space,
mut color_metadata,
mut color_info,
track_default_duration_ns,
) = {
let track_info = mkv
.tracks()
.iter()
.find(|t| t.track_type() == MkvTrackType::Video)
.context("no video track in MKV")?;
let track_number = track_info.track_number().get();
let track_uid = track_info.track_uid().get();
let codec_id = track_info.codec_id().to_string();
let default_duration_ns = track_info.default_duration().map(|d| d.get());
let (annexb_prepend, length_size): (Vec<Vec<u8>>, u8) = if codec_id == "V_MPEG4/ISO/AVC" {
let priv_bytes = track_info
.codec_private()
.context("V_MPEG4/ISO/AVC CodecPrivate missing")?;
let cfg = parse_avcc(priv_bytes).context("V_MPEG4/ISO/AVC CodecPrivate malformed")?;
(cfg.parameter_sets, cfg.length_size)
} else if codec_id == "V_MPEGH/ISO/HEVC" {
let priv_bytes = track_info
.codec_private()
.context("V_MPEGH/ISO/HEVC CodecPrivate missing")?;
let cfg = parse_hvcc(priv_bytes).context("V_MPEGH/ISO/HEVC CodecPrivate malformed")?;
(cfg.parameter_sets, cfg.length_size)
} else {
(Vec::new(), 4)
};
if mkv_codec_needs_annexb(&codec_id) && annexb_prepend.is_empty() {
bail!("AVC/HEVC MKV CodecPrivate missing or empty — no parameter sets to prepend");
}
let video = track_info
.video()
.context("video track missing Video element")?;
let w = video.pixel_width().get() as u32;
let h = video.pixel_height().get() as u32;
let (color_space, color_metadata, color_info) = match video.colour() {
Some(colour) => colour_to_pipeline(colour),
None => (
ColorSpace::Bt709,
ColorMetadata::default(),
MkvColorInfo::default(),
),
};
(
track_number,
track_uid,
codec_id,
w,
h,
annexb_prepend,
length_size,
color_space,
color_metadata,
color_info,
default_duration_ns,
)
};
color_info.max_cll = None;
color_info.max_fall = None;
color_metadata.content_light_level = None;
if let Some(md) = color_metadata.mastering_display.as_mut() {
md.primaries_r_y = 0;
md.primaries_g_y = 0;
md.primaries_b_y = 0;
}
if let Some(local) = color_info.mastering.as_mut() {
local.primary_r_chromaticity_y = None;
local.primary_g_chromaticity_y = None;
local.primary_b_chromaticity_y = None;
}
if let Some(fix) = scan_mkv_colour_raw(data) {
color_info.max_cll = fix.max_cll;
color_info.max_fall = fix.max_fall;
if fix.max_cll.is_some() || fix.max_fall.is_some() {
color_metadata.content_light_level = Some(ContentLightLevel {
max_cll: fix.max_cll.unwrap_or(0).min(u16::MAX as u32) as u16,
max_fall: fix.max_fall.unwrap_or(0).min(u16::MAX as u32) as u16,
});
}
let chrom = |v: f64| (v * 50_000.0).round().clamp(0.0, u16::MAX as f64) as u16;
if let Some(md) = color_metadata.mastering_display.as_mut() {
if let Some(y) = fix.primary_r_chromaticity_y {
md.primaries_r_y = chrom(y);
}
if let Some(y) = fix.primary_g_chromaticity_y {
md.primaries_g_y = chrom(y);
}
if let Some(y) = fix.primary_b_chromaticity_y {
md.primaries_b_y = chrom(y);
}
}
if let Some(local) = color_info.mastering.as_mut() {
if fix.primary_r_chromaticity_y.is_some() {
local.primary_r_chromaticity_y = fix.primary_r_chromaticity_y;
}
if fix.primary_g_chromaticity_y.is_some() {
local.primary_g_chromaticity_y = fix.primary_g_chromaticity_y;
}
if fix.primary_b_chromaticity_y.is_some() {
local.primary_b_chromaticity_y = fix.primary_b_chromaticity_y;
}
}
}
let needs_annexb = mkv_codec_needs_annexb(&codec_id);
let codec = match codec_id.as_str() {
"V_VP9" => "vp9".to_string(),
"V_VP8" => "vp8".to_string(),
"V_AV1" => "av1".to_string(),
"V_MPEG4/ISO/AVC" => "h264".to_string(),
"V_MPEGH/ISO/HEVC" => "h265".to_string(),
other => other.to_lowercase(),
};
let timestamp_scale = mkv.info().timestamp_scale().get();
let duration_ticks = mkv.info().duration().unwrap_or(0.0);
let duration = duration_ticks * (timestamp_scale as f64) / 1_000_000_000.0;
let tag_bitrate = mkv
.tags()
.and_then(|tags| bitrate_from_tags(tags, track_uid));
if color_info != MkvColorInfo::default() {
tracing::info!(
bits_per_channel = ?color_info.bits_per_channel,
max_cll = ?color_info.max_cll,
max_fall = ?color_info.max_fall,
mastering = ?color_info.mastering,
"MKV Colour: parsed HDR-adjacent metadata"
);
}
let mut samples: Vec<Vec<u8>> = Vec::new();
let mut frame = MkvFrame::default();
let mut total_video_bytes: u64 = 0;
let mut mkv_tracker = if needs_annexb {
Some(ParamSetTracker::new(if codec_id == "V_MPEG4/ISO/AVC" {
NaluCodec::Avc
} else {
NaluCodec::Hevc
}))
} else {
None
};
loop {
match mkv.next_frame(&mut frame) {
Ok(true) => {
if frame.track == track_number {
let raw = std::mem::take(&mut frame.data);
total_video_bytes += raw.len() as u64;
if let Some(tracker) = mkv_tracker.as_mut() {
let annexb = length_prefixed_to_annexb_tracked(
&raw,
length_size,
tracker,
&annexb_prepend,
);
samples.push(annexb);
} else {
samples.push(raw);
}
}
}
Ok(false) => break,
Err(e) => bail!("MKV frame read error: {e}"),
}
}
let total_frames = samples.len() as u64;
let frame_rate = if duration > 0.0 {
total_frames as f64 / duration
} else if let Some(dd_ns) = track_default_duration_ns.filter(|n| *n > 0) {
1_000_000_000.0 / dd_ns as f64
} else {
30.0
};
let detected_pf = codec::pixel_format::detect(&codec, &samples);
let bitrate = match tag_bitrate {
Some(b) if b > 0 => b,
_ => {
if duration > 0.0 && total_video_bytes > 0 {
((total_video_bytes as f64 * 8.0) / duration) as u64
} else {
0
}
}
};
let info = StreamInfo {
codec: codec.clone(),
width,
height,
frame_rate,
duration,
pixel_format: detected_pf,
color_space,
total_frames,
bitrate,
color_metadata,
};
let audio = extract_mkv_audio(data);
Ok(DemuxResult {
codec,
info,
samples,
audio,
})
}
fn extract_mkv_audio(data: &[u8]) -> Option<AudioTrack> {
let cursor = Cursor::new(data);
let mut mkv = MatroskaFile::open(cursor).ok()?;
enum MkvAudioKind {
Aac,
Opus,
Ac3,
Eac3,
}
let (track_number, kind, codec_private_or_empty, sample_rate, channels, default_duration) = {
let track = mkv
.tracks()
.iter()
.find(|t| t.track_type() == MkvTrackType::Audio)?;
let codec_id = track.codec_id();
let kind = match codec_id {
"A_AAC" => MkvAudioKind::Aac,
"A_OPUS" => MkvAudioKind::Opus,
"A_AC3" => MkvAudioKind::Ac3,
"A_EAC3" => MkvAudioKind::Eac3,
other => {
tracing::warn!(
codec = other,
"audio passthrough skipped: only AAC / Opus / AC-3 / E-AC-3 are supported"
);
return None;
}
};
let codec_private = match kind {
MkvAudioKind::Aac => {
let cp = track.codec_private()?.to_vec();
if cp.is_empty() {
return None;
}
cp
}
MkvAudioKind::Opus => {
let mut cp = track.codec_private()?.to_vec();
if cp.is_empty() {
return None;
}
if cp.len() >= 8 && &cp[..8] == b"OpusHead" {
cp.drain(..8);
}
if cp.is_empty() {
return None;
}
cp
}
MkvAudioKind::Ac3 | MkvAudioKind::Eac3 => track
.codec_private()
.map(|p| p.to_vec())
.unwrap_or_default(),
};
let audio = track.audio()?;
let sr = audio.sampling_frequency() as u32;
let ch = audio.channels().get() as u16;
let default_duration = track.default_duration().map(|d| d.get());
(
track.track_number().get(),
kind,
codec_private,
sr,
ch,
default_duration,
)
};
let timescale = match kind {
MkvAudioKind::Aac => sample_rate,
MkvAudioKind::Opus => 48_000,
MkvAudioKind::Ac3 | MkvAudioKind::Eac3 => sample_rate,
};
let default_frame_samples_at_ts = match kind {
MkvAudioKind::Aac => 1024u64,
MkvAudioKind::Opus => 960u64,
MkvAudioKind::Ac3 | MkvAudioKind::Eac3 => 1536u64,
};
let timescale_for_fallback = if timescale == 0 { 48_000 } else { timescale };
let mut samples: Vec<Vec<u8>> = Vec::new();
let mut durations: Vec<u32> = Vec::new();
let mut frame = MkvFrame::default();
loop {
match mkv.next_frame(&mut frame) {
Ok(true) => {
if frame.track == track_number {
let dur_ns = frame.duration.or(default_duration).unwrap_or_else(|| {
1_000_000_000u64 * default_frame_samples_at_ts
/ timescale_for_fallback as u64
});
let dur_ticks = ((dur_ns as u128) * (timescale as u128) / 1_000_000_000) as u32;
durations.push(dur_ticks.max(1));
samples.push(std::mem::take(&mut frame.data));
}
}
Ok(false) => break,
Err(_) => break,
}
}
if samples.is_empty() {
return None;
}
Some(match kind {
MkvAudioKind::Aac => {
let parsed = crate::aac_asc::parse_aac_asc(&codec_private_or_empty);
let aac_channels = parsed
.as_ref()
.map(crate::aac_asc::effective_output_channels)
.unwrap_or(channels);
let aac_sample_rate = parsed
.as_ref()
.and_then(|p| p.sbr_sample_rate.or(Some(p.sample_rate)))
.unwrap_or(sample_rate);
AudioTrack {
codec: "aac".into(),
samples,
sample_rate: aac_sample_rate,
channels: aac_channels,
asc: codec_private_or_empty,
codec_private: Vec::new(),
timescale: aac_sample_rate, durations,
}
}
MkvAudioKind::Opus => AudioTrack {
codec: "opus".into(),
samples,
sample_rate,
channels,
asc: Vec::new(),
codec_private: codec_private_or_empty,
timescale,
durations,
},
MkvAudioKind::Ac3 => {
let dac3 = match samples
.first()
.and_then(|f| crate::ac3_sync::parse_sync_info(f).ok())
{
Some(crate::ac3_sync::SyncInfo::Ac3(s)) => {
crate::mux::dac3_body_from_sync(&s).to_vec()
}
_ => {
tracing::warn!(
"MKV A_AC3: failed to parse first frame sync header — dropping audio"
);
return None;
}
};
let (sr, ch) =
ac3_sample_rate_channels_from_dac3(&dac3).unwrap_or((sample_rate, channels));
AudioTrack {
codec: "ac3".into(),
samples,
sample_rate: sr,
channels: ch,
asc: Vec::new(),
codec_private: dac3,
timescale: sr,
durations,
}
}
MkvAudioKind::Eac3 => {
let (dec3, sr, ch) = match samples
.first()
.and_then(|f| crate::ac3_sync::parse_sync_info(f).ok())
{
Some(crate::ac3_sync::SyncInfo::Eac3(s)) => {
let sr = crate::ac3_sync::eac3_sample_rate_hz(s.fscod, s.fscod2);
let spf = crate::ac3_sync::eac3_samples_per_frame(s.numblkscod) as u64;
let frame_bytes = ((s.frmsiz as u64) + 1) * 2;
let bitrate_kbps = if spf > 0 && sr > 0 {
(frame_bytes * 8 * sr as u64) / spf / 1000
} else {
0
};
let data_rate = bitrate_kbps.div_ceil(2) as u16;
let dec3 = crate::mux::dec3_body_from_sync(&s, data_rate).to_vec();
let ch = crate::ac3_sync::channel_count(s.acmod, s.lfeon);
(dec3, sr, ch)
}
_ => {
tracing::warn!(
"MKV A_EAC3: failed to parse first frame sync header — dropping audio"
);
return None;
}
};
AudioTrack {
codec: "eac3".into(),
samples,
sample_rate: sr,
channels: ch,
asc: Vec::new(),
codec_private: dec3,
timescale: sr,
durations,
}
}
})
}
fn mkv_codec_needs_annexb(codec_id: &str) -> bool {
matches!(codec_id, "V_MPEG4/ISO/AVC" | "V_MPEGH/ISO/HEVC")
}
fn hevc_sample_entry_fourcc(data: &[u8]) -> Option<[u8; 4]> {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let stsd_body = find_box_body(data, path)?;
if stsd_body.len() < 16 {
return None;
}
let mut pos = 8; while pos + 8 <= stsd_body.len() {
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
let entry_type: [u8; 4] = stsd_body[pos + 4..pos + 8].try_into().ok()?;
match &entry_type {
b"hvc1" | b"hev1" | b"hvc2" | b"hev2" | b"dvh1" | b"dvhe" => {
return Some(entry_type);
}
_ => {}
}
if entry_size == 0 {
break;
}
pos = pos.saturating_add(entry_size);
}
None
}
fn prores_sample_entry_fourcc(data: &[u8]) -> Option<[u8; 4]> {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let stsd_body = find_box_body(data, path)?;
if stsd_body.len() < 16 {
return None;
}
let mut pos = 8;
while pos + 8 <= stsd_body.len() {
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
let entry_type: [u8; 4] = stsd_body[pos + 4..pos + 8].try_into().ok()?;
match &entry_type {
b"apcn" | b"apch" | b"apcs" | b"apco" | b"ap4h" | b"ap4x" => {
return Some(entry_type);
}
_ => {}
}
if entry_size == 0 {
break;
}
pos = pos.saturating_add(entry_size);
}
None
}
fn extract_avc_config(data: &[u8]) -> Option<AvcConfig> {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let stsd_body = find_box_body(data, path)?;
if stsd_body.len() < 16 {
return None;
}
let mut pos = 8;
while pos + 8 <= stsd_body.len() {
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
let entry_type = &stsd_body[pos + 4..pos + 8];
let is_avc = matches!(entry_type, b"avc1" | b"avc3");
if !is_avc {
if entry_size == 0 {
break;
}
pos = pos.saturating_add(entry_size);
continue;
}
let end = pos.saturating_add(entry_size);
if end > stsd_body.len() {
return None;
}
let child_start = pos + 8 + 78; if child_start >= end {
return None;
}
let avcc = find_direct_child(&stsd_body[child_start..end], b"avcC")?;
return parse_avcc(avcc);
}
None
}
#[derive(Debug, Default, Clone, Copy)]
struct Mp4VisualColorMetadata {
mastering_display: Option<MasteringDisplay>,
content_light_level: Option<ContentLightLevel>,
}
fn extract_mp4_visual_color_metadata(data: &[u8]) -> Mp4VisualColorMetadata {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let Some(stsd_body) = find_box_body(data, path) else {
return Mp4VisualColorMetadata::default();
};
if stsd_body.len() < 16 {
return Mp4VisualColorMetadata::default();
}
let mut pos = 8; while pos + 8 <= stsd_body.len() {
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
if entry_size < 8 || pos.saturating_add(entry_size) > stsd_body.len() {
break;
}
let entry_type: [u8; 4] = match stsd_body[pos + 4..pos + 8].try_into() {
Ok(v) => v,
Err(_) => break,
};
let is_visual = matches!(
&entry_type,
b"av01"
| b"avc1"
| b"avc3"
| b"hvc1"
| b"hev1"
| b"hvc2"
| b"hev2"
| b"dvh1"
| b"dvhe"
| b"vp08"
| b"vp09"
| b"apcn"
| b"apch"
| b"apcs"
| b"apco"
| b"ap4h"
| b"ap4x"
);
if !is_visual {
pos = pos.saturating_add(entry_size);
continue;
}
let end = pos.saturating_add(entry_size);
let child_start = pos + 8 + 78;
if child_start >= end {
return Mp4VisualColorMetadata::default();
}
let children = &stsd_body[child_start..end];
let mut out = Mp4VisualColorMetadata::default();
if let Some(mdcv) = find_direct_child(children, b"mdcv") {
out.mastering_display = parse_mp4_mdcv(mdcv);
}
if let Some(clli) = find_direct_child(children, b"clli") {
out.content_light_level = parse_mp4_clli(clli);
}
return out;
}
Mp4VisualColorMetadata::default()
}
fn parse_mp4_mdcv(body: &[u8]) -> Option<MasteringDisplay> {
if body.len() < 24 {
return None;
}
let u16be = |o: usize| u16::from_be_bytes([body[o], body[o + 1]]);
let u32be = |o: usize| u32::from_be_bytes([body[o], body[o + 1], body[o + 2], body[o + 3]]);
Some(MasteringDisplay {
primaries_g_x: u16be(0),
primaries_g_y: u16be(2),
primaries_b_x: u16be(4),
primaries_b_y: u16be(6),
primaries_r_x: u16be(8),
primaries_r_y: u16be(10),
white_point_x: u16be(12),
white_point_y: u16be(14),
max_luminance: u32be(16),
min_luminance: u32be(20),
})
}
fn parse_mp4_clli(body: &[u8]) -> Option<ContentLightLevel> {
if body.len() < 4 {
return None;
}
Some(ContentLightLevel {
max_cll: u16::from_be_bytes([body[0], body[1]]),
max_fall: u16::from_be_bytes([body[2], body[3]]),
})
}
fn extract_hevc_config(data: &[u8]) -> Option<HevcConfig> {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let stsd_body = find_box_body(data, path)?;
if stsd_body.len() < 16 {
return None;
}
let mut pos = 8;
while pos + 8 <= stsd_body.len() {
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
let entry_type = &stsd_body[pos + 4..pos + 8];
let is_hevc = matches!(
entry_type,
b"hvc1" | b"hev1" | b"hvc2" | b"hev2" | b"dvh1" | b"dvhe"
);
if !is_hevc {
if entry_size == 0 {
break;
}
pos = pos.saturating_add(entry_size);
continue;
}
let end = pos.saturating_add(entry_size);
if end > stsd_body.len() {
return None;
}
let child_start = pos + 8 + 78;
if child_start >= end {
return None;
}
let hvcc = find_direct_child(&stsd_body[child_start..end], b"hvcC")?;
return parse_hvcc(hvcc);
}
None
}
#[allow(dead_code)]
fn extract_hevc_parameter_sets(data: &[u8]) -> Vec<Vec<u8>> {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let Some(stsd_body) = find_box_body(data, path) else {
return Vec::new();
};
if stsd_body.len() < 16 {
return Vec::new();
}
let mut pos = 8;
while pos + 8 <= stsd_body.len() {
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
let entry_type = &stsd_body[pos + 4..pos + 8];
let is_hevc = matches!(
entry_type,
b"hvc1" | b"hev1" | b"hvc2" | b"hev2" | b"dvh1" | b"dvhe"
);
if !is_hevc {
if entry_size == 0 {
break;
}
pos = pos.saturating_add(entry_size);
continue;
}
let end = pos.saturating_add(entry_size);
if end > stsd_body.len() {
return Vec::new();
}
let entry_body_start = pos + 8;
let child_start = entry_body_start + 78;
if child_start >= end {
return Vec::new();
}
let child_area = &stsd_body[child_start..end];
let hvcc = match find_direct_child(child_area, b"hvcC") {
Some(b) => b,
None => return Vec::new(),
};
return parse_hvcc_param_sets(hvcc);
}
Vec::new()
}
#[allow(dead_code)]
fn parse_avcc_param_sets(avcc: &[u8]) -> Vec<Vec<u8>> {
if avcc.len() < 7 {
return Vec::new();
}
let num_sps = (avcc[5] & 0x1F) as usize;
let mut out = Vec::new();
let mut cur = 6;
for _ in 0..num_sps {
if cur + 2 > avcc.len() {
return out;
}
let nalu_len = u16::from_be_bytes([avcc[cur], avcc[cur + 1]]) as usize;
cur += 2;
if cur + nalu_len > avcc.len() {
return out;
}
out.push(avcc[cur..cur + nalu_len].to_vec());
cur += nalu_len;
}
if cur >= avcc.len() {
return out;
}
let num_pps = avcc[cur] as usize;
cur += 1;
for _ in 0..num_pps {
if cur + 2 > avcc.len() {
return out;
}
let nalu_len = u16::from_be_bytes([avcc[cur], avcc[cur + 1]]) as usize;
cur += 2;
if cur + nalu_len > avcc.len() {
return out;
}
out.push(avcc[cur..cur + nalu_len].to_vec());
cur += nalu_len;
}
out
}
#[allow(dead_code)]
fn parse_hvcc_param_sets(hvcc: &[u8]) -> Vec<Vec<u8>> {
if hvcc.len() < 23 {
return Vec::new();
}
let num_arrays = hvcc[22] as usize;
let mut out = Vec::new();
let mut cur = 23;
for _ in 0..num_arrays {
if cur + 3 > hvcc.len() {
break;
}
let _array_hdr = hvcc[cur];
let num_nalus = u16::from_be_bytes([hvcc[cur + 1], hvcc[cur + 2]]) as usize;
cur += 3;
for _ in 0..num_nalus {
if cur + 2 > hvcc.len() {
return out;
}
let nalu_len = u16::from_be_bytes([hvcc[cur], hvcc[cur + 1]]) as usize;
cur += 2;
if cur + nalu_len > hvcc.len() {
return out;
}
out.push(hvcc[cur..cur + nalu_len].to_vec());
cur += nalu_len;
}
}
out
}
fn has_av01_sample_entry(data: &[u8]) -> bool {
let path: &[&[u8; 4]] = &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"];
let stsd_body = match find_box_body(data, path) {
Some(b) => b,
None => return false,
};
if stsd_body.len() < 16 {
return false;
}
let mut pos = 8; while pos + 8 <= stsd_body.len() {
let entry_type = &stsd_body[pos + 4..pos + 8];
if entry_type == b"av01" {
return true;
}
let entry_size = u32::from_be_bytes([
stsd_body[pos],
stsd_body[pos + 1],
stsd_body[pos + 2],
stsd_body[pos + 3],
]) as usize;
if entry_size == 0 {
break;
}
pos = pos.saturating_add(entry_size);
}
false
}
fn find_box_body<'a>(data: &'a [u8], path: &[&[u8; 4]]) -> Option<&'a [u8]> {
let mut slice = data;
for (i, target) in path.iter().enumerate() {
let found = find_direct_child(slice, target)?;
if i + 1 == path.len() {
return Some(found);
}
slice = found;
}
None
}
fn find_direct_child<'a>(data: &'a [u8], target: &[u8; 4]) -> Option<&'a [u8]> {
let mut pos = 0;
while pos + 8 <= data.len() {
let size =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
let btype = &data[pos + 4..pos + 8];
if size < 8 || pos.checked_add(size).is_none_or(|end| end > data.len()) {
return None;
}
if btype == target {
return Some(&data[pos + 8..pos + size]);
}
pos += size;
}
None
}
fn format_codec(track: &mp4::Mp4Track) -> String {
match track.media_type() {
Ok(mp4::MediaType::H264) => "h264".into(),
Ok(mp4::MediaType::H265) => "h265".into(),
Ok(mp4::MediaType::VP9) => "vp9".into(),
_ => "unknown".into(),
}
}
fn extract_sps_pps(reader: &Mp4Reader<Cursor<&[u8]>>, track_id: u32) -> Vec<Vec<u8>> {
let mut nalus = Vec::new();
if let Some(track) = reader.tracks().get(&track_id)
&& let Some(ref avc1) = track.trak.mdia.minf.stbl.stsd.avc1
{
for sps in &avc1.avcc.sequence_parameter_sets {
nalus.push(sps.bytes.to_vec());
}
for pps in &avc1.avcc.picture_parameter_sets {
nalus.push(pps.bytes.to_vec());
}
}
nalus
}
fn colour_to_pipeline(colour: &MkvColour) -> (ColorSpace, ColorMetadata, MkvColorInfo) {
let matrix_u8 = colour
.matrix_coefficients()
.map(matrix_coefficients_to_h273);
let primaries_u8 = colour.primaries().map(primaries_to_h273);
let transfer_u8 = colour.transfer_characteristics().map(transfer_to_h273);
let range = colour.range();
let color_space = match colour.matrix_coefficients() {
Some(MatrixCoefficients::Bt709) => ColorSpace::Bt709,
Some(MatrixCoefficients::Bt470bg) | Some(MatrixCoefficients::Smpte170) => ColorSpace::Bt601,
Some(MatrixCoefficients::Bt2020Ncl)
| Some(MatrixCoefficients::Bt2020Cl)
| Some(MatrixCoefficients::Bt2100) => ColorSpace::Bt2020,
_ => ColorSpace::Bt709,
};
let mastering = colour.mastering_metadata().map(mkv_mastering_to_local);
let mkv_max_cll = colour.max_cll().and_then(|v| u32::try_from(v).ok());
let mkv_max_fall = colour.max_fall().and_then(|v| u32::try_from(v).ok());
let unified_mastering = mastering.as_ref().and_then(mkv_mastering_to_unified);
let unified_cll = match (mkv_max_cll, mkv_max_fall) {
(None, None) => None,
(cll, fall) => Some(ContentLightLevel {
max_cll: cll.unwrap_or(0).min(u16::MAX as u32) as u16,
max_fall: fall.unwrap_or(0).min(u16::MAX as u32) as u16,
}),
};
let color_metadata = ColorMetadata {
transfer: transfer_u8.map(TransferFn::from_h273).unwrap_or_default(),
matrix_coefficients: matrix_u8.unwrap_or(1),
colour_primaries: primaries_u8.unwrap_or(1),
full_range: matches!(range, Some(MkvRange::Full)),
mastering_display: unified_mastering,
content_light_level: unified_cll,
};
let extra = MkvColorInfo {
bits_per_channel: colour.bits_per_channel().and_then(|v| u8::try_from(v).ok()),
chroma_subsampling_horz: colour
.chroma_subsampling_horz()
.and_then(|v| u8::try_from(v).ok()),
chroma_subsampling_vert: colour
.chroma_subsampling_vert()
.and_then(|v| u8::try_from(v).ok()),
chroma_siting_horz: colour.chroma_sitting_horz().map(chroma_siting_horz_to_u8),
chroma_siting_vert: colour.chroma_sitting_vert().map(chroma_siting_vert_to_u8),
max_cll: mkv_max_cll,
max_fall: mkv_max_fall,
mastering,
};
(color_space, color_metadata, extra)
}
fn mkv_mastering_to_unified(m: &MkvMasteringMetadata) -> Option<MasteringDisplay> {
if m.primary_r_chromaticity_x.is_none()
&& m.primary_g_chromaticity_x.is_none()
&& m.primary_b_chromaticity_x.is_none()
&& m.white_point_chromaticity_x.is_none()
&& m.luminance_max.is_none()
&& m.luminance_min.is_none()
{
return None;
}
let chrom = |v: Option<f64>| -> u16 {
let scaled = (v.unwrap_or(0.0) * 50_000.0).round();
scaled.clamp(0.0, u16::MAX as f64) as u16
};
let max_lum = (m.luminance_max.unwrap_or(0.0) * 10_000.0).round();
let min_lum = (m.luminance_min.unwrap_or(0.0) * 10_000.0).round();
Some(MasteringDisplay {
primaries_r_x: chrom(m.primary_r_chromaticity_x),
primaries_r_y: chrom(m.primary_r_chromaticity_y),
primaries_g_x: chrom(m.primary_g_chromaticity_x),
primaries_g_y: chrom(m.primary_g_chromaticity_y),
primaries_b_x: chrom(m.primary_b_chromaticity_x),
primaries_b_y: chrom(m.primary_b_chromaticity_y),
white_point_x: chrom(m.white_point_chromaticity_x),
white_point_y: chrom(m.white_point_chromaticity_y),
max_luminance: max_lum.clamp(0.0, u32::MAX as f64) as u32,
min_luminance: min_lum.clamp(0.0, u32::MAX as f64) as u32,
})
}
fn mkv_mastering_to_local(m: &MkvMastering) -> MkvMasteringMetadata {
MkvMasteringMetadata {
primary_r_chromaticity_x: m.primary_r_chromaticity_x(),
primary_r_chromaticity_y: m.primary_r_chromaticity_y(),
primary_g_chromaticity_x: m.primary_g_chromaticity_x(),
primary_g_chromaticity_y: m.primary_g_chromaticity_y(),
primary_b_chromaticity_x: m.primary_b_chromaticity_x(),
primary_b_chromaticity_y: m.primary_b_chromaticity_y(),
white_point_chromaticity_x: m.white_point_chromaticity_x(),
white_point_chromaticity_y: m.white_point_chromaticity_y(),
luminance_max: m.luminance_max(),
luminance_min: m.luminance_min(),
}
}
fn matrix_coefficients_to_h273(m: MatrixCoefficients) -> u8 {
match m {
MatrixCoefficients::Identity => 0,
MatrixCoefficients::Bt709 => 1,
MatrixCoefficients::Fcc73682 => 4,
MatrixCoefficients::Bt470bg => 5,
MatrixCoefficients::Smpte170 => 6,
MatrixCoefficients::Smpte240 => 7,
MatrixCoefficients::YCoCg => 8,
MatrixCoefficients::Bt2020Ncl => 9,
MatrixCoefficients::Bt2020Cl => 10,
MatrixCoefficients::SmpteSt2085 => 11,
MatrixCoefficients::ChromaDerivedNcl => 12,
MatrixCoefficients::ChromaDerivedCl => 13,
MatrixCoefficients::Bt2100 => 14,
MatrixCoefficients::Unknown => 2, }
}
fn transfer_to_h273(t: TransferCharacteristics) -> u8 {
match t {
TransferCharacteristics::Bt709 => 1,
TransferCharacteristics::Bt407m => 4,
TransferCharacteristics::Bt407bg => 5,
TransferCharacteristics::Smpte170 => 6,
TransferCharacteristics::Smpte240 => 7,
TransferCharacteristics::Linear => 8,
TransferCharacteristics::Log => 9,
TransferCharacteristics::LogSqrt => 10,
TransferCharacteristics::Iec61966_2_4 => 11,
TransferCharacteristics::Bt1361 => 12,
TransferCharacteristics::Iec61966_2_1 => 13,
TransferCharacteristics::Bt220_10 => 14,
TransferCharacteristics::Bt220_12 => 15,
TransferCharacteristics::Bt2100 => 16,
TransferCharacteristics::SmpteSt428_1 => 17,
TransferCharacteristics::Hlg => 18,
TransferCharacteristics::Unknown => 2,
}
}
fn primaries_to_h273(p: Primaries) -> u8 {
match p {
Primaries::Bt709 => 1,
Primaries::Bt470m => 4,
Primaries::Bt601 => 5,
Primaries::Smpte170 => 6,
Primaries::Smpte240 => 7,
Primaries::Film => 8,
Primaries::Bt2020 => 9,
Primaries::SmpteSt428_1 => 10,
Primaries::SmpteRp432_2 => 11,
Primaries::SmpteEg432_2 => 12,
Primaries::JedecP22 => 22,
Primaries::Unknown => 2,
}
}
fn chroma_siting_horz_to_u8(s: matroska_demuxer::ChromaSitingHorz) -> u8 {
match s {
matroska_demuxer::ChromaSitingHorz::LeftCollated => 1,
matroska_demuxer::ChromaSitingHorz::Half => 2,
matroska_demuxer::ChromaSitingHorz::Unknown => 0,
}
}
fn chroma_siting_vert_to_u8(s: matroska_demuxer::ChromaSitingVert) -> u8 {
match s {
matroska_demuxer::ChromaSitingVert::LeftCollated => 1,
matroska_demuxer::ChromaSitingVert::Half => 2,
matroska_demuxer::ChromaSitingVert::Unknown => 0,
}
}
fn bitrate_from_tags(tags: &[matroska_demuxer::Tag], track_uid: u64) -> Option<u64> {
let matches_track = |tag: &matroska_demuxer::Tag| -> bool {
match tag.targets() {
None => true, Some(t) => match t.tag_track_uid() {
None | Some(0) => true,
Some(uid) => uid == track_uid,
},
}
};
let mut segment_wide: Option<u64> = None;
let mut track_scoped: Option<u64> = None;
for tag in tags {
if !matches_track(tag) {
continue;
}
for st in tag.simple_tags() {
let name = st.name();
let is_bitrate = name.eq_ignore_ascii_case("BIT_RATE")
|| name.eq_ignore_ascii_case("BPS")
|| name.to_ascii_uppercase().starts_with("BPS-");
if !is_bitrate {
continue;
}
let Some(val) = st.string() else {
continue;
};
let Ok(parsed) = val.trim().parse::<u64>() else {
continue;
};
let is_track_scoped = tag
.targets()
.and_then(|t| t.tag_track_uid())
.map(|uid| uid == track_uid)
.unwrap_or(false);
if is_track_scoped {
track_scoped = Some(parsed);
} else if segment_wide.is_none() {
segment_wide = Some(parsed);
}
}
}
track_scoped.or(segment_wide)
}
pub fn probe_mkv_color_info(data: &[u8]) -> Option<MkvColorInfo> {
let cursor = Cursor::new(data);
let mkv = MatroskaFile::open(cursor).ok()?;
let track = mkv
.tracks()
.iter()
.find(|t| t.track_type() == MkvTrackType::Video)?;
let colour = track.video()?.colour()?;
let (_, _, mut info) = colour_to_pipeline(colour);
info.max_cll = None;
info.max_fall = None;
if let Some(local) = info.mastering.as_mut() {
local.primary_r_chromaticity_y = None;
local.primary_g_chromaticity_y = None;
local.primary_b_chromaticity_y = None;
}
if let Some(fix) = scan_mkv_colour_raw(data) {
info.max_cll = fix.max_cll;
info.max_fall = fix.max_fall;
if let Some(local) = info.mastering.as_mut() {
if fix.primary_r_chromaticity_y.is_some() {
local.primary_r_chromaticity_y = fix.primary_r_chromaticity_y;
}
if fix.primary_g_chromaticity_y.is_some() {
local.primary_g_chromaticity_y = fix.primary_g_chromaticity_y;
}
if fix.primary_b_chromaticity_y.is_some() {
local.primary_b_chromaticity_y = fix.primary_b_chromaticity_y;
}
}
}
Some(info)
}
#[derive(Default)]
struct RawColourFix {
max_cll: Option<u32>,
max_fall: Option<u32>,
primary_r_chromaticity_y: Option<f64>,
primary_g_chromaticity_y: Option<f64>,
primary_b_chromaticity_y: Option<f64>,
}
fn scan_mkv_colour_raw(data: &[u8]) -> Option<RawColourFix> {
let mut cursor = 0;
let seg_body: &[u8] = loop {
let (el, after) = next_ebml_element(data, cursor)?;
if el.id == 0x18538067 {
break &data[el.body_start..el.body_start + el.body_len];
}
cursor = after;
};
let tracks = find_ebml_child(seg_body, 0x1654AE6B)?;
let mut cur = 0;
while cur < tracks.len() {
let (el, after) = next_ebml_element(tracks, cur)?;
cur = after;
if el.id != 0xAE {
continue;
}
let entry = &tracks[el.body_start..el.body_start + el.body_len];
let Some(video) = find_ebml_child(entry, 0xE0) else {
continue;
};
let Some(colour) = find_ebml_child(video, 0x55B0) else {
continue;
};
let mut fix = RawColourFix::default();
let mut c = 0;
while c < colour.len() {
let (ce, after_ce) = match next_ebml_element(colour, c) {
Some(v) => v,
None => break,
};
c = after_ce;
let value_bytes = &colour[ce.body_start..ce.body_start + ce.body_len];
match ce.id {
0x55BC => {
fix.max_cll = read_unsigned(value_bytes).and_then(|v| u32::try_from(v).ok());
}
0x55BD => {
fix.max_fall = read_unsigned(value_bytes).and_then(|v| u32::try_from(v).ok());
}
0x55D0 => {
let md = value_bytes;
let mut mc = 0;
while mc < md.len() {
let (mce, after_mce) = match next_ebml_element(md, mc) {
Some(v) => v,
None => break,
};
mc = after_mce;
let mv = &md[mce.body_start..mce.body_start + mce.body_len];
match mce.id {
0x55D2 => fix.primary_r_chromaticity_y = read_float(mv),
0x55D4 => fix.primary_g_chromaticity_y = read_float(mv),
0x55D6 => fix.primary_b_chromaticity_y = read_float(mv),
_ => {}
}
}
}
_ => {}
}
}
if fix.max_cll.is_some()
|| fix.max_fall.is_some()
|| fix.primary_r_chromaticity_y.is_some()
|| fix.primary_g_chromaticity_y.is_some()
|| fix.primary_b_chromaticity_y.is_some()
{
return Some(fix);
}
}
None
}
fn find_ebml_child(buf: &[u8], want: u32) -> Option<&[u8]> {
let mut cur = 0;
while cur < buf.len() {
let (el, after) = next_ebml_element(buf, cur)?;
cur = after;
if el.id == want {
return Some(&buf[el.body_start..el.body_start + el.body_len]);
}
}
None
}
#[derive(Debug)]
struct RawEbmlElement {
id: u32,
body_start: usize,
body_len: usize,
}
fn next_ebml_element(buf: &[u8], off: usize) -> Option<(RawEbmlElement, usize)> {
if off >= buf.len() {
return None;
}
let (id, id_len) = read_id_vint(&buf[off..])?;
let body_off = off + id_len;
if body_off >= buf.len() {
return None;
}
let (size, size_len) = read_size_vint(&buf[body_off..])?;
let body_start = body_off + size_len;
if body_start + size as usize > buf.len() {
return None;
}
let elem = RawEbmlElement {
id,
body_start,
body_len: size as usize,
};
Some((elem, body_start + size as usize))
}
fn read_id_vint(buf: &[u8]) -> Option<(u32, usize)> {
if buf.is_empty() {
return None;
}
let first = buf[0];
let len = if first & 0x80 != 0 {
1
} else if first & 0x40 != 0 {
2
} else if first & 0x20 != 0 {
3
} else if first & 0x10 != 0 {
4
} else {
return None;
};
if buf.len() < len {
return None;
}
let mut id: u32 = 0;
for b in &buf[..len] {
id = (id << 8) | (*b as u32);
}
Some((id, len))
}
fn read_size_vint(buf: &[u8]) -> Option<(u64, usize)> {
if buf.is_empty() {
return None;
}
let first = buf[0];
if first == 0 {
return None;
}
let len = first.leading_zeros() as usize + 1;
if len > 8 || buf.len() < len {
return None;
}
let mask: u8 = if len == 8 { 0 } else { 0xFFu8 >> len };
let mut v: u64 = (first & mask) as u64;
for b in &buf[1..len] {
v = (v << 8) | (*b as u64);
}
Some((v, len))
}
fn read_unsigned(buf: &[u8]) -> Option<u64> {
if buf.len() > 8 {
return None;
}
let mut v: u64 = 0;
for b in buf {
v = (v << 8) | (*b as u64);
}
Some(v)
}
fn read_float(buf: &[u8]) -> Option<f64> {
match buf.len() {
4 => {
let arr: [u8; 4] = buf.try_into().ok()?;
Some(f32::from_be_bytes(arr) as f64)
}
8 => {
let arr: [u8; 8] = buf.try_into().ok()?;
Some(f64::from_be_bytes(arr))
}
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
struct FragSample {
offset: u64,
size: u32,
pts_ticks: i64,
duration_ticks: u32,
}
pub struct Mp4StreamingDemuxer {
data: Vec<u8>,
reader: Mp4Reader<Cursor<Vec<u8>>>,
header: DemuxHeader,
audio: Option<AudioTrack>,
track_id: u32,
sample_count: u32,
next_idx: u32,
sps_pps: Vec<Vec<u8>>,
length_size: u8,
tracker: Option<ParamSetTracker>,
fragmented_samples: Option<Vec<FragSample>>,
}
fn build_fragmented_sample_table(
data: &[u8],
track_id: u32,
default_sample_duration_from_trex: u32,
default_sample_size_from_trex: u32,
) -> Option<Vec<FragSample>> {
let mut samples: Vec<FragSample> = Vec::new();
let mut pos: usize = 0;
let mut accumulated_pts: i64 = 0;
let mut found_any_moof = false;
while pos + 8 <= data.len() {
let box_size_field = u32::from_be_bytes(data[pos..pos + 4].try_into().ok()?);
let box_type = &data[pos + 4..pos + 8];
let (box_size, header_size): (usize, usize) = if box_size_field == 1 {
if pos + 16 > data.len() {
break;
}
let big = u64::from_be_bytes(data[pos + 8..pos + 16].try_into().ok()?);
(big as usize, 16)
} else if box_size_field == 0 {
(data.len() - pos, 8)
} else {
(box_size_field as usize, 8)
};
if box_size < header_size || pos + box_size > data.len() {
break;
}
if box_type == b"moof" {
found_any_moof = true;
let moof_start = pos;
let moof_end = pos + box_size;
walk_moof(
data,
moof_start + header_size,
moof_end,
moof_start as u64,
track_id,
default_sample_duration_from_trex,
default_sample_size_from_trex,
&mut accumulated_pts,
&mut samples,
);
}
pos = pos
.checked_add(box_size)
.filter(|&n| n <= data.len())
.unwrap_or(data.len());
}
if found_any_moof { Some(samples) } else { None }
}
#[allow(clippy::too_many_arguments)]
fn walk_moof(
data: &[u8],
children_start: usize,
moof_end: usize,
moof_offset: u64,
track_id: u32,
default_sample_duration_from_trex: u32,
default_sample_size_from_trex: u32,
accumulated_pts: &mut i64,
samples: &mut Vec<FragSample>,
) {
let mut pos = children_start;
while pos + 8 <= moof_end {
let size = u32::from_be_bytes(match data[pos..pos + 4].try_into() {
Ok(b) => b,
Err(_) => break,
});
let typ = &data[pos + 4..pos + 8];
if size == 0 || size as usize + pos > moof_end {
break;
}
if typ == b"traf" {
walk_traf(
data,
pos + 8,
pos + size as usize,
moof_offset,
track_id,
default_sample_duration_from_trex,
default_sample_size_from_trex,
accumulated_pts,
samples,
);
}
pos += size as usize;
}
}
#[allow(clippy::too_many_arguments)]
fn walk_traf(
data: &[u8],
children_start: usize,
traf_end: usize,
moof_offset: u64,
track_id: u32,
default_sample_duration_from_trex: u32,
default_sample_size_from_trex: u32,
accumulated_pts: &mut i64,
samples: &mut Vec<FragSample>,
) {
let mut this_track: Option<u32> = None;
let mut tfhd_default_sample_duration: u32 = default_sample_duration_from_trex;
let mut tfhd_default_sample_size: u32 = default_sample_size_from_trex;
let mut base_data_offset: u64 = moof_offset; let mut base_data_offset_explicit = false;
let mut tfdt_base_pts: Option<i64> = None;
let mut pos = children_start;
while pos + 8 <= traf_end {
let size = u32::from_be_bytes(match data[pos..pos + 4].try_into() {
Ok(b) => b,
Err(_) => break,
});
let typ = &data[pos + 4..pos + 8];
if size == 0 || size as usize + pos > traf_end {
break;
}
if typ == b"tfhd" {
if pos + 16 > traf_end {
pos += size as usize;
continue;
}
let flags = u32::from_be_bytes(match data[pos + 8..pos + 12].try_into() {
Ok(b) => b,
Err(_) => break,
}) & 0x00ff_ffff;
let tk = u32::from_be_bytes(match data[pos + 12..pos + 16].try_into() {
Ok(b) => b,
Err(_) => break,
});
this_track = Some(tk);
let mut p = pos + 16;
if flags & 0x01 != 0 {
if p + 8 > traf_end {
break;
}
base_data_offset = u64::from_be_bytes(match data[p..p + 8].try_into() {
Ok(b) => b,
Err(_) => break,
});
base_data_offset_explicit = true;
p += 8;
}
if flags & 0x02 != 0 {
p += 4;
}
if flags & 0x08 != 0 {
if p + 4 > traf_end {
break;
}
tfhd_default_sample_duration =
u32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => break,
});
p += 4;
}
if flags & 0x10 != 0 {
if p + 4 > traf_end {
break;
}
tfhd_default_sample_size = u32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => break,
});
p += 4;
}
if flags & 0x20 != 0 {
p += 4;
}
let _ = p;
} else if typ == b"tfdt" {
if pos + 12 > traf_end {
pos += size as usize;
continue;
}
let version = data[pos + 8];
if version == 1 {
if pos + 20 > traf_end {
pos += size as usize;
continue;
}
let bmdt =
u64::from_be_bytes(data[pos + 12..pos + 20].try_into().unwrap_or([0; 8]));
tfdt_base_pts = Some(bmdt as i64);
} else {
let bmdt =
u32::from_be_bytes(data[pos + 12..pos + 16].try_into().unwrap_or([0; 4]));
tfdt_base_pts = Some(bmdt as i64);
}
}
pos += size as usize;
}
let Some(tk) = this_track else {
return;
};
if tk != track_id {
return;
}
if let Some(bp) = tfdt_base_pts {
*accumulated_pts = bp;
}
let mut pos = children_start;
while pos + 8 <= traf_end {
let size = u32::from_be_bytes(match data[pos..pos + 4].try_into() {
Ok(b) => b,
Err(_) => break,
});
let typ = &data[pos + 4..pos + 8];
if size == 0 || size as usize + pos > traf_end {
break;
}
if typ == b"trun" {
walk_trun(
data,
pos + 8,
pos + size as usize,
if base_data_offset_explicit {
base_data_offset
} else {
moof_offset
},
tfhd_default_sample_duration,
tfhd_default_sample_size,
accumulated_pts,
samples,
);
}
pos += size as usize;
}
let _ = base_data_offset_explicit;
}
#[allow(clippy::too_many_arguments)]
fn walk_trun(
data: &[u8],
children_start: usize,
trun_end: usize,
base_offset: u64,
default_sample_duration: u32,
default_sample_size: u32,
accumulated_pts: &mut i64,
samples: &mut Vec<FragSample>,
) {
if children_start + 8 > trun_end {
return;
}
let version = data[children_start];
let flags = u32::from_be_bytes(match data[children_start..children_start + 4].try_into() {
Ok(b) => b,
Err(_) => return,
}) & 0x00ff_ffff;
let sample_count = u32::from_be_bytes(
match data[children_start + 4..children_start + 8].try_into() {
Ok(b) => b,
Err(_) => return,
},
);
let mut p = children_start + 8;
let mut data_offset_in_trun: i32 = 0;
if flags & 0x000_001 != 0 {
if p + 4 > trun_end {
return;
}
data_offset_in_trun = i32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => return,
});
p += 4;
}
if flags & 0x000_004 != 0 {
p += 4;
}
let sample_duration_present = flags & 0x000_100 != 0;
let sample_size_present = flags & 0x000_200 != 0;
let sample_flags_present = flags & 0x000_400 != 0;
let sample_cto_present = flags & 0x000_800 != 0;
let mut current_offset = base_offset.wrapping_add(data_offset_in_trun as u64);
for _ in 0..sample_count {
let dur = if sample_duration_present {
if p + 4 > trun_end {
return;
}
let d = u32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => return,
});
p += 4;
d
} else {
default_sample_duration
};
let sz = if sample_size_present {
if p + 4 > trun_end {
return;
}
let s = u32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => return,
});
p += 4;
s
} else {
default_sample_size
};
if sample_flags_present {
p += 4;
}
let cto: i32 = if sample_cto_present {
if p + 4 > trun_end {
return;
}
let c = if version == 0 {
u32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => return,
}) as i32
} else {
i32::from_be_bytes(match data[p..p + 4].try_into() {
Ok(b) => b,
Err(_) => return,
})
};
p += 4;
c
} else {
0
};
if sz > 0 {
samples.push(FragSample {
offset: current_offset,
size: sz,
pts_ticks: accumulated_pts.saturating_add(cto as i64),
duration_ticks: dur,
});
}
current_offset = current_offset.saturating_add(sz as u64);
*accumulated_pts = accumulated_pts.saturating_add(dur as i64);
}
}
pub(crate) fn demux_mp4_streaming_init(data: &[u8]) -> Result<Mp4StreamingDemuxer> {
let owned = sanitize_isobmff_box_sizes(data);
let size = owned.len() as u64;
let probe = Mp4Reader::read_header(Cursor::new(owned.as_slice()), size)
.context("reading MP4 header")?;
let video_track = probe
.tracks()
.values()
.find(|t| t.track_type().ok() == Some(mp4::TrackType::Video))
.context("no video track in MP4")?;
let track_id = video_track.track_id();
let codec_from_mp4 = format_codec(video_track);
let codec = if codec_from_mp4 == "unknown" && has_av01_sample_entry(&owned) {
"av1".to_string()
} else if codec_from_mp4 == "unknown" && hevc_sample_entry_fourcc(&owned).is_some() {
"h265".to_string()
} else if codec_from_mp4 == "unknown" && prores_sample_entry_fourcc(&owned).is_some() {
"prores".to_string()
} else {
codec_from_mp4
};
let width = video_track.width() as u32;
let height = video_track.height() as u32;
let sample_count = video_track.sample_count();
let duration = video_track.duration().as_secs_f64();
let video_track_timescale = video_track.timescale();
let frame_rate = if duration > 0.0 {
sample_count as f64 / duration
} else {
30.0
};
let bitrate = video_track.bitrate() as u64;
let mp4_color = extract_mp4_visual_color_metadata(&owned);
let initial_color_metadata = ColorMetadata {
mastering_display: mp4_color.mastering_display,
content_light_level: mp4_color.content_light_level,
..Default::default()
};
let mut info = StreamInfo {
codec: codec.clone(),
width,
height,
frame_rate,
duration,
pixel_format: PixelFormat::Yuv420p,
color_space: ColorSpace::Bt709,
total_frames: sample_count as u64,
bitrate,
color_metadata: initial_color_metadata,
};
let needs_annexb = matches!(codec.as_str(), "h264" | "h265");
let (sps_pps, length_size) = if needs_annexb {
if codec == "h264" {
match extract_avc_config(&owned) {
Some(cfg) => (cfg.parameter_sets, cfg.length_size),
None => (extract_sps_pps(&probe, track_id), 4u8),
}
} else {
match extract_hevc_config(&owned) {
Some(cfg) => (cfg.parameter_sets, cfg.length_size),
None => (Vec::new(), 4u8),
}
}
} else {
(Vec::new(), 4u8)
};
if sample_count > 0 {
let mut probe_for_pf = Mp4Reader::read_header(Cursor::new(owned.as_slice()), size)
.context("re-reading MP4 for pixel-format probe")?;
if let Ok(Some(s)) = probe_for_pf.read_sample(track_id, 1) {
let first_sample = s.bytes.to_vec();
let detected_pf = codec::pixel_format::detect(&codec, &[first_sample]);
info.pixel_format = detected_pf;
}
}
drop(probe);
let audio = extract_mp4_audio(&owned);
let reader_cursor = Cursor::new(owned.clone());
let reader =
Mp4Reader::read_header(reader_cursor, size).context("opening MP4 streaming reader")?;
let tracker = if needs_annexb {
Some(ParamSetTracker::new(if codec == "h264" {
NaluCodec::Avc
} else {
NaluCodec::Hevc
}))
} else {
None
};
let _ = needs_annexb;
let fragmented_samples = build_fragmented_sample_table(&owned, track_id, 0, 0).map(|table| {
tracing::info!(
track_id,
sample_count = table.len(),
"fragmented MP4 detected; built sample table from moof/traf/trun"
);
table
});
let final_sample_count = match &fragmented_samples {
Some(table) => table.len() as u32,
None => sample_count,
};
if let Some(table) = fragmented_samples.as_ref() {
if !table.is_empty() && (sample_count == 0 || duration <= 0.0) && video_track_timescale > 0
{
let total_ticks: u64 = table.iter().map(|s| s.duration_ticks as u64).sum();
if total_ticks > 0 {
let total_seconds = total_ticks as f64 / video_track_timescale as f64;
if total_seconds > 0.0 {
let avg_fps = table.len() as f64 / total_seconds;
info.frame_rate = avg_fps.clamp(1.0, 240.0);
info.duration = total_seconds;
info.total_frames = table.len() as u64;
tracing::info!(
track_id,
avg_fps,
total_seconds,
sample_count = table.len(),
timescale = video_track_timescale,
"fragmented MP4: recomputed frame_rate + duration from \
moof/traf/trun timestamps (static moov sample table \
was empty)"
);
}
}
}
}
Ok(Mp4StreamingDemuxer {
data: owned,
reader,
header: DemuxHeader { codec, info },
audio,
track_id,
sample_count: final_sample_count,
next_idx: 1,
sps_pps,
length_size,
tracker,
fragmented_samples,
})
}
impl StreamingDemuxer for Mp4StreamingDemuxer {
fn header(&self) -> &DemuxHeader {
&self.header
}
fn next_video_sample(&mut self) -> Result<Option<Sample>> {
if let Some(table) = self.fragmented_samples.as_ref() {
let idx_zero_based = (self.next_idx - 1) as usize;
if idx_zero_based >= table.len() {
return Ok(None);
}
self.next_idx += 1;
let entry = table[idx_zero_based];
let off = entry.offset as usize;
let end = off.saturating_add(entry.size as usize);
if end > self.data.len() {
tracing::warn!(
idx = idx_zero_based + 1,
offset = entry.offset,
size = entry.size,
data_len = self.data.len(),
"fragmented sample reaches past EOF; stopping at the previous frame"
);
return Ok(None);
}
let raw = self.data[off..end].to_vec();
let data = if let Some(tracker) = self.tracker.as_mut() {
length_prefixed_to_annexb_tracked(&raw, self.length_size, tracker, &self.sps_pps)
} else {
raw
};
return Ok(Some(Sample {
data,
pts_ticks: entry.pts_ticks,
duration_ticks: entry.duration_ticks,
}));
}
loop {
if self.next_idx > self.sample_count {
return Ok(None);
}
let idx = self.next_idx;
self.next_idx += 1;
let s = match self.reader.read_sample(self.track_id, idx) {
Ok(s) => s,
Err(e) => {
tracing::warn!(
track_id = self.track_id,
idx,
emitted = idx.saturating_sub(1),
sample_count = self.sample_count,
error = %e,
"video stream: read_sample error mid-track; \
stopping at sample {} of {} (truncated source — \
iPhone fragmented MP4 with a missing trun entry \
is the typical cause)",
idx.saturating_sub(1),
self.sample_count,
);
return Ok(None);
}
};
let Some(sample) = s else { continue };
let pts_ticks = sample.start_time as i64;
let duration_ticks = sample.duration;
let raw = sample.bytes.to_vec();
let data = if let Some(tracker) = self.tracker.as_mut() {
length_prefixed_to_annexb_tracked(&raw, self.length_size, tracker, &self.sps_pps)
} else {
raw
};
return Ok(Some(Sample {
data,
pts_ticks,
duration_ticks,
}));
}
}
fn audio(&self) -> Option<&AudioTrack> {
self.audio.as_ref()
}
}
impl Mp4StreamingDemuxer {
#[allow(dead_code)]
pub(crate) fn raw_bytes(&self) -> &[u8] {
&self.data
}
}
pub struct MkvStreamingDemuxer {
mkv: MatroskaFile<Cursor<Vec<u8>>>,
header: DemuxHeader,
audio: Option<AudioTrack>,
track_number: u64,
timestamp_scale: u64,
annexb_prepend: Vec<Vec<u8>>,
length_size: u8,
tracker: Option<ParamSetTracker>,
default_duration_ns: Option<u64>,
pixel_format_detected: bool,
}
pub(crate) fn demux_mkv_streaming_init(data: &[u8]) -> Result<MkvStreamingDemuxer> {
let owned = data.to_vec();
let cursor = Cursor::new(owned.as_slice());
let probe =
MatroskaFile::open(cursor).map_err(|e| anyhow::anyhow!("reading MKV header: {e}"))?;
let (
track_number,
track_uid,
codec_id,
width,
height,
annexb_prepend,
length_size,
color_space,
mut color_metadata,
mut color_info,
track_default_duration_ns,
) = {
let track_info = probe
.tracks()
.iter()
.find(|t| t.track_type() == MkvTrackType::Video)
.context("no video track in MKV")?;
let track_number = track_info.track_number().get();
let track_uid = track_info.track_uid().get();
let codec_id = track_info.codec_id().to_string();
let default_duration_ns = track_info.default_duration().map(|d| d.get());
let (annexb_prepend, length_size): (Vec<Vec<u8>>, u8) = if codec_id == "V_MPEG4/ISO/AVC" {
let priv_bytes = track_info
.codec_private()
.context("V_MPEG4/ISO/AVC CodecPrivate missing")?;
let cfg = parse_avcc(priv_bytes).context("V_MPEG4/ISO/AVC CodecPrivate malformed")?;
(cfg.parameter_sets, cfg.length_size)
} else if codec_id == "V_MPEGH/ISO/HEVC" {
let priv_bytes = track_info
.codec_private()
.context("V_MPEGH/ISO/HEVC CodecPrivate missing")?;
let cfg = parse_hvcc(priv_bytes).context("V_MPEGH/ISO/HEVC CodecPrivate malformed")?;
(cfg.parameter_sets, cfg.length_size)
} else {
(Vec::new(), 4)
};
if mkv_codec_needs_annexb(&codec_id) && annexb_prepend.is_empty() {
bail!("AVC/HEVC MKV CodecPrivate missing or empty — no parameter sets to prepend");
}
let video = track_info
.video()
.context("video track missing Video element")?;
let w = video.pixel_width().get() as u32;
let h = video.pixel_height().get() as u32;
let (color_space, color_metadata, color_info) = match video.colour() {
Some(colour) => colour_to_pipeline(colour),
None => (
ColorSpace::Bt709,
ColorMetadata::default(),
MkvColorInfo::default(),
),
};
(
track_number,
track_uid,
codec_id,
w,
h,
annexb_prepend,
length_size,
color_space,
color_metadata,
color_info,
default_duration_ns,
)
};
color_info.max_cll = None;
color_info.max_fall = None;
color_metadata.content_light_level = None;
if let Some(md) = color_metadata.mastering_display.as_mut() {
md.primaries_r_y = 0;
md.primaries_g_y = 0;
md.primaries_b_y = 0;
}
if let Some(local) = color_info.mastering.as_mut() {
local.primary_r_chromaticity_y = None;
local.primary_g_chromaticity_y = None;
local.primary_b_chromaticity_y = None;
}
if let Some(fix) = scan_mkv_colour_raw(&owned) {
color_info.max_cll = fix.max_cll;
color_info.max_fall = fix.max_fall;
if fix.max_cll.is_some() || fix.max_fall.is_some() {
color_metadata.content_light_level = Some(ContentLightLevel {
max_cll: fix.max_cll.unwrap_or(0).min(u16::MAX as u32) as u16,
max_fall: fix.max_fall.unwrap_or(0).min(u16::MAX as u32) as u16,
});
}
let chrom = |v: f64| (v * 50_000.0).round().clamp(0.0, u16::MAX as f64) as u16;
if let Some(md) = color_metadata.mastering_display.as_mut() {
if let Some(y) = fix.primary_r_chromaticity_y {
md.primaries_r_y = chrom(y);
}
if let Some(y) = fix.primary_g_chromaticity_y {
md.primaries_g_y = chrom(y);
}
if let Some(y) = fix.primary_b_chromaticity_y {
md.primaries_b_y = chrom(y);
}
}
if let Some(local) = color_info.mastering.as_mut() {
if fix.primary_r_chromaticity_y.is_some() {
local.primary_r_chromaticity_y = fix.primary_r_chromaticity_y;
}
if fix.primary_g_chromaticity_y.is_some() {
local.primary_g_chromaticity_y = fix.primary_g_chromaticity_y;
}
if fix.primary_b_chromaticity_y.is_some() {
local.primary_b_chromaticity_y = fix.primary_b_chromaticity_y;
}
}
}
let needs_annexb = mkv_codec_needs_annexb(&codec_id);
let codec = match codec_id.as_str() {
"V_VP9" => "vp9".to_string(),
"V_VP8" => "vp8".to_string(),
"V_AV1" => "av1".to_string(),
"V_MPEG4/ISO/AVC" => "h264".to_string(),
"V_MPEGH/ISO/HEVC" => "h265".to_string(),
other => other.to_lowercase(),
};
let timestamp_scale = probe.info().timestamp_scale().get();
let duration_ticks = probe.info().duration().unwrap_or(0.0);
let duration = duration_ticks * (timestamp_scale as f64) / 1_000_000_000.0;
let tag_bitrate = probe
.tags()
.and_then(|tags| bitrate_from_tags(tags, track_uid));
if color_info != MkvColorInfo::default() {
tracing::info!(
bits_per_channel = ?color_info.bits_per_channel,
max_cll = ?color_info.max_cll,
max_fall = ?color_info.max_fall,
mastering = ?color_info.mastering,
"MKV Colour: parsed HDR-adjacent metadata"
);
}
drop(probe);
let audio = extract_mkv_audio(&owned);
let mkv = MatroskaFile::open(Cursor::new(owned.clone()))
.map_err(|e| anyhow::anyhow!("opening MKV streaming reader: {e}"))?;
let bitrate = tag_bitrate.unwrap_or(0);
let frame_rate = if let Some(dd_ns) = track_default_duration_ns.filter(|n| *n > 0) {
1_000_000_000.0 / dd_ns as f64
} else if duration > 0.0 {
30.0
} else {
30.0
};
let pixel_format = PixelFormat::Yuv420p;
let info = StreamInfo {
codec: codec.clone(),
width,
height,
frame_rate,
duration,
pixel_format,
color_space,
total_frames: 0, bitrate,
color_metadata,
};
let tracker = if needs_annexb {
Some(ParamSetTracker::new(if codec_id == "V_MPEG4/ISO/AVC" {
NaluCodec::Avc
} else {
NaluCodec::Hevc
}))
} else {
None
};
let _ = needs_annexb; Ok(MkvStreamingDemuxer {
mkv,
header: DemuxHeader { codec, info },
audio,
track_number,
timestamp_scale,
annexb_prepend,
length_size,
tracker,
default_duration_ns: track_default_duration_ns,
pixel_format_detected: false,
})
}
impl StreamingDemuxer for MkvStreamingDemuxer {
fn header(&self) -> &DemuxHeader {
&self.header
}
fn next_video_sample(&mut self) -> Result<Option<Sample>> {
let mut frame = MkvFrame::default();
loop {
match self.mkv.next_frame(&mut frame) {
Ok(true) => {
if frame.track != self.track_number {
continue;
}
let raw = std::mem::take(&mut frame.data);
let data = if let Some(tracker) = self.tracker.as_mut() {
length_prefixed_to_annexb_tracked(
&raw,
self.length_size,
tracker,
&self.annexb_prepend,
)
} else {
raw
};
if !self.pixel_format_detected {
let detected = codec::pixel_format::detect(
&self.header.codec,
std::slice::from_ref(&data),
);
self.header.info.pixel_format = detected;
self.pixel_format_detected = true;
}
let pts_ticks = frame.timestamp.saturating_mul(self.timestamp_scale) as i64;
let duration_ticks = frame
.duration
.or(self.default_duration_ns)
.map(|ns| ns.min(u32::MAX as u64) as u32)
.unwrap_or(0);
return Ok(Some(Sample {
data,
pts_ticks,
duration_ticks,
}));
}
Ok(false) => return Ok(None),
Err(e) => bail!("MKV frame read error: {e}"),
}
}
}
fn audio(&self) -> Option<&AudioTrack> {
self.audio.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mkv_annexb_guard_flags_avc_and_hevc() {
assert!(mkv_codec_needs_annexb("V_MPEG4/ISO/AVC"));
assert!(mkv_codec_needs_annexb("V_MPEGH/ISO/HEVC"));
}
#[test]
fn mkv_annexb_guard_passes_self_contained_codecs() {
assert!(!mkv_codec_needs_annexb("V_VP9"));
assert!(!mkv_codec_needs_annexb("V_VP8"));
assert!(!mkv_codec_needs_annexb("V_AV1"));
assert!(!mkv_codec_needs_annexb("V_UNKNOWN"));
}
#[test]
fn parse_avcc_extracts_sps_and_pps() {
let sps: [u8; 6] = [0x67, 0x42, 0x00, 0x1e, 0xab, 0x40];
let pps: [u8; 4] = [0x68, 0xce, 0x3c, 0x80];
let mut avcc = Vec::new();
avcc.push(0x01); avcc.push(0x42); avcc.push(0x00); avcc.push(0x1e); avcc.push(0xff); avcc.push(0xe1); avcc.extend_from_slice(&(sps.len() as u16).to_be_bytes());
avcc.extend_from_slice(&sps);
avcc.push(0x01); avcc.extend_from_slice(&(pps.len() as u16).to_be_bytes());
avcc.extend_from_slice(&pps);
let sets = parse_avcc_param_sets(&avcc);
assert_eq!(sets.len(), 2, "expected SPS + PPS");
assert_eq!(&sets[0], &sps);
assert_eq!(&sets[1], &pps);
}
#[test]
fn parse_avcc_truncated_returns_partial() {
let avcc: [u8; 6] = [0x01, 0x42, 0x00, 0x1e, 0xff, 0xe1];
let sets = parse_avcc_param_sets(&avcc);
assert!(sets.is_empty());
}
#[test]
fn parse_avcc_empty_record_returns_empty() {
assert!(parse_avcc_param_sets(&[]).is_empty());
assert!(parse_avcc_param_sets(&[0x01]).is_empty());
}
fn mkbox(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = (8 + payload.len()) as u32;
let mut out = Vec::with_capacity(size as usize);
out.extend_from_slice(&size.to_be_bytes());
out.extend_from_slice(fourcc);
out.extend_from_slice(payload);
out
}
#[test]
fn av01_detector_finds_sample_entry_in_nested_stsd() {
let mut stsd_body = vec![0u8; 8];
stsd_body.extend_from_slice(&mkbox(b"av01", &[0u8; 8]));
let stsd = mkbox(b"stsd", &stsd_body);
let stbl = mkbox(b"stbl", &stsd);
let minf = mkbox(b"minf", &stbl);
let mdia = mkbox(b"mdia", &minf);
let trak = mkbox(b"trak", &mdia);
let moov = mkbox(b"moov", &trak);
assert!(has_av01_sample_entry(&moov));
}
#[test]
fn av01_detector_ignores_av01_in_wrong_place() {
let mdat = mkbox(b"mdat", b"...av01... garbage");
assert!(!has_av01_sample_entry(&mdat));
}
#[test]
fn read_size_vint_8_byte_encoding() {
let size: u64 = 1000;
let v = (1u64 << 56) | size;
let bytes = v.to_be_bytes();
let (read, len) = read_size_vint(&bytes).expect("parse 8-byte size");
assert_eq!(len, 8);
assert_eq!(read, 1000);
}
#[test]
fn read_size_vint_1_byte_encoding() {
let (v, l) = read_size_vint(&[0x81]).expect("1-byte size");
assert_eq!(l, 1);
assert_eq!(v, 1);
}
#[test]
fn read_id_vint_parses_matroska_ids() {
assert_eq!(read_id_vint(&[0xAE]), Some((0xAE, 1)));
assert_eq!(
read_id_vint(&[0x1A, 0x45, 0xDF, 0xA3, 0xFF]),
Some((0x1A45DFA3, 4))
);
assert_eq!(read_id_vint(&[0x55, 0xB0, 0xFF]), Some((0x55B0, 2)));
}
#[test]
fn av01_detector_returns_false_for_avc1_sample_entry() {
let mut stsd_body = vec![0u8; 8];
stsd_body.extend_from_slice(&mkbox(b"avc1", &[0u8; 8]));
let stsd = mkbox(b"stsd", &stsd_body);
let stbl = mkbox(b"stbl", &stsd);
let minf = mkbox(b"minf", &stbl);
let mdia = mkbox(b"mdia", &minf);
let trak = mkbox(b"trak", &mdia);
let moov = mkbox(b"moov", &trak);
assert!(!has_av01_sample_entry(&moov));
}
fn mov_with_sample_entry(fourcc: &[u8; 4]) -> Vec<u8> {
let mut stsd_body = vec![0u8; 8]; stsd_body.extend_from_slice(&mkbox(fourcc, &[0u8; 8]));
let stsd = mkbox(b"stsd", &stsd_body);
let stbl = mkbox(b"stbl", &stsd);
let minf = mkbox(b"minf", &stbl);
let mdia = mkbox(b"mdia", &minf);
let trak = mkbox(b"trak", &mdia);
mkbox(b"moov", &trak)
}
#[test]
fn prores_detector_finds_all_six_fourccs() {
for fcc in [b"apco", b"apcs", b"apcn", b"apch", b"ap4h", b"ap4x"] {
let moov = mov_with_sample_entry(fcc);
let detected = prores_sample_entry_fourcc(&moov)
.unwrap_or_else(|| panic!("did not detect ProRes fourcc {fcc:?}"));
assert_eq!(&detected, fcc, "fourcc round-trip for {fcc:?}");
}
}
#[test]
fn prores_detector_ignores_non_prores_fourccs() {
for fcc in [b"avc1", b"hvc1", b"av01", b"vp09", b"mp4v"] {
let moov = mov_with_sample_entry(fcc);
assert!(
prores_sample_entry_fourcc(&moov).is_none(),
"false positive on fourcc {fcc:?}"
);
}
}
#[test]
fn prores_detector_returns_none_when_no_stsd() {
let moov = mkbox(b"moov", &[0u8; 4]);
assert!(prores_sample_entry_fourcc(&moov).is_none());
}
#[test]
fn detect_container_recognises_mpeg_ts_sync_pattern() {
let mut buf = vec![0xFFu8; 12];
buf[0] = 0x47;
while buf.len() < 400 {
buf.push(0x00);
}
buf[188] = 0x47;
buf[376] = 0x47;
assert_eq!(detect_container(&buf), "ts");
}
#[test]
fn detect_container_rejects_lone_0x47_byte() {
let mut buf = vec![0u8; 400];
buf[0] = 0x47;
buf[188] = 0x00; assert_ne!(detect_container(&buf), "ts");
}
#[test]
fn detect_container_recognises_avi_riff_signature() {
let mut buf: Vec<u8> = b"RIFF".to_vec();
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
buf.extend_from_slice(b"AVI ");
buf.extend_from_slice(&[0u8; 32]);
assert_eq!(detect_container(&buf), "avi");
}
}