use futures::FutureExt;
use hang::catalog::Container;
use mp4_atom::{Decode, Encode};
#[cfg(test)]
fn drain_group_sequences(consumer: &mut moq_net::TrackConsumer) -> Vec<u64> {
let mut sequences = Vec::new();
while let Some(group) = consumer.recv_group().now_or_never().and_then(|r| r.ok().flatten()) {
sequences.push(group.sequence);
}
sequences
}
fn run_fmp4(data: &[u8]) -> crate::catalog::hang::Catalog {
let mut broadcast = moq_net::Broadcast::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap();
let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.clone());
let mut buf = bytes::BytesMut::from(data);
let _ = fmp4.decode(&mut buf);
catalog.snapshot()
}
fn decode_init(init: &[u8]) -> (mp4_atom::Ftyp, mp4_atom::Moov) {
let mut cursor = std::io::Cursor::new(init);
let ftyp = mp4_atom::Ftyp::decode(&mut cursor).expect("invalid ftyp");
let moov = mp4_atom::Moov::decode(&mut cursor).expect("invalid moov");
(ftyp, moov)
}
#[test]
fn test_bbb_catalog() {
let data = include_bytes!("test_data/bbb.mp4");
let catalog = run_fmp4(data);
assert_eq!(catalog.video.renditions.len(), 1);
assert_eq!(catalog.audio.renditions.len(), 1);
let video = catalog.video.renditions.values().next().unwrap();
assert_eq!(video.codec.to_string(), "avc1.64001f");
assert_eq!(video.coded_width, Some(1280));
assert_eq!(video.coded_height, Some(720));
assert!(matches!(video.container, Container::Cmaf { .. }));
let audio = catalog.audio.renditions.values().next().unwrap();
assert_eq!(audio.codec.to_string(), "mp4a.40.2");
assert_eq!(audio.sample_rate, 44100);
assert_eq!(audio.channel_count, 2);
assert!(matches!(audio.container, Container::Cmaf { .. }));
}
#[test]
fn test_bbb_init_roundtrip() {
let data = include_bytes!("test_data/bbb.mp4");
let catalog = run_fmp4(data);
let video = catalog.video.renditions.values().next().unwrap();
let Container::Cmaf { init, .. } = &video.container else {
panic!("expected Cmaf container");
};
let (ftyp, moov) = decode_init(init);
assert_eq!(ftyp.major_brand, mp4_atom::FourCC::new(b"isom"));
assert_eq!(moov.trak.len(), 1);
assert_eq!(moov.trak[0].tkhd.track_id, 1);
assert_eq!(moov.trak[0].mdia.mdhd.timescale, 24000);
let mvex = moov.mvex.as_ref().unwrap();
assert_eq!(mvex.trex.len(), 1);
assert_eq!(mvex.trex[0].track_id, 1);
let mut buf = Vec::new();
ftyp.encode(&mut buf).unwrap();
moov.encode(&mut buf).unwrap();
let (ftyp2, moov2) = decode_init(&buf);
assert_eq!(ftyp2.major_brand, mp4_atom::FourCC::new(b"isom"));
assert_eq!(moov2.trak.len(), 1);
let audio = catalog.audio.renditions.values().next().unwrap();
let Container::Cmaf { init, .. } = &audio.container else {
panic!("expected Cmaf container");
};
let (ftyp, moov) = decode_init(init);
assert_eq!(ftyp.major_brand, mp4_atom::FourCC::new(b"isom"));
assert_eq!(moov.trak.len(), 1);
assert_eq!(moov.trak[0].tkhd.track_id, 2);
assert_eq!(moov.trak[0].mdia.mdhd.timescale, 44100);
let mvex = moov.mvex.as_ref().unwrap();
assert_eq!(mvex.trex.len(), 1);
assert_eq!(mvex.trex[0].track_id, 2);
}
#[test]
fn test_av1_catalog() {
let data = include_bytes!("test_data/av1.mp4");
let catalog = run_fmp4(data);
assert_eq!(catalog.video.renditions.len(), 1);
assert_eq!(catalog.audio.renditions.len(), 0);
let video = catalog.video.renditions.values().next().unwrap();
assert!(video.codec.to_string().starts_with("av01."), "codec: {}", video.codec);
assert!(matches!(video.container, Container::Cmaf { .. }));
let Container::Cmaf { init, .. } = &video.container else {
panic!("expected Cmaf container");
};
let (ftyp, moov) = decode_init(init);
assert_eq!(ftyp.major_brand, mp4_atom::FourCC::new(b"isom"));
assert_eq!(moov.trak.len(), 1);
let mvex = moov.mvex.as_ref().unwrap();
assert_eq!(mvex.trex.len(), 1);
assert_eq!(mvex.trex[0].track_id, moov.trak[0].tkhd.track_id);
}
#[test]
fn test_vp9_catalog() {
let data = include_bytes!("test_data/vp9.mp4");
let catalog = run_fmp4(data);
assert_eq!(catalog.video.renditions.len(), 1);
assert_eq!(catalog.audio.renditions.len(), 0);
let video = catalog.video.renditions.values().next().unwrap();
assert!(video.codec.to_string().starts_with("vp09."), "codec: {}", video.codec);
assert!(matches!(video.container, Container::Cmaf { .. }));
let Container::Cmaf { init, .. } = &video.container else {
panic!("expected Cmaf container");
};
let (ftyp, moov) = decode_init(init);
assert_eq!(ftyp.major_brand, mp4_atom::FourCC::new(b"isom"));
assert_eq!(moov.trak.len(), 1);
let mvex = moov.mvex.as_ref().unwrap();
assert_eq!(mvex.trex.len(), 1);
assert_eq!(mvex.trex[0].track_id, moov.trak[0].tkhd.track_id);
}
#[tokio::test]
async fn test_seek_sets_initial_sequence() {
use mp4_atom::{Any, DecodeMaybe};
let mut broadcast = moq_net::Broadcast::new().produce();
let broadcast_consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap();
let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.clone());
let data = include_bytes!("test_data/bbb.mp4");
let mut init_buf = bytes::BytesMut::new();
let mut frag_buf = bytes::BytesMut::new();
let mut cursor = std::io::Cursor::new(&data[..]);
let mut init_done = false;
let mut position = 0;
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap_or(None) {
let end = cursor.position() as usize;
let bytes = &data[position..end];
match atom {
Any::Ftyp(_) | Any::Styp(_) | Any::Moov(_) => init_buf.extend_from_slice(bytes),
_ => {
init_done = true;
frag_buf.extend_from_slice(bytes);
}
}
position = end;
if init_done && frag_buf.len() > 1024 {
break;
}
}
fmp4.decode(&mut init_buf).unwrap();
assert!(fmp4.is_initialized());
let snap = catalog.snapshot();
let video_name = snap.video.renditions.keys().next().expect("video track").clone();
let mut video_track = broadcast_consumer
.subscribe_track(&moq_net::Track::new(&video_name))
.expect("video track should exist");
fmp4.seek(100).unwrap();
let _ = fmp4.decode(&mut frag_buf);
fmp4.finish().unwrap();
let sequences = drain_group_sequences(&mut video_track);
assert!(!sequences.is_empty(), "expected at least one group");
assert_eq!(sequences[0], 100, "first group should land at the seeked sequence");
for win in sequences.windows(2) {
assert_eq!(win[1], win[0] + 1, "subsequent groups should auto-increment");
}
}
#[tokio::test]
async fn test_msf_catalog_roundtrip() {
let mut broadcast = moq_net::Broadcast::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap();
let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog);
let data = include_bytes!("test_data/bbb.mp4");
let mut buf = bytes::BytesMut::from(&data[..]);
let _ = fmp4.decode(&mut buf);
let track = consumer
.subscribe_track(&moq_net::Track::new(moq_msf::DEFAULT_NAME))
.expect("MSF catalog track should exist");
let mut msf = crate::catalog::msf::Consumer::new(track);
let catalog = msf
.next()
.await
.expect("MSF catalog should decode")
.expect("MSF catalog should be present");
assert_eq!(catalog.video.renditions.len(), 1);
assert_eq!(catalog.audio.renditions.len(), 1);
let video = catalog.video.renditions.values().next().unwrap();
assert_eq!(video.codec.to_string(), "avc1.64001f");
assert_eq!(video.coded_width, Some(1280));
assert_eq!(video.coded_height, Some(720));
assert!(matches!(video.container, Container::Cmaf { .. }));
let audio = catalog.audio.renditions.values().next().unwrap();
assert_eq!(audio.codec.to_string(), "mp4a.40.2");
assert_eq!(audio.sample_rate, 44100);
assert_eq!(audio.channel_count, 2);
assert!(matches!(audio.container, Container::Cmaf { .. }));
}