mod atom;
mod boxes;
pub use atom::Mp4Atom;
pub use boxes::{
BoxHeader, BoxType, CttsEntry, FtypBox, MoovBox, MvhdBox, StscEntry, SttsEntry, TkhdBox,
TrakBox,
};
use std::io::SeekFrom;
use async_trait::async_trait;
use bytes::Bytes;
use oximedia_core::{CodecId, OxiError, OxiResult, Rational, Timestamp};
use oximedia_io::MediaSource;
use crate::demux::Demuxer;
use crate::DecodeSkipCursor;
use crate::{CodecParams, ContainerFormat, Metadata, Packet, PacketFlags, ProbeResult, StreamInfo};
#[allow(dead_code)]
pub struct Mp4Demuxer<R> {
source: R,
buffer: Vec<u8>,
ftyp: Option<FtypBox>,
moov: Option<MoovBox>,
streams: Vec<StreamInfo>,
tracks: Vec<TrackState>,
position: u64,
mdat_start: u64,
mdat_size: u64,
header_parsed: bool,
}
#[derive(Clone, Debug, Default)]
pub struct TrackState {
pub track_id: u32,
pub stream_index: usize,
pub sample_index: u32,
pub sample_count: u32,
pub samples: Vec<SampleInfo>,
}
#[derive(Clone, Debug)]
pub struct SampleInfo {
pub offset: u64,
pub size: u32,
pub duration: u32,
pub cts_offset: i32,
pub is_sync: bool,
}
impl<R> Mp4Demuxer<R> {
#[must_use]
pub fn new(source: R) -> Self {
Self {
source,
buffer: Vec::with_capacity(65536),
ftyp: None,
moov: None,
streams: Vec::new(),
tracks: Vec::new(),
position: 0,
mdat_start: 0,
mdat_size: 0,
header_parsed: false,
}
}
#[must_use]
pub const fn source(&self) -> &R {
&self.source
}
pub fn source_mut(&mut self) -> &mut R {
&mut self.source
}
#[must_use]
#[allow(dead_code)]
pub fn into_source(self) -> R {
self.source
}
#[must_use]
pub const fn ftyp(&self) -> Option<&FtypBox> {
self.ftyp.as_ref()
}
#[must_use]
pub const fn moov(&self) -> Option<&MoovBox> {
self.moov.as_ref()
}
#[must_use]
pub fn traks(&self) -> &[TrakBox] {
self.moov.as_ref().map_or(&[], |m| m.traks.as_slice())
}
}
impl<R: MediaSource> Mp4Demuxer<R> {
async fn read_n(&mut self, n: usize) -> OxiResult<Vec<u8>> {
let mut buf = vec![0u8; n];
let mut filled = 0usize;
while filled < n {
let count = self.source.read(&mut buf[filled..]).await?;
if count == 0 {
return Err(OxiError::UnexpectedEof);
}
filled += count;
}
self.position += n as u64;
Ok(buf)
}
async fn seek_to(&mut self, pos: u64) -> OxiResult<()> {
self.source.seek(SeekFrom::Start(pos)).await?;
self.position = pos;
Ok(())
}
async fn parse_headers(&mut self) -> OxiResult<()> {
self.seek_to(0).await?;
let mut ftyp_data: Option<Vec<u8>> = None;
let mut moov_data: Option<Vec<u8>> = None;
loop {
let mut header_buf = [0u8; 8];
let mut filled = 0usize;
loop {
let n = self.source.read(&mut header_buf[filled..]).await?;
if n == 0 {
break;
}
filled += n;
if filled == 8 {
break;
}
}
if filled < 8 {
break;
}
let size32 =
u32::from_be_bytes([header_buf[0], header_buf[1], header_buf[2], header_buf[3]]);
let box_type =
BoxType::new([header_buf[4], header_buf[5], header_buf[6], header_buf[7]]);
let (box_size, header_size): (u64, u64) = if size32 == 1 {
let mut ext = [0u8; 8];
let mut ef = 0usize;
while ef < 8 {
let n = self.source.read(&mut ext[ef..]).await?;
if n == 0 {
return Err(OxiError::UnexpectedEof);
}
ef += n;
}
let ext_size = u64::from_be_bytes(ext);
self.position += 16;
(ext_size, 16)
} else if size32 == 0 {
self.position += 8;
(0, 8)
} else {
self.position += 8;
(u64::from(size32), 8)
};
let content_size = if box_size == 0 {
0u64
} else {
box_size.saturating_sub(header_size)
};
match box_type {
BoxType::FTYP => {
if content_size > 0 && content_size <= 4096 {
let data = self.read_n(content_size as usize).await?;
ftyp_data = Some(data);
} else if content_size > 0 {
self.source
.seek(SeekFrom::Current(content_size as i64))
.await?;
self.position += content_size;
}
}
BoxType::MOOV => {
if content_size > 0 {
let data = self.read_n(content_size as usize).await?;
moov_data = Some(data);
}
}
BoxType::MDAT => {
self.mdat_start = self.position;
self.mdat_size = content_size;
if content_size > 0 {
self.source
.seek(SeekFrom::Current(content_size as i64))
.await?;
self.position += content_size;
}
}
BoxType::FREE | BoxType::SKIP | BoxType::UDTA | BoxType::META => {
if content_size > 0 {
self.source
.seek(SeekFrom::Current(content_size as i64))
.await?;
self.position += content_size;
}
}
_ => {
if content_size > 0 {
self.source
.seek(SeekFrom::Current(content_size as i64))
.await?;
self.position += content_size;
} else if box_size == 0 {
break;
}
}
}
if ftyp_data.is_some() && moov_data.is_some() {
break;
}
}
if let Some(ref data) = ftyp_data {
let ftyp = FtypBox::parse(data)?;
if !ftyp.is_mp4() {
return Err(OxiError::InvalidData(
"Not a valid MP4/ISOBMFF file (ftyp brand not recognized)".into(),
));
}
self.ftyp = Some(ftyp);
}
if let Some(ref data) = moov_data {
let moov = MoovBox::parse(data)?;
self.build_streams_and_tracks(&moov)?;
self.moov = Some(moov);
}
self.header_parsed = true;
Ok(())
}
fn build_streams_and_tracks(&mut self, moov: &MoovBox) -> OxiResult<()> {
let movie_timescale = moov.mvhd.as_ref().map_or(1000, |mvhd| mvhd.timescale);
let mut stream_index = 0usize;
for trak in &moov.traks {
if !matches!(trak.handler_type.as_str(), "vide" | "soun") {
continue;
}
let stream_info = match build_stream_info(stream_index, trak, movie_timescale) {
Ok(info) => info,
Err(OxiError::PatentViolation(_)) => {
return Err(build_stream_info(stream_index, trak, movie_timescale).unwrap_err());
}
Err(_) => {
continue;
}
};
let samples = build_sample_table(trak);
#[allow(clippy::cast_possible_truncation)]
let sample_count = samples.len() as u32;
let track_id = trak.tkhd.as_ref().map_or(0, |tkhd| tkhd.track_id);
let track_state = TrackState {
track_id,
stream_index,
sample_index: 0,
sample_count,
samples,
};
self.streams.push(stream_info);
self.tracks.push(track_state);
stream_index += 1;
}
Ok(())
}
async fn read_sample_data(&mut self, offset: u64, size: u32) -> OxiResult<Bytes> {
if size == 0 {
return Ok(Bytes::new());
}
self.source.seek(SeekFrom::Start(offset)).await?;
self.position = offset;
let data = self.read_n(size as usize).await?;
Ok(Bytes::from(data))
}
fn next_track_index(&self) -> Option<usize> {
let mut best: Option<(usize, u64)> = None;
for (i, track) in self.tracks.iter().enumerate() {
if track.sample_index >= track.sample_count {
continue;
}
let idx = track.sample_index as usize;
let dts: u64 = track.samples[..idx]
.iter()
.map(|s| u64::from(s.duration))
.sum();
match best {
Some((_, best_dts)) if dts < best_dts => {
best = Some((i, dts));
}
None => {
best = Some((i, dts));
}
_ => {}
}
}
best.map(|(i, _)| i)
}
fn sample_dts(track: &TrackState, sample_index: usize) -> u64 {
track.samples[..sample_index]
.iter()
.map(|s| u64::from(s.duration))
.sum()
}
fn sample_pts(track: &TrackState, sample_index: usize) -> Option<i64> {
let sample = track.samples.get(sample_index)?;
let dts = Self::sample_dts(track, sample_index);
let dts_i64 = i64::try_from(dts).ok()?;
Some(dts_i64 + i64::from(sample.cts_offset))
}
fn sample_accurate_cursor_for_track(
&self,
track: &TrackState,
target_pts: u64,
) -> OxiResult<DecodeSkipCursor> {
if track.samples.is_empty() {
return Err(OxiError::InvalidData("Track has no samples".into()));
}
let target_pts_i64 = i64::try_from(target_pts)
.map_err(|_| OxiError::InvalidData("Target PTS is out of range".into()))?;
let target_index = track
.samples
.iter()
.enumerate()
.find_map(|(index, _)| {
Self::sample_pts(track, index)
.filter(|&pts| pts >= target_pts_i64)
.map(|_| index)
})
.unwrap_or(track.samples.len().saturating_sub(1));
let keyframe_index = (0..=target_index)
.rev()
.find(|&index| track.samples[index].is_sync)
.unwrap_or(0);
let byte_offset = track.samples[keyframe_index].offset;
let skip_samples = u32::try_from(target_index.saturating_sub(keyframe_index))
.map_err(|_| OxiError::InvalidData("Sample skip count is out of range".into()))?;
Ok(DecodeSkipCursor {
byte_offset,
sample_index: keyframe_index,
skip_samples,
target_pts: target_pts_i64,
})
}
pub async fn seek_sample_accurate(&mut self, target_pts: u64) -> OxiResult<DecodeSkipCursor> {
if !self.header_parsed {
self.parse_headers().await?;
}
let track_index = self
.streams
.iter()
.position(StreamInfo::is_video)
.unwrap_or(0);
let track = self
.tracks
.get(track_index)
.ok_or_else(|| OxiError::InvalidData("No MP4 tracks available".into()))?;
self.sample_accurate_cursor_for_track(track, target_pts)
}
}
#[async_trait]
impl<R: MediaSource> Demuxer for Mp4Demuxer<R> {
async fn probe(&mut self) -> OxiResult<ProbeResult> {
if !self.header_parsed {
self.parse_headers().await?;
}
let confidence = if self.ftyp.is_some() && !self.streams.is_empty() {
0.99
} else if self.ftyp.is_some() || self.moov.is_some() {
0.95
} else {
0.90
};
Ok(ProbeResult::new(ContainerFormat::Mp4, confidence))
}
async fn read_packet(&mut self) -> OxiResult<Packet> {
if !self.header_parsed {
self.parse_headers().await?;
}
let track_idx = self.next_track_index().ok_or(OxiError::Eof)?;
let sample_idx = self.tracks[track_idx].sample_index as usize;
let sample = self.tracks[track_idx].samples[sample_idx].clone();
let stream_index = self.tracks[track_idx].stream_index;
let data = self.read_sample_data(sample.offset, sample.size).await?;
self.tracks[track_idx].sample_index += 1;
let stream = &self.streams[stream_index];
let timebase = stream.timebase;
let dts: i64 = self.tracks[track_idx].samples[..sample_idx]
.iter()
.map(|s| i64::from(s.duration))
.sum();
let pts = dts + i64::from(sample.cts_offset);
let mut timestamp = Timestamp::new(pts, timebase);
timestamp.dts = Some(dts);
timestamp.duration = Some(i64::from(sample.duration));
let mut flags = PacketFlags::empty();
if sample.is_sync {
flags |= PacketFlags::KEYFRAME;
}
Ok(Packet::new(stream_index, data, timestamp, flags))
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
}
pub fn map_codec(handler: &str, codec_tag: u32) -> OxiResult<CodecId> {
match (handler, codec_tag) {
("vide", 0x6176_3031) => Ok(CodecId::Av1),
("vide", 0x7670_3039) => Ok(CodecId::Vp9),
("vide", 0x7670_3038) => Ok(CodecId::Vp8),
("soun", 0x4F70_7573) => Ok(CodecId::Opus),
("soun", 0x664C_6143) => Ok(CodecId::Flac),
("soun", 0x766F_7262) => Ok(CodecId::Vorbis),
("vide", 0x6176_6331..=0x6176_6334) => Err(OxiError::PatentViolation("H.264/AVC".into())),
("vide", 0x6876_6331 | 0x6865_7631 | 0x6876_6332 | 0x6865_7632) => {
Err(OxiError::PatentViolation("H.265/HEVC".into()))
}
("vide", 0x7676_6331 | 0x7676_6931) => Err(OxiError::PatentViolation("H.266/VVC".into())),
("soun", 0x6D70_3461) => Err(OxiError::PatentViolation("AAC".into())),
("soun", 0x6163_2D33) => Err(OxiError::PatentViolation("AC-3".into())),
("soun", 0x6563_2D33) => Err(OxiError::PatentViolation("E-AC-3".into())),
("soun", 0x6474_7363 | 0x6474_7368 | 0x6474_736C | 0x6474_7365) => {
Err(OxiError::PatentViolation("DTS".into()))
}
("soun", 0x6D70_3320 | 0x2E6D_7033) => Err(OxiError::PatentViolation("MP3".into())),
_ => Err(OxiError::Unsupported(format!(
"Unknown MP4 codec: handler={handler}, tag=0x{codec_tag:08X} ({})",
tag_to_string(codec_tag)
))),
}
}
fn tag_to_string(tag: u32) -> String {
let bytes = tag.to_be_bytes();
String::from_utf8_lossy(&bytes).into_owned()
}
#[allow(dead_code)]
fn build_stream_info(index: usize, track: &TrakBox, movie_timescale: u32) -> OxiResult<StreamInfo> {
let codec = map_codec(&track.handler_type, track.codec_tag)?;
let timescale = if track.timescale > 0 {
track.timescale
} else {
movie_timescale
};
let mut stream = StreamInfo::new(index, codec, Rational::new(1, i64::from(timescale)));
match track.handler_type.as_str() {
"vide" => {
if let (Some(w), Some(h)) = (track.width, track.height) {
stream.codec_params = CodecParams::video(w, h);
}
}
"soun" => {
if let (Some(rate), Some(ch)) = (track.sample_rate, track.channels) {
stream.codec_params = CodecParams::audio(rate, u8::try_from(ch).unwrap_or(2));
}
}
_ => {}
}
if let Some(ref extra) = track.extradata {
stream.codec_params.extradata = Some(Bytes::copy_from_slice(extra));
}
if let Some(tkhd) = &track.tkhd {
if movie_timescale > 0 {
#[allow(clippy::cast_possible_wrap)]
let duration_in_stream_tb =
(tkhd.duration as i64 * i64::from(timescale)) / i64::from(movie_timescale);
stream.duration = Some(duration_in_stream_tb);
}
}
stream.metadata = Metadata::new();
Ok(stream)
}
#[allow(dead_code)]
fn build_sample_table(track: &TrakBox) -> Vec<SampleInfo> {
let mut samples = Vec::new();
let sample_count = if track.sample_sizes.is_empty() {
track
.stts_entries
.iter()
.map(|e| e.sample_count as usize)
.sum()
} else {
track.sample_sizes.len()
};
if sample_count == 0 {
return samples;
}
let sync_set: Option<std::collections::HashSet<u32>> = track
.sync_samples
.as_ref()
.map(|ss| ss.iter().copied().collect());
let mut chunk_sample_map: Vec<(u32, u32, u32)> = Vec::new(); let mut sample_num = 1u32;
for i in 0..track.stsc_entries.len() {
let entry = &track.stsc_entries[i];
let next_first_chunk = if i + 1 < track.stsc_entries.len() {
track.stsc_entries[i + 1].first_chunk
} else {
#[allow(clippy::cast_possible_truncation)]
let chunk_count = track.chunk_offsets.len() as u32 + 1;
chunk_count
};
for chunk in entry.first_chunk..next_first_chunk {
chunk_sample_map.push((sample_num, chunk, entry.samples_per_chunk));
sample_num += entry.samples_per_chunk;
}
}
let mut durations: Vec<u32> = Vec::with_capacity(sample_count);
for entry in &track.stts_entries {
for _ in 0..entry.sample_count {
durations.push(entry.sample_delta);
}
}
let mut cts_offsets: Vec<i32> = Vec::with_capacity(sample_count);
for entry in &track.ctts_entries {
for _ in 0..entry.sample_count {
cts_offsets.push(entry.sample_offset);
}
}
let mut current_chunk_idx = 0usize;
let mut sample_in_chunk = 0u32;
for sample_idx in 0..sample_count {
#[allow(clippy::cast_possible_truncation)]
let sample_num_1based = sample_idx as u32 + 1;
let chunk_offset = if current_chunk_idx < track.chunk_offsets.len() {
track.chunk_offsets[current_chunk_idx]
} else {
0
};
let samples_per_chunk = if current_chunk_idx < chunk_sample_map.len() {
chunk_sample_map[current_chunk_idx].2
} else {
1
};
let mut offset_in_chunk = 0u64;
let first_sample_in_chunk = sample_idx - sample_in_chunk as usize;
for i in first_sample_in_chunk..sample_idx {
let size = if track.default_sample_size > 0 {
track.default_sample_size
} else if i < track.sample_sizes.len() {
track.sample_sizes[i]
} else {
0
};
offset_in_chunk += u64::from(size);
}
let offset = chunk_offset + offset_in_chunk;
let size = if track.default_sample_size > 0 {
track.default_sample_size
} else if sample_idx < track.sample_sizes.len() {
track.sample_sizes[sample_idx]
} else {
0
};
let duration = durations.get(sample_idx).copied().unwrap_or(0);
let cts_offset = cts_offsets.get(sample_idx).copied().unwrap_or(0);
let is_sync = sync_set
.as_ref()
.map_or(true, |set| set.contains(&sample_num_1based));
samples.push(SampleInfo {
offset,
size,
duration,
cts_offset,
is_sync,
});
sample_in_chunk += 1;
if sample_in_chunk >= samples_per_chunk {
sample_in_chunk = 0;
current_chunk_idx += 1;
}
}
samples
}
#[cfg(test)]
mod tests {
use super::*;
use oximedia_io::MemorySource;
fn build_test_mp4() -> Vec<u8> {
fn u32be(v: u32) -> [u8; 4] {
v.to_be_bytes()
}
fn u16be(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
fn box_with_content(tag: &[u8; 4], content: &[u8]) -> Vec<u8> {
let size = 8u32 + content.len() as u32;
let mut b = Vec::with_capacity(size as usize);
b.extend_from_slice(&u32be(size));
b.extend_from_slice(tag);
b.extend_from_slice(content);
b
}
let mut mdhd = Vec::new();
mdhd.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); mdhd.extend_from_slice(&u32be(0)); mdhd.extend_from_slice(&u32be(0)); mdhd.extend_from_slice(&u32be(1000)); mdhd.extend_from_slice(&u32be(100)); mdhd.extend_from_slice(&[0x55, 0xC4]); mdhd.extend_from_slice(&[0x00, 0x00]); let mdhd_box = box_with_content(b"mdhd", &mdhd);
let mut hdlr = Vec::new();
hdlr.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); hdlr.extend_from_slice(&u32be(0)); hdlr.extend_from_slice(b"vide"); hdlr.extend_from_slice(&[0u8; 12]); hdlr.push(0x00); let hdlr_box = box_with_content(b"hdlr", &hdlr);
let mut entry_body = Vec::new();
entry_body.extend_from_slice(&[0u8; 6]); entry_body.extend_from_slice(&u16be(1)); entry_body.extend_from_slice(&[0u8; 2]); entry_body.extend_from_slice(&[0u8; 2]); entry_body.extend_from_slice(&[0u8; 12]); entry_body.extend_from_slice(&u16be(320)); entry_body.extend_from_slice(&u16be(240)); entry_body.extend_from_slice(&u32be(0x00480000)); entry_body.extend_from_slice(&u32be(0x00480000)); entry_body.extend_from_slice(&u32be(0)); entry_body.extend_from_slice(&u16be(1)); entry_body.extend_from_slice(&[0u8; 32]); entry_body.extend_from_slice(&u16be(0x0018)); entry_body.extend_from_slice(&[0xFF, 0xFF]);
let entry_size = 8u32 + entry_body.len() as u32;
let mut stsd_content = Vec::new();
stsd_content.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); stsd_content.extend_from_slice(&u32be(1)); stsd_content.extend_from_slice(&u32be(entry_size));
stsd_content.extend_from_slice(b"av01");
stsd_content.extend_from_slice(&entry_body);
let stsd_box = box_with_content(b"stsd", &stsd_content);
let mut stts = Vec::new();
stts.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); stts.extend_from_slice(&u32be(1)); stts.extend_from_slice(&u32be(1)); stts.extend_from_slice(&u32be(100)); let stts_box = box_with_content(b"stts", &stts);
let mut stsc = Vec::new();
stsc.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); stsc.extend_from_slice(&u32be(1)); stsc.extend_from_slice(&u32be(1)); stsc.extend_from_slice(&u32be(1)); stsc.extend_from_slice(&u32be(1)); let stsc_box = box_with_content(b"stsc", &stsc);
let mut stsz = Vec::new();
stsz.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); stsz.extend_from_slice(&u32be(4)); stsz.extend_from_slice(&u32be(1)); let stsz_box = box_with_content(b"stsz", &stsz);
let stco_placeholder_offset = 0u32;
let mut stco = Vec::new();
stco.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); stco.extend_from_slice(&u32be(1)); stco.extend_from_slice(&u32be(stco_placeholder_offset)); let stco_box = box_with_content(b"stco", &stco);
let mut stbl_content = Vec::new();
stbl_content.extend_from_slice(&stsd_box);
stbl_content.extend_from_slice(&stts_box);
stbl_content.extend_from_slice(&stsc_box);
stbl_content.extend_from_slice(&stsz_box);
stbl_content.extend_from_slice(&stco_box);
let stbl_box = box_with_content(b"stbl", &stbl_content);
let minf_box = box_with_content(b"minf", &stbl_box);
let mut mdia_content = Vec::new();
mdia_content.extend_from_slice(&mdhd_box);
mdia_content.extend_from_slice(&hdlr_box);
mdia_content.extend_from_slice(&minf_box);
let mdia_box = box_with_content(b"mdia", &mdia_content);
let mut tkhd = Vec::new();
tkhd.extend_from_slice(&[0x00, 0x00, 0x00, 0x03]); tkhd.extend_from_slice(&u32be(0)); tkhd.extend_from_slice(&u32be(0)); tkhd.extend_from_slice(&u32be(1)); tkhd.extend_from_slice(&u32be(0)); tkhd.extend_from_slice(&u32be(100)); tkhd.extend_from_slice(&[0u8; 8]); tkhd.extend_from_slice(&[0u8; 4]); tkhd.extend_from_slice(&[0u8; 4]); tkhd.extend_from_slice(&u32be(0x00010000)); tkhd.extend_from_slice(&u32be(0));
tkhd.extend_from_slice(&u32be(0));
tkhd.extend_from_slice(&u32be(0));
tkhd.extend_from_slice(&u32be(0x00010000)); tkhd.extend_from_slice(&u32be(0));
tkhd.extend_from_slice(&u32be(0));
tkhd.extend_from_slice(&u32be(0));
tkhd.extend_from_slice(&u32be(0x40000000)); tkhd.extend_from_slice(&u32be(320u32 << 16)); tkhd.extend_from_slice(&u32be(240u32 << 16)); let tkhd_box = box_with_content(b"tkhd", &tkhd);
let mut trak_content = Vec::new();
trak_content.extend_from_slice(&tkhd_box);
trak_content.extend_from_slice(&mdia_box);
let trak_box = box_with_content(b"trak", &trak_content);
let mut mvhd = Vec::new();
mvhd.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); mvhd.extend_from_slice(&u32be(0)); mvhd.extend_from_slice(&u32be(0)); mvhd.extend_from_slice(&u32be(1000)); mvhd.extend_from_slice(&u32be(100)); mvhd.extend_from_slice(&u32be(0x00010000)); mvhd.extend_from_slice(&u16be(0x0100)); mvhd.extend_from_slice(&[0u8; 2]); mvhd.extend_from_slice(&[0u8; 8]); mvhd.extend_from_slice(&u32be(0x00010000));
mvhd.extend_from_slice(&u32be(0));
mvhd.extend_from_slice(&u32be(0));
mvhd.extend_from_slice(&u32be(0));
mvhd.extend_from_slice(&u32be(0x00010000));
mvhd.extend_from_slice(&u32be(0));
mvhd.extend_from_slice(&u32be(0));
mvhd.extend_from_slice(&u32be(0));
mvhd.extend_from_slice(&u32be(0x40000000));
mvhd.extend_from_slice(&[0u8; 24]);
mvhd.extend_from_slice(&u32be(2)); let mvhd_box = box_with_content(b"mvhd", &mvhd);
let mut ftyp_content = Vec::new();
ftyp_content.extend_from_slice(b"isom"); ftyp_content.extend_from_slice(&u32be(0)); ftyp_content.extend_from_slice(b"isom"); let ftyp_box = box_with_content(b"ftyp", &ftyp_content);
let mut moov_content = Vec::new();
moov_content.extend_from_slice(&mvhd_box);
moov_content.extend_from_slice(&trak_box);
let moov_box = box_with_content(b"moov", &moov_content);
let mdat_payload = &[0xDE_u8, 0xAD, 0xBE, 0xEF];
let mdat_box = box_with_content(b"mdat", mdat_payload);
let mut file = Vec::new();
file.extend_from_slice(&ftyp_box);
file.extend_from_slice(&moov_box);
let mdat_offset = file.len() as u32 + 8; file.extend_from_slice(&mdat_box);
for i in 0..file.len().saturating_sub(16) {
if &file[i..i + 4] == b"stco" {
let offset_pos = i + 12;
if offset_pos + 4 <= file.len() {
let bytes = mdat_offset.to_be_bytes();
file[offset_pos..offset_pos + 4].copy_from_slice(&bytes);
}
break;
}
}
file
}
#[test]
fn test_mp4_demuxer_new() {
let source = MemorySource::new(bytes::Bytes::new());
let demuxer = Mp4Demuxer::new(source);
assert!(!demuxer.header_parsed);
assert!(demuxer.streams().is_empty());
assert!(demuxer.ftyp().is_none());
assert!(demuxer.moov().is_none());
}
#[tokio::test]
async fn test_mp4_demuxer_probe() {
let data = build_test_mp4();
let source = MemorySource::from_vec(data);
let mut demuxer = Mp4Demuxer::new(source);
let result = demuxer.probe().await.expect("probe should succeed");
assert_eq!(result.format, ContainerFormat::Mp4);
assert!(result.confidence > 0.8);
assert!(demuxer.header_parsed);
assert!(demuxer.ftyp().is_some());
assert!(demuxer.moov().is_some());
}
#[tokio::test]
async fn test_mp4_demuxer_streams() {
let data = build_test_mp4();
let source = MemorySource::from_vec(data);
let mut demuxer = Mp4Demuxer::new(source);
demuxer.probe().await.expect("probe should succeed");
let streams = demuxer.streams();
assert_eq!(streams.len(), 1);
assert_eq!(streams[0].codec, CodecId::Av1);
assert!(streams[0].is_video());
assert_eq!(streams[0].codec_params.width, Some(320));
assert_eq!(streams[0].codec_params.height, Some(240));
}
#[tokio::test]
async fn test_mp4_demuxer_read_packet() {
let data = build_test_mp4();
let source = MemorySource::from_vec(data);
let mut demuxer = Mp4Demuxer::new(source);
demuxer.probe().await.expect("probe should succeed");
let packet = demuxer.read_packet().await.expect("should read one packet");
assert_eq!(packet.stream_index, 0);
assert_eq!(packet.size(), 4);
assert_eq!(&packet.data[..], &[0xDE, 0xAD, 0xBE, 0xEF]);
assert!(packet.is_keyframe());
}
#[tokio::test]
async fn test_mp4_demuxer_eof() {
let data = build_test_mp4();
let source = MemorySource::from_vec(data);
let mut demuxer = Mp4Demuxer::new(source);
demuxer.probe().await.expect("probe should succeed");
let _ = demuxer.read_packet().await.expect("first packet");
let result = demuxer.read_packet().await;
assert!(matches!(result, Err(OxiError::Eof)));
}
#[test]
fn test_map_codec_av1() {
let codec = map_codec("vide", 0x6176_3031).expect("operation should succeed");
assert_eq!(codec, CodecId::Av1);
}
#[test]
fn test_map_codec_vp9() {
let codec = map_codec("vide", 0x7670_3039).expect("operation should succeed");
assert_eq!(codec, CodecId::Vp9);
}
#[test]
fn test_map_codec_opus() {
let codec = map_codec("soun", 0x4F70_7573).expect("operation should succeed");
assert_eq!(codec, CodecId::Opus);
}
#[test]
fn test_map_codec_flac() {
let codec = map_codec("soun", 0x664C_6143).expect("operation should succeed");
assert_eq!(codec, CodecId::Flac);
}
#[test]
fn test_map_codec_h264_rejected() {
let result = map_codec("vide", 0x6176_6331);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_patent_violation());
assert!(format!("{err}").contains("H.264"));
}
#[test]
fn test_map_codec_h265_rejected() {
let result = map_codec("vide", 0x6876_6331);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_patent_violation());
assert!(format!("{err}").contains("H.265"));
}
#[test]
fn test_map_codec_aac_rejected() {
let result = map_codec("soun", 0x6D70_3461);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_patent_violation());
assert!(format!("{err}").contains("AAC"));
}
#[test]
fn test_map_codec_ac3_rejected() {
let result = map_codec("soun", 0x6163_2D33);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_patent_violation());
assert!(format!("{err}").contains("AC-3"));
}
#[test]
fn test_map_codec_eac3_rejected() {
let result = map_codec("soun", 0x6563_2D33);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_patent_violation());
assert!(format!("{err}").contains("E-AC-3"));
}
#[test]
fn test_map_codec_dts_rejected() {
let result = map_codec("soun", 0x6474_7363);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_patent_violation());
assert!(format!("{err}").contains("DTS"));
}
#[test]
fn test_map_codec_unknown() {
let result = map_codec("vide", 0x1234_5678);
assert!(result.is_err());
match result.unwrap_err() {
OxiError::Unsupported(msg) => {
assert!(msg.contains("Unknown MP4 codec"));
}
other => panic!("Expected Unsupported error, got: {other:?}"),
}
}
#[test]
fn test_tag_to_string() {
assert_eq!(tag_to_string(0x6176_3031), "av01");
assert_eq!(tag_to_string(0x6D70_3461), "mp4a");
}
#[test]
fn test_build_stream_info_av1() {
let mut track = TrakBox::default();
track.handler_type = "vide".into();
track.codec_tag = 0x6176_3031; track.timescale = 24000;
track.width = Some(1920);
track.height = Some(1080);
track.tkhd = Some(TkhdBox {
track_id: 1,
duration: 240_000,
width: 1920.0,
height: 1080.0,
});
let stream = build_stream_info(0, &track, 1000).expect("operation should succeed");
assert_eq!(stream.codec, CodecId::Av1);
assert_eq!(stream.codec_params.width, Some(1920));
assert_eq!(stream.codec_params.height, Some(1080));
assert!(stream.is_video());
}
#[test]
fn test_build_stream_info_opus() {
let mut track = TrakBox::default();
track.handler_type = "soun".into();
track.codec_tag = 0x4F70_7573; track.timescale = 48000;
track.sample_rate = Some(48000);
track.channels = Some(2);
let stream = build_stream_info(0, &track, 1000).expect("operation should succeed");
assert_eq!(stream.codec, CodecId::Opus);
assert_eq!(stream.codec_params.sample_rate, Some(48000));
assert_eq!(stream.codec_params.channels, Some(2));
assert!(stream.is_audio());
}
#[test]
fn test_build_stream_info_rejected() {
let mut track = TrakBox::default();
track.handler_type = "vide".into();
track.codec_tag = 0x6176_6331;
let result = build_stream_info(0, &track, 1000);
assert!(result.is_err());
assert!(result.unwrap_err().is_patent_violation());
}
#[test]
fn test_build_sample_table_basic() {
let mut track = TrakBox::default();
track.default_sample_size = 1000;
track.stts_entries = vec![SttsEntry {
sample_count: 10,
sample_delta: 100,
}];
track.stsc_entries = vec![StscEntry {
first_chunk: 1,
samples_per_chunk: 5,
sample_description_index: 1,
}];
track.chunk_offsets = vec![0, 5000];
track.sync_samples = None;
let samples = build_sample_table(&track);
assert_eq!(samples.len(), 10);
assert_eq!(samples[0].offset, 0);
assert_eq!(samples[0].size, 1000);
assert_eq!(samples[0].duration, 100);
assert!(samples[0].is_sync);
assert_eq!(samples[4].offset, 4000);
assert_eq!(samples[5].offset, 5000);
}
}