use std::io::{BufReader, Read, Seek, SeekFrom};
use std::time::Duration;
use super::{Codec, Demuxer, Metadata, Packet, PcmFormat, StreamInfo, StreamKind};
use crate::source::MediaSource;
use crate::{Error, Result};
struct SampleRef {
stream: u32,
offset: u64,
size: u64,
pts: Duration,
dts: Duration,
keyframe: bool,
}
pub struct Mp4Demuxer {
src: Box<dyn MediaSource>,
streams: Vec<StreamInfo>,
samples: Vec<SampleRef>,
cursor: usize,
video_track: Option<u32>,
metadata: Option<Metadata>,
}
impl Mp4Demuxer {
pub fn open(mut src: Box<dyn MediaSource>) -> Result<Self> {
if !src.is_seekable() {
return Err(Error::Unsupported("MP4 from a non-seekable source"));
}
let size = src.byte_len().ok_or(Error::Unsupported("MP4 from a source of unknown length"))?;
let (moov_start, moov) = read_moov(src.as_mut(), size).unwrap_or_default();
let mut meta = Metadata::default();
super::tags::mp4::parse_moov_tags(&moov, &mut meta);
let hide = udta_offset(&moov).map(|at| moov_start + at + 4);
src.seek(SeekFrom::Start(0))?;
let mp4 = re_mp4::Mp4::read(BufReader::new(HideBox { src: src.as_mut(), hide, pos: 0 }), size)
.map_err(|e| Error::Demux(format!("mp4: {e}")))?;
let mut streams = Vec::new();
let mut samples = Vec::new();
let mut video_unapplied_skip = Duration::ZERO;
for track in mp4.tracks().values() {
let kind = match track.kind {
Some(re_mp4::TrackKind::Video) => StreamKind::Video,
Some(re_mp4::TrackKind::Audio) => StreamKind::Audio,
_ => StreamKind::Other,
};
let codec = track
.codec_string(&mp4)
.map(|s| Codec::from_mp4_codec_string(&s))
.unwrap_or_else(|| Codec::Other("unknown".into()));
let timescale = track.timescale.max(1);
let trak = track.trak(&mp4);
let edit_skip = trak.edts.as_ref().and_then(|e| e.elst.as_ref()).and_then(|elst| {
leading_skip(elst.entries.iter().map(|e| e.media_time))
});
let fragmented = trak.mdia.minf.stbl.stsz.sample_count == 0 && !track.samples.is_empty();
let shift = match (kind, edit_skip) {
(StreamKind::Video, Some(skip)) if fragmented => skip as i64,
_ => 0,
};
if kind == StreamKind::Video
&& !fragmented
&& let Some(skip) = edit_skip
{
let stbl = &trak.mdia.minf.stbl;
let stts: Vec<(u32, u32)> = stbl.stts.entries.iter().map(|e| (e.sample_count, e.sample_delta)).collect();
let ctts: Vec<(u32, i32)> =
stbl.ctts.iter().flat_map(|c| &c.entries).map(|e| (e.sample_count, e.sample_offset)).collect();
let unapplied = skip as i64 - min_composition(&stts, &ctts);
if unapplied > 0 {
video_unapplied_skip = video_unapplied_skip.max(ticks(unapplied, timescale));
}
}
let mut info = StreamInfo::new(track.track_id, kind, codec);
info.width = track.width as u32;
info.height = track.height as u32;
info.duration = Some(ticks(track.duration as i64, timescale));
info.extradata = track.raw_codec_config(&mp4);
if let re_mp4::StsdBoxContent::Mp4a(mp4a) = &track.trak(&mp4).mdia.minf.stbl.stsd.contents {
let object_type = mp4a_esds(&moov, track.track_id).and_then(parse_esds).map(|(t, _)| t);
info.codec = match object_type {
Some(0x69 | 0x6B) => Codec::Mp3,
_ => Codec::Aac,
};
info.sample_rate = mp4a.samplerate.value() as u32;
info.channels = mp4a.channelcount;
info.extradata = raw_audio_specific_config(&moov, track.track_id).or_else(|| {
mp4a.esds.as_ref().map(|esds| {
let d = &esds.es_desc.dec_config.dec_specific;
audio_specific_config(d.profile, d.freq_index, d.chan_conf)
})
});
if let Some(skip) = edit_skip {
info.codec_delay = ticks(skip as i64, timescale);
}
}
if matches!(info.codec, Codec::Other(_))
&& let Some(fourcc) = sample_entry_fourcc(&moov, track.track_id)
&& let Codec::ProRes = Codec::from_mp4_codec_string(&String::from_utf8_lossy(&fourcc))
{
info.codec = Codec::ProRes;
info.kind = StreamKind::Video;
}
if matches!(info.codec, Codec::Other(_))
&& let Some((fourcc, entry)) = sample_entry(&moov, track.track_id)
&& let Some((codec, extradata)) = compressed_audio_entry(fourcc, entry)
{
info.codec = codec;
info.kind = StreamKind::Audio;
info.extradata = extradata;
let u16_at = |at: usize| entry.get(at..at + 2).map(|b| u16::from_be_bytes([b[0], b[1]]));
info.channels = u16_at(16).unwrap_or(0);
info.sample_rate = u16_at(24).unwrap_or(0) as u32;
}
if matches!(info.codec, Codec::Other(_))
&& let Some((fourcc, entry)) = sample_entry(&moov, track.track_id)
&& let Some((format, channels, rate)) = pcm_entry(fourcc, entry)
{
info.codec = Codec::Pcm(format);
info.kind = StreamKind::Audio;
(info.channels, info.sample_rate) = (channels, rate);
}
if info.kind == StreamKind::Other {
info.kind = match handler_type(&moov, track.track_id) {
Some(b"soun") => StreamKind::Audio,
Some(b"vide") => StreamKind::Video,
_ => StreamKind::Other,
};
}
let track_samples = track.samples.iter().map(|s| SampleRef {
stream: track.track_id,
offset: s.offset,
size: s.size,
pts: ticks(s.composition_timestamp - shift, s.timescale.max(1)),
dts: ticks(s.decode_timestamp - shift, s.timescale.max(1)),
keyframe: s.is_sync,
});
match info.codec {
Codec::Pcm(f) => {
let frame = (f.bits as u64 / 8 * info.channels as u64).max(1);
samples.extend(merge_contiguous(track_samples, frame * PCM_PACKET_FRAMES));
}
_ => samples.extend(track_samples),
}
streams.push(info);
}
for s in streams.iter_mut().filter(|s| s.kind == StreamKind::Audio) {
s.codec_delay = s.codec_delay.saturating_sub(video_unapplied_skip);
}
let samples = interleave_by_time(samples);
let video_track = streams.iter().find(|s| s.kind == StreamKind::Video).map(|s| s.id);
Ok(Self { src, streams, samples, cursor: 0, video_track, metadata: (!meta.is_empty()).then_some(meta) })
}
}
impl Demuxer for Mp4Demuxer {
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn metadata(&self) -> Option<&Metadata> {
self.metadata.as_ref()
}
fn next_packet(&mut self) -> Result<Option<Packet>> {
let Some(s) = self.samples.get(self.cursor) else {
return Ok(None);
};
self.cursor += 1;
let mut data = vec![0u8; s.size as usize];
self.src.seek(SeekFrom::Start(s.offset))?;
self.src.read_exact(&mut data)?;
Ok(Some(Packet { stream: s.stream, pts: s.pts, keyframe: s.keyframe, data, generation: 0 }))
}
fn seek(&mut self, target: Duration) -> Result<Duration> {
let video = self.video_track;
let is_video_key = |s: &SampleRef| Some(s.stream) == video && s.keyframe;
let key = self
.samples
.iter()
.enumerate()
.filter(|(_, s)| is_video_key(s) && s.pts <= target)
.max_by_key(|(_, s)| s.pts)
.or_else(|| self.samples.iter().enumerate().find(|(_, s)| is_video_key(s)))
.map(|(i, _)| i)
.unwrap_or(0);
let mut index = key;
while index > 0 && self.samples[index - 1].dts == self.samples[key].dts {
index -= 1;
}
self.cursor = index;
Ok(self.samples.get(key).map(|s| s.pts).unwrap_or_default())
}
}
const PCM_PACKET_FRAMES: u64 = 2048;
fn merge_contiguous(samples: impl Iterator<Item = SampleRef>, max_bytes: u64) -> Vec<SampleRef> {
let mut out: Vec<SampleRef> = Vec::new();
for s in samples {
if let Some(last) = out.last_mut()
&& last.offset + last.size == s.offset
&& last.size + s.size <= max_bytes
{
last.size += s.size;
continue;
}
out.push(s);
}
out
}
fn interleave_by_time(mut samples: Vec<SampleRef>) -> Vec<SampleRef> {
samples.sort_by_key(|s| (s.dts, s.stream));
samples
}
fn udta_offset(moov: &[u8]) -> Option<u64> {
let mut at = 0usize;
while at + 8 <= moov.len() {
let size = match u32::from_be_bytes(moov[at..at + 4].try_into().ok()?) as usize {
1 => u64::from_be_bytes(moov.get(at + 8..at + 16)?.try_into().ok()?) as usize,
0 => moov.len() - at,
s => s,
};
if &moov[at + 4..at + 8] == b"udta" {
return Some(at as u64);
}
if size < 8 || size > moov.len() - at {
return None;
}
at += size;
}
None
}
struct HideBox<'a> {
src: &'a mut dyn MediaSource,
hide: Option<u64>,
pos: u64,
}
impl Read for HideBox<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.src.read(buf)?;
if let Some(hide) = self.hide {
for (i, &b) in b"free".iter().enumerate() {
let at = hide + i as u64;
if (self.pos..self.pos + n as u64).contains(&at) {
buf[(at - self.pos) as usize] = b;
}
}
}
self.pos += n as u64;
Ok(n)
}
}
impl Seek for HideBox<'_> {
fn seek(&mut self, to: SeekFrom) -> std::io::Result<u64> {
self.pos = self.src.seek(to)?;
Ok(self.pos)
}
}
fn read_moov(src: &mut dyn MediaSource, size: u64) -> Option<(u64, Vec<u8>)> {
let mut pos = 0u64;
while pos + 8 <= size {
src.seek(SeekFrom::Start(pos)).ok()?;
let mut head = [0u8; 16];
src.read_exact(&mut head[..8]).ok()?;
let mut len = u32::from_be_bytes(head[..4].try_into().ok()?) as u64;
let mut header = 8;
if len == 1 {
src.read_exact(&mut head[8..16]).ok()?;
len = u64::from_be_bytes(head[8..16].try_into().ok()?);
header = 16;
} else if len == 0 {
len = size - pos;
}
if len < header {
return None;
}
if &head[4..8] == b"moov" {
let body = len - header;
if body > 256 << 20 {
return None;
}
let mut moov = vec![0u8; body as usize];
src.read_exact(&mut moov).ok()?;
return Some((pos + header, moov));
}
pos += len;
}
None
}
fn boxes(data: &[u8]) -> impl Iterator<Item = (&[u8], &[u8])> {
let mut pos = 0;
std::iter::from_fn(move || {
let head = data.get(pos..pos + 8)?;
let len = u32::from_be_bytes(head[..4].try_into().ok()?) as usize;
let (start, end) = match len {
1 => {
let big = u64::from_be_bytes(data.get(pos + 8..pos + 16)?.try_into().ok()?) as usize;
(pos + 16, pos.checked_add(big)?)
}
0 => (pos + 8, data.len()),
n if n >= 8 => (pos + 8, pos + n),
_ => return None,
};
let payload = data.get(start..end)?;
let kind = &head[4..8];
pos = end;
Some((kind, payload))
})
}
fn child<'a>(data: &'a [u8], kind: &[u8]) -> Option<&'a [u8]> {
boxes(data).find(|(k, _)| *k == kind).map(|(_, p)| p)
}
fn sample_entries(moov: &[u8], track_id: u32) -> Option<&[u8]> {
let trak = trak(moov, track_id)?;
let stsd = child(child(child(child(trak, b"mdia")?, b"minf")?, b"stbl")?, b"stsd")?;
stsd.get(8..)
}
fn trak(moov: &[u8], track_id: u32) -> Option<&[u8]> {
boxes(moov).filter(|(k, _)| *k == b"trak").map(|(_, p)| p).find(|trak| {
child(trak, b"tkhd").and_then(|tkhd| {
let at = if tkhd.first() == Some(&1) { 20 } else { 12 };
Some(u32::from_be_bytes(tkhd.get(at..at + 4)?.try_into().ok()?))
}) == Some(track_id)
})
}
fn handler_type(moov: &[u8], track_id: u32) -> Option<&[u8; 4]> {
let hdlr = child(child(trak(moov, track_id)?, b"mdia")?, b"hdlr")?;
hdlr.get(8..12)?.try_into().ok()
}
fn sample_entry(moov: &[u8], track_id: u32) -> Option<(&[u8], &[u8])> {
boxes(sample_entries(moov, track_id)?).next()
}
fn compressed_audio_entry(fourcc: &[u8], e: &[u8]) -> Option<(Codec, Option<Vec<u8>>)> {
let kids = e.get(28..).unwrap_or_default();
match fourcc {
b"alac" => Some((Codec::Alac, child(kids, b"alac").and_then(|c| c.get(4..)).map(<[u8]>::to_vec))),
b"ac-3" => Some((Codec::Ac3, None)),
b"ec-3" => Some((Codec::Eac3, None)),
b"fLaC" => Some((
Codec::Flac,
child(kids, b"dfLa").and_then(|d| d.get(4..)).map(|blocks| [b"fLaC".as_slice(), blocks].concat()),
)),
_ => None,
}
}
fn pcm_entry(fourcc: &[u8], e: &[u8]) -> Option<(PcmFormat, u16, u32)> {
let u16_at = |at: usize| Some(u16::from_be_bytes(e.get(at..at + 2)?.try_into().ok()?));
let u32_at = |at: usize| Some(u32::from_be_bytes(e.get(at..at + 4)?.try_into().ok()?));
let version = u16_at(8)?;
let (mut channels, sample_size, mut rate) = (u16_at(16)?, u16_at(18)?, u32_at(24)? >> 16);
let children = match version {
0 => 28,
1 => 44,
2 => 64,
_ => return None,
};
let kids = e.get(children..).unwrap_or_default();
let little = child(kids, b"wave").and_then(|w| child(w, b"enda")).and_then(|v| v.get(..2)).is_some_and(|v| v != [0, 0]);
let format = match fourcc {
b"sowt" => PcmFormat::int(sample_size.max(16), false, true),
b"twos" => PcmFormat::int(sample_size.max(8), true, true),
b"raw " => PcmFormat::int(8, false, false),
b"in24" => PcmFormat::int(24, !little, true),
b"in32" => PcmFormat::int(32, !little, true),
b"fl32" => PcmFormat::float(32, !little),
b"fl64" => PcmFormat::float(64, !little),
b"lpcm" if version == 2 => {
rate = f64::from_bits(u64::from_be_bytes(e.get(32..40)?.try_into().ok()?)) as u32;
channels = u32_at(40)? as u16;
let (bits, flags) = (u32_at(48)? as u16, u32_at(52)?);
match flags & 1 != 0 {
true => PcmFormat::float(bits, flags & 2 != 0),
false => PcmFormat::int(bits, flags & 2 != 0, flags & 4 != 0 || bits > 8),
}
}
b"ipcm" | b"fpcm" => {
let pcmc = child(kids, b"pcmC")?;
let (flags, bits) = (*pcmc.get(4)?, *pcmc.get(5)? as u16);
match fourcc == b"fpcm" {
true => PcmFormat::float(bits, flags & 1 == 0),
false => PcmFormat::int(bits, flags & 1 == 0, true),
}
}
_ => return None,
};
let supported = if format.float { matches!(format.bits, 32 | 64) } else { matches!(format.bits, 8 | 16 | 24 | 32) };
(supported && channels > 0 && rate > 0).then_some((format, channels, rate))
}
fn sample_entry_fourcc(moov: &[u8], track_id: u32) -> Option<[u8; 4]> {
let (kind, _) = boxes(sample_entries(moov, track_id)?).next()?;
kind.try_into().ok()
}
fn mp4a_esds(moov: &[u8], track_id: u32) -> Option<&[u8]> {
let mp4a = child(sample_entries(moov, track_id)?, b"mp4a")?;
child(mp4a.get(28..)?, b"esds")
}
fn raw_audio_specific_config(moov: &[u8], track_id: u32) -> Option<Vec<u8>> {
let mp4a = child(sample_entries(moov, track_id)?, b"mp4a")?;
parse_esds_asc(child(mp4a.get(28..)?, b"esds")?)
}
fn min_composition(stts: &[(u32, u32)], ctts: &[(u32, i32)]) -> i64 {
let offsets = ctts.iter().flat_map(|&(n, o)| std::iter::repeat_n(o as i64, n as usize));
let mut dts = 0i64;
let decode_times = stts.iter().flat_map(|&(n, d)| std::iter::repeat_n(d as i64, n as usize)).map(move |d| {
let t = dts;
dts += d;
t
});
let mut offsets = offsets.chain(std::iter::repeat(0));
decode_times.map(|t| t + offsets.next().unwrap()).min().unwrap_or(0)
}
fn leading_skip(media_times: impl IntoIterator<Item = u64>) -> Option<u64> {
media_times.into_iter().find(|&t| t != u32::MAX as u64 && t != u64::MAX).filter(|&t| t > 0)
}
fn parse_esds_asc(esds: &[u8]) -> Option<Vec<u8>> {
parse_esds(esds)?.1
}
fn parse_esds(esds: &[u8]) -> Option<(u8, Option<Vec<u8>>)> {
fn descriptor(d: &[u8], pos: usize) -> Option<(u8, usize, usize)> {
let tag = *d.get(pos)?;
let mut len = 0usize;
let mut i = pos + 1;
for _ in 0..4 {
let b = *d.get(i)?;
i += 1;
len = (len << 7) | (b & 0x7F) as usize;
if b & 0x80 == 0 {
break;
}
}
d.get(i..i + len)?;
Some((tag, len, i))
}
let (tag, _, mut pos) = descriptor(esds, 4)?; if tag != 0x03 {
return None;
}
let flags = *esds.get(pos + 2)?;
pos += 3; if flags & 0x80 != 0 {
pos += 2; }
if flags & 0x40 != 0 {
pos += 1 + *esds.get(pos)? as usize; }
if flags & 0x20 != 0 {
pos += 2; }
let (tag, _, pos) = descriptor(esds, pos)?;
if tag != 0x04 {
return None;
}
let object_type = *esds.get(pos)?;
let specific = descriptor(esds, pos + 13) .filter(|(tag, _, _)| *tag == 0x05)
.map(|(_, len, start)| esds[start..start + len].to_vec());
Some((object_type, specific))
}
fn audio_specific_config(profile: u8, freq_index: u8, chan_conf: u8) -> Vec<u8> {
let v = ((profile as u16 & 0x1F) << 11) | ((freq_index as u16 & 0x0F) << 7) | ((chan_conf as u16 & 0x0F) << 3);
v.to_be_bytes().to_vec()
}
fn ticks(t: i64, timescale: u64) -> Duration {
let t = t.max(0) as u128;
Duration::from_nanos((t * 1_000_000_000 / timescale as u128) as u64)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::FileSource;
use std::time::Duration;
#[test]
fn min_composition_is_the_earliest_presented_sample() {
assert_eq!(min_composition(&[(10, 512)], &[]), 0);
assert_eq!(min_composition(&[(4, 512)], &[(1, 1024), (1, 2048), (1, 512), (1, 1024)]), 1024);
assert_eq!(min_composition(&[], &[]), 0, "no samples");
}
#[test]
fn leading_skip_ignores_empty_edits() {
assert_eq!(leading_skip([1024]), Some(1024));
assert_eq!(leading_skip([u32::MAX as u64, 2048]), Some(2048), "empty edit (v0) then the media");
assert_eq!(leading_skip([u64::MAX, 7106]), Some(7106), "empty edit (v1)");
assert_eq!(leading_skip([0]), None);
assert_eq!(leading_skip([u32::MAX as u64]), None);
assert_eq!(leading_skip(Vec::<u64>::new()), None);
}
#[test]
fn aac_tracks_report_their_edit_list_skip_as_codec_delay() {
let d = Mp4Demuxer::open(Box::new(FileSource::open("tests/fixtures/h264_aac.mp4").unwrap())).unwrap();
let audio = d.streams().iter().find(|s| s.kind == StreamKind::Audio).unwrap();
assert_eq!(audio.codec_delay, ticks(1024, 44_100), "same rounding as every MP4 timestamp");
let video = d.streams().iter().find(|s| s.kind == StreamKind::Video).unwrap();
assert_eq!(video.codec_delay, Duration::ZERO, "video is unchanged");
}
#[test]
fn seek_to_the_start_keeps_audio_stored_before_the_keyframe() {
let mut d = Mp4Demuxer::open(Box::new(FileSource::open("tests/fixtures/audio_first.mp4").unwrap())).unwrap();
let audio = d.streams().iter().find(|s| s.kind == StreamKind::Audio).unwrap().id;
let first_audio = |d: &mut Mp4Demuxer| loop {
let p = d.next_packet().unwrap().unwrap();
if p.stream == audio {
break p.pts;
}
};
assert_eq!(first_audio(&mut d), Duration::ZERO);
for _ in 0..20 {
d.next_packet().unwrap();
}
let landed = d.seek(Duration::ZERO).unwrap();
assert_eq!(landed, Duration::ZERO, "lands on the keyframe");
assert_eq!(first_audio(&mut d), Duration::ZERO, "the first audio packet (the padding) survives the seek");
}
#[test]
fn a_huge_box_size_in_moov_is_not_followed() {
let moov = [&[0, 0, 0, 1][..], b"trak", &[0xFF; 8]].concat();
assert_eq!(udta_offset(&moov), None);
let mut ok = [&[0, 0, 0, 16][..], b"trak", &[0; 8]].concat();
ok.extend([0, 0, 0, 8]);
ok.extend(b"udta");
assert_eq!(udta_offset(&ok), Some(16));
}
fn open() -> Mp4Demuxer {
Mp4Demuxer::open(Box::new(FileSource::open("tests/fixtures/av1.mp4").unwrap())).unwrap()
}
#[test]
fn reads_stream_info_and_packets() {
let mut d = open();
let s = &d.streams()[0];
assert_eq!((s.kind, s.codec.clone(), s.width, s.height), (StreamKind::Video, Codec::Av1, 320, 240));
let mut n = 0;
let mut first = None;
while let Some(p) = d.next_packet().unwrap() {
first.get_or_insert(p.keyframe);
n += 1;
}
assert_eq!(n, 60);
assert_eq!(first, Some(true));
}
#[test]
fn quicktime_prores_track_is_video() {
for name in ["prores_hq.mov", "prores_4444.mov"] {
let src = Box::new(FileSource::open(format!("tests/fixtures/{name}")).unwrap());
let mut d = Mp4Demuxer::open(src).unwrap();
let s = &d.streams()[0];
assert_eq!((s.kind, &s.codec, s.width, s.height), (StreamKind::Video, &Codec::ProRes, 192, 128), "{name}");
let p = d.next_packet().unwrap().unwrap();
assert_eq!(&p.data[4..8], b"icpf", "{name}: packets are whole ProRes frames");
}
}
#[test]
fn mp3_in_mp4_is_not_aac() {
let d = Mp4Demuxer::open(Box::new(FileSource::open("tests/fixtures/mp3.mp4").unwrap())).unwrap();
let s = &d.streams()[0];
assert_eq!((s.kind, &s.codec, s.sample_rate, s.channels), (StreamKind::Audio, &Codec::Mp3, 48_000, 2));
}
#[test]
fn alac_track_carries_its_magic_cookie() {
let d = Mp4Demuxer::open(Box::new(FileSource::open("tests/fixtures/alac.m4a").unwrap())).unwrap();
let s = &d.streams()[0];
assert_eq!((s.kind, &s.codec, s.sample_rate, s.channels), (StreamKind::Audio, &Codec::Alac, 48_000, 2));
let cookie = s.extradata.as_deref().expect("magic cookie");
assert_eq!(cookie.len(), 24);
assert_eq!(u32::from_be_bytes(cookie[..4].try_into().unwrap()), 4096);
}
#[test]
fn reads_aac_audio_track() {
let d = Mp4Demuxer::open(Box::new(FileSource::open("tests/fixtures/aac_only.m4a").unwrap())).unwrap();
let a = d.streams().iter().find(|s| s.kind == StreamKind::Audio).unwrap();
assert_eq!(a.codec, Codec::Aac);
assert_eq!((a.sample_rate, a.channels), (44_100, 1));
assert_eq!(a.extradata.as_deref(), Some(&[0x12, 0x08, 0x56, 0xE5, 0x00][..]));
}
#[test]
fn seek_lands_on_keyframe_at_or_before_target() {
let mut d = open();
let landed = d.seek(Duration::from_millis(1500)).unwrap();
assert_eq!(landed, Duration::from_secs(1));
let p = d.next_packet().unwrap().unwrap();
assert!(p.keyframe);
assert_eq!(p.pts, Duration::from_secs(1));
assert_eq!(d.seek(Duration::from_millis(400)).unwrap(), Duration::ZERO);
}
#[test]
fn packets_come_out_in_decode_order_even_if_stored_apart() {
let s = |stream, offset, ms| SampleRef {
stream,
offset,
size: 1,
pts: Duration::from_millis(ms),
dts: Duration::from_millis(ms),
keyframe: true,
};
let ordered = interleave_by_time(vec![s(1, 0, 0), s(1, 10, 40), s(1, 20, 80), s(2, 100, 0), s(2, 110, 20), s(2, 120, 60)]);
let order: Vec<(u32, u128)> = ordered.iter().map(|x| (x.stream, x.dts.as_millis())).collect();
assert_eq!(order, [(1, 0), (2, 0), (2, 20), (1, 40), (2, 60), (1, 80)]);
}
#[test]
fn esds_parser_returns_the_raw_audio_specific_config() {
let real = [
0, 0, 0, 0, 0x03, 0x80, 0x80, 0x80, 0x25, 0x00, 0x01, 0x00, 0x04, 0x80, 0x80, 0x80, 0x17, 0x40, 0x15, 0, 0, 0,
0, 0, 0xFD, 0xCD, 0, 0, 0xFD, 0xCD, 0x05, 0x80, 0x80, 0x80, 0x05, 0x12, 0x08, 0x56, 0xE5, 0x00, 0x06, 0x80,
0x80, 0x80, 0x01, 0x02,
];
assert_eq!(parse_esds_asc(&real), Some(vec![0x12, 0x08, 0x56, 0xE5, 0x00]));
let asc = [0x17, 0x80, 0x5D, 0xC0, 0x08];
let mut esds = vec![0, 0, 0, 0, 0x03, 0];
let es = {
let mut es = vec![0x00, 0x01, 0x40, 3, b'a', b'b', b'c']; es.extend([0x04, 13 + 2 + asc.len() as u8, 0x40, 0x15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
es.extend([0x05, asc.len() as u8]);
es.extend(asc);
es
};
esds[5] = es.len() as u8;
esds.extend(es);
assert_eq!(parse_esds_asc(&esds), Some(asc.to_vec()));
assert_eq!(parse_esds_asc(&[0, 0, 0, 0, 0x03, 0x05, 0x00]), None, "truncated");
}
}