diff --git a/examples/mp4copy.rs b/examples/mp4copy.rs index 98d1ba80..64634864 100644 --- a/examples/mp4copy.rs +++ b/examples/mp4copy.rs @@ -5,8 +5,8 @@ use std::io::{self, BufReader, BufWriter}; use std::path::Path; use mp4::{ - AacConfig, AvcConfig, HevcConfig, MediaConfig, MediaType, Mp4Config, Result, TrackConfig, - TtxtConfig, Vp9Config, + AacConfig, Av1Config, AvcConfig, HevcConfig, MediaConfig, MediaType, Mp4Config, Result, + TrackConfig, TtxtConfig, Vp9Config, }; fn main() { @@ -58,6 +58,10 @@ fn copy>(src_filename: &P, dst_filename: &P) -> Result<()> { width: track.width(), height: track.height(), }), + MediaType::AV1 => MediaConfig::Av1Config(Av1Config { + width: track.width(), + height: track.height(), + }), MediaType::AAC => MediaConfig::AacConfig(AacConfig { bitrate: track.bitrate(), profile: track.audio_profile()?, diff --git a/src/mp4box/av01.rs b/src/mp4box/av01.rs new file mode 100644 index 00000000..814b94a7 --- /dev/null +++ b/src/mp4box/av01.rs @@ -0,0 +1,426 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::types::Av1Config; + +/// AV1 Sample Entry box (`av01`). +/// +/// Defined in the AV1 Codec ISO Media File Format Binding: +/// +/// +/// The sample entry uses the standard VisualSampleEntry base layout +/// (same 78-byte prefix as `avc1` / `hev1`) with an `av1C` child box. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Av01Box { + pub data_reference_index: u16, + pub width: u16, + pub height: u16, + + #[serde(with = "value_u32")] + pub horizresolution: FixedPointU16, + + #[serde(with = "value_u32")] + pub vertresolution: FixedPointU16, + pub frame_count: u16, + pub depth: u16, + pub av1c: Av1CBox, +} + +impl Default for Av01Box { + fn default() -> Self { + Av01Box { + data_reference_index: 0, + width: 0, + height: 0, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 0x0018, + av1c: Av1CBox::default(), + } + } +} + +impl Av01Box { + pub fn new(config: &Av1Config) -> Self { + Av01Box { + data_reference_index: 1, + width: config.width, + height: config.height, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 0x0018, + av1c: Av1CBox::default(), + } + } + + pub fn get_type(&self) -> BoxType { + BoxType::Av01Box + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + 8 + 70 + self.av1c.box_size() + } +} + +impl Mp4Box for Av01Box { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "data_reference_index={} width={} height={} frame_count={}", + self.data_reference_index, self.width, self.height, self.frame_count + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for Av01Box { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + reader.read_u32::()?; // reserved + reader.read_u16::()?; // reserved + let data_reference_index = reader.read_u16::()?; + + reader.read_u32::()?; // pre-defined, reserved + reader.read_u64::()?; // pre-defined + reader.read_u32::()?; // pre-defined + let width = reader.read_u16::()?; + let height = reader.read_u16::()?; + let horizresolution = FixedPointU16::new_raw(reader.read_u32::()?); + let vertresolution = FixedPointU16::new_raw(reader.read_u32::()?); + reader.read_u32::()?; // reserved + let frame_count = reader.read_u16::()?; + skip_bytes(reader, 32)?; // compressorname + let depth = reader.read_u16::()?; + reader.read_i16::()?; // pre-defined + + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "av01 box contains a box with a larger size than it", + )); + } + if name == BoxType::Av1CBox { + let av1c = Av1CBox::read_box(reader, s)?; + + skip_bytes_to(reader, start + size)?; + + Ok(Av01Box { + data_reference_index, + width, + height, + horizresolution, + vertresolution, + frame_count, + depth, + av1c, + }) + } else { + Err(Error::InvalidData("av1C not found")) + } + } +} + +impl WriteBox<&mut W> for Av01Box { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(0)?; // reserved + writer.write_u16::(0)?; // reserved + writer.write_u16::(self.data_reference_index)?; + + writer.write_u32::(0)?; // pre-defined, reserved + writer.write_u64::(0)?; // pre-defined + writer.write_u32::(0)?; // pre-defined + writer.write_u16::(self.width)?; + writer.write_u16::(self.height)?; + writer.write_u32::(self.horizresolution.raw_value())?; + writer.write_u32::(self.vertresolution.raw_value())?; + writer.write_u32::(0)?; // reserved + writer.write_u16::(self.frame_count)?; + // skip compressorname + write_zeros(writer, 32)?; + writer.write_u16::(self.depth)?; + writer.write_i16::(-1)?; // pre-defined + + self.av1c.write_box(writer)?; + + Ok(size) + } +} + +/// AV1 Codec Configuration Box (`av1C`). +/// +/// Contains the AV1CodecConfigurationRecord: 4 bytes of fixed bitfields +/// followed by zero or more configOBUs (typically a Sequence Header OBU). +/// +/// Layout (from the AV1-ISOBMFF spec §2.3.3): +/// ```text +/// unsigned int (1) marker = 1; +/// unsigned int (7) version = 1; +/// unsigned int (3) seq_profile; +/// unsigned int (5) seq_level_idx_0; +/// unsigned int (1) seq_tier_0; +/// unsigned int (1) high_bitdepth; +/// unsigned int (1) twelve_bit; +/// unsigned int (1) monochrome; +/// unsigned int (1) chroma_subsampling_x; +/// unsigned int (1) chroma_subsampling_y; +/// unsigned int (2) chroma_sample_position; +/// unsigned int (3) reserved = 0; +/// unsigned int (1) initial_presentation_delay_present; +/// unsigned int (4) initial_presentation_delay_minus_one / reserved; +/// unsigned int (8)[] configOBUs; +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct Av1CBox { + pub seq_profile: u8, + pub seq_level_idx_0: u8, + pub seq_tier_0: bool, + pub high_bitdepth: bool, + pub twelve_bit: bool, + pub monochrome: bool, + pub chroma_subsampling_x: bool, + pub chroma_subsampling_y: bool, + pub chroma_sample_position: u8, + pub initial_presentation_delay_present: bool, + pub initial_presentation_delay_minus_one: u8, + /// Raw OBU bytes (typically a Sequence Header OBU). + #[serde(skip)] + pub config_obus: Vec, +} + +impl Mp4Box for Av1CBox { + fn box_type(&self) -> BoxType { + BoxType::Av1CBox + } + + fn box_size(&self) -> u64 { + HEADER_SIZE + 4 + self.config_obus.len() as u64 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(format!( + "profile={} level={} tier={}", + self.seq_profile, self.seq_level_idx_0, self.seq_tier_0 as u8 + )) + } +} + +impl ReadBox<&mut R> for Av1CBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let byte0 = reader.read_u8()?; + let _marker = (byte0 >> 7) & 1; // must be 1 + let _version = byte0 & 0x7F; // must be 1 + + let byte1 = reader.read_u8()?; + let seq_profile = (byte1 >> 5) & 0x07; + let seq_level_idx_0 = byte1 & 0x1F; + + let byte2 = reader.read_u8()?; + let seq_tier_0 = (byte2 >> 7) & 1 == 1; + let high_bitdepth = (byte2 >> 6) & 1 == 1; + let twelve_bit = (byte2 >> 5) & 1 == 1; + let monochrome = (byte2 >> 4) & 1 == 1; + let chroma_subsampling_x = (byte2 >> 3) & 1 == 1; + let chroma_subsampling_y = (byte2 >> 2) & 1 == 1; + let chroma_sample_position = byte2 & 0x03; + + let byte3 = reader.read_u8()?; + let initial_presentation_delay_present = (byte3 >> 4) & 1 == 1; + let initial_presentation_delay_minus_one = if initial_presentation_delay_present { + byte3 & 0x0F + } else { + 0 + }; + + // Remaining bytes are configOBUs + let config_obus_size = size + .checked_sub(HEADER_SIZE + 4) + .ok_or(Error::InvalidData("av1C size too small"))?; + let mut config_obus = vec![0u8; config_obus_size as usize]; + reader.read_exact(&mut config_obus)?; + + skip_bytes_to(reader, start + size)?; + + Ok(Av1CBox { + seq_profile, + seq_level_idx_0, + seq_tier_0, + high_bitdepth, + twelve_bit, + monochrome, + chroma_subsampling_x, + chroma_subsampling_y, + chroma_sample_position, + initial_presentation_delay_present, + initial_presentation_delay_minus_one, + config_obus, + }) + } +} + +impl WriteBox<&mut W> for Av1CBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + // byte 0: marker(1) = 1 | version(7) = 1 + writer.write_u8(0x81)?; + + // byte 1: seq_profile(3) | seq_level_idx_0(5) + writer.write_u8((self.seq_profile << 5) | (self.seq_level_idx_0 & 0x1F))?; + + // byte 2: seq_tier_0(1) | high_bitdepth(1) | twelve_bit(1) | monochrome(1) + // | chroma_subsampling_x(1) | chroma_subsampling_y(1) | chroma_sample_position(2) + let byte2 = ((self.seq_tier_0 as u8) << 7) + | ((self.high_bitdepth as u8) << 6) + | ((self.twelve_bit as u8) << 5) + | ((self.monochrome as u8) << 4) + | ((self.chroma_subsampling_x as u8) << 3) + | ((self.chroma_subsampling_y as u8) << 2) + | (self.chroma_sample_position & 0x03); + writer.write_u8(byte2)?; + + // byte 3: reserved(3) = 0 | initial_presentation_delay_present(1) + // | initial_presentation_delay_minus_one(4) / reserved(4) = 0 + let byte3 = if self.initial_presentation_delay_present { + (1 << 4) | (self.initial_presentation_delay_minus_one & 0x0F) + } else { + 0 + }; + writer.write_u8(byte3)?; + + // configOBUs + writer.write_all(&self.config_obus)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_av1c_round_trip() { + let src_box = Av1CBox { + seq_profile: 0, + seq_level_idx_0: 4, + seq_tier_0: false, + high_bitdepth: false, + twelve_bit: false, + monochrome: false, + chroma_subsampling_x: true, + chroma_subsampling_y: true, + chroma_sample_position: 0, + initial_presentation_delay_present: false, + initial_presentation_delay_minus_one: 0, + config_obus: vec![ + 0x0a, 0x0b, 0x00, 0x00, 0x00, 0x24, 0xcf, 0x7f, 0x0d, 0xbf, 0xff, 0x30, 0x08, + ], + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Av1CBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Av1CBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_av01_round_trip() { + let src_box = Av01Box { + data_reference_index: 1, + width: 960, + height: 540, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 24, + av1c: Av1CBox { + seq_profile: 0, + seq_level_idx_0: 4, + seq_tier_0: false, + high_bitdepth: false, + twelve_bit: false, + monochrome: false, + chroma_subsampling_x: true, + chroma_subsampling_y: true, + chroma_sample_position: 0, + initial_presentation_delay_present: false, + initial_presentation_delay_minus_one: 0, + config_obus: vec![0x0a, 0x0b], + }, + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Av01Box); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Av01Box::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_av1c_with_presentation_delay() { + let src_box = Av1CBox { + seq_profile: 1, + seq_level_idx_0: 8, + seq_tier_0: true, + high_bitdepth: true, + twelve_bit: false, + monochrome: false, + chroma_subsampling_x: true, + chroma_subsampling_y: false, + chroma_sample_position: 1, + initial_presentation_delay_present: true, + initial_presentation_delay_minus_one: 3, + config_obus: vec![], + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + let dst_box = Av1CBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/src/mp4box/mod.rs b/src/mp4box/mod.rs index 4bbdd410..1f051974 100644 --- a/src/mp4box/mod.rs +++ b/src/mp4box/mod.rs @@ -27,6 +27,7 @@ //! stsd //! avc1 //! hev1 +//! av01 //! mp4a //! tx3g //! stts @@ -62,6 +63,7 @@ use std::io::{Read, Seek, SeekFrom, Write}; use crate::*; +pub(crate) mod av01; pub(crate) mod avc1; pub(crate) mod co64; pub(crate) mod ctts; @@ -106,6 +108,7 @@ pub(crate) mod vmhd; pub(crate) mod vp09; pub(crate) mod vpcc; +pub use av01::Av01Box; pub use avc1::Avc1Box; pub use co64::Co64Box; pub use ctts::CttsBox; @@ -222,6 +225,8 @@ boxtype! { DrefBox => 0x64726566, UrlBox => 0x75726C20, SmhdBox => 0x736d6864, + Av01Box => 0x61763031, + Av1CBox => 0x61763143, Avc1Box => 0x61766331, AvcCBox => 0x61766343, Hev1Box => 0x68657631, diff --git a/src/mp4box/stsd.rs b/src/mp4box/stsd.rs index af947c6c..1cb596bf 100644 --- a/src/mp4box/stsd.rs +++ b/src/mp4box/stsd.rs @@ -2,6 +2,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use serde::Serialize; use std::io::{Read, Seek, Write}; +use crate::mp4box::av01::Av01Box; use crate::mp4box::vp09::Vp09Box; use crate::mp4box::*; use crate::mp4box::{avc1::Avc1Box, hev1::Hev1Box, mp4a::Mp4aBox, tx3g::Tx3gBox}; @@ -20,6 +21,9 @@ pub struct StsdBox { #[serde(skip_serializing_if = "Option::is_none")] pub vp09: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub av01: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub mp4a: Option, @@ -40,6 +44,8 @@ impl StsdBox { size += hev1.box_size(); } else if let Some(ref vp09) = self.vp09 { size += vp09.box_size(); + } else if let Some(ref av01) = self.av01 { + size += av01.box_size(); } else if let Some(ref mp4a) = self.mp4a { size += mp4a.box_size(); } else if let Some(ref tx3g) = self.tx3g { @@ -79,6 +85,7 @@ impl ReadBox<&mut R> for StsdBox { let mut avc1 = None; let mut hev1 = None; let mut vp09 = None; + let mut av01 = None; let mut mp4a = None; let mut tx3g = None; @@ -101,6 +108,9 @@ impl ReadBox<&mut R> for StsdBox { BoxType::Vp09Box => { vp09 = Some(Vp09Box::read_box(reader, s)?); } + BoxType::Av01Box => { + av01 = Some(Av01Box::read_box(reader, s)?); + } BoxType::Mp4aBox => { mp4a = Some(Mp4aBox::read_box(reader, s)?); } @@ -118,6 +128,7 @@ impl ReadBox<&mut R> for StsdBox { avc1, hev1, vp09, + av01, mp4a, tx3g, }) @@ -139,6 +150,8 @@ impl WriteBox<&mut W> for StsdBox { hev1.write_box(writer)?; } else if let Some(ref vp09) = self.vp09 { vp09.write_box(writer)?; + } else if let Some(ref av01) = self.av01 { + av01.write_box(writer)?; } else if let Some(ref mp4a) = self.mp4a { mp4a.write_box(writer)?; } else if let Some(ref tx3g) = self.tx3g { diff --git a/src/track.rs b/src/track.rs index 7eada834..6e00e81d 100644 --- a/src/track.rs +++ b/src/track.rs @@ -30,6 +30,7 @@ impl From for TrackConfig { MediaConfig::AacConfig(aac_conf) => Self::from(aac_conf), MediaConfig::TtxtConfig(ttxt_conf) => Self::from(ttxt_conf), MediaConfig::Vp9Config(vp9_config) => Self::from(vp9_config), + MediaConfig::Av1Config(av1_config) => Self::from(av1_config), } } } @@ -89,6 +90,17 @@ impl From for TrackConfig { } } +impl From for TrackConfig { + fn from(av1_conf: Av1Config) -> Self { + Self { + track_type: TrackType::Video, + timescale: 1000, // XXX + language: String::from("und"), // XXX + media_conf: MediaConfig::Av1Config(av1_conf), + } + } +} + #[derive(Debug)] pub struct Mp4Track { pub trak: TrakBox, @@ -125,6 +137,8 @@ impl Mp4Track { Ok(MediaType::H265) } else if self.trak.mdia.minf.stbl.stsd.vp09.is_some() { Ok(MediaType::VP9) + } else if self.trak.mdia.minf.stbl.stsd.av01.is_some() { + Ok(MediaType::AV1) } else if self.trak.mdia.minf.stbl.stsd.mp4a.is_some() { Ok(MediaType::AAC) } else if self.trak.mdia.minf.stbl.stsd.tx3g.is_some() { @@ -675,6 +689,15 @@ impl Mp4TrackWriter { trak.mdia.minf.stbl.stsd.vp09 = Some(Vp09Box::new(config)); } + MediaConfig::Av1Config(ref config) => { + trak.tkhd.set_width(config.width); + trak.tkhd.set_height(config.height); + + let vmhd = VmhdBox::default(); + trak.mdia.minf.vmhd = Some(vmhd); + + trak.mdia.minf.stbl.stsd.av01 = Some(Av01Box::new(config)); + } MediaConfig::AacConfig(ref aac_config) => { let smhd = SmhdBox::default(); trak.mdia.minf.smhd = Some(smhd); diff --git a/src/types.rs b/src/types.rs index 540f7fb0..a3f6e473 100644 --- a/src/types.rs +++ b/src/types.rs @@ -220,6 +220,7 @@ impl From for FourCC { const MEDIA_TYPE_H264: &str = "h264"; const MEDIA_TYPE_H265: &str = "h265"; const MEDIA_TYPE_VP9: &str = "vp9"; +const MEDIA_TYPE_AV1: &str = "av1"; const MEDIA_TYPE_AAC: &str = "aac"; const MEDIA_TYPE_TTXT: &str = "ttxt"; @@ -228,6 +229,7 @@ pub enum MediaType { H264, H265, VP9, + AV1, AAC, TTXT, } @@ -246,6 +248,7 @@ impl TryFrom<&str> for MediaType { MEDIA_TYPE_H264 => Ok(MediaType::H264), MEDIA_TYPE_H265 => Ok(MediaType::H265), MEDIA_TYPE_VP9 => Ok(MediaType::VP9), + MEDIA_TYPE_AV1 => Ok(MediaType::AV1), MEDIA_TYPE_AAC => Ok(MediaType::AAC), MEDIA_TYPE_TTXT => Ok(MediaType::TTXT), _ => Err(Error::InvalidData("unsupported media type")), @@ -259,6 +262,7 @@ impl From for &str { MediaType::H264 => MEDIA_TYPE_H264, MediaType::H265 => MEDIA_TYPE_H265, MediaType::VP9 => MEDIA_TYPE_VP9, + MediaType::AV1 => MEDIA_TYPE_AV1, MediaType::AAC => MEDIA_TYPE_AAC, MediaType::TTXT => MEDIA_TYPE_TTXT, } @@ -271,6 +275,7 @@ impl From<&MediaType> for &str { MediaType::H264 => MEDIA_TYPE_H264, MediaType::H265 => MEDIA_TYPE_H265, MediaType::VP9 => MEDIA_TYPE_VP9, + MediaType::AV1 => MEDIA_TYPE_AV1, MediaType::AAC => MEDIA_TYPE_AAC, MediaType::TTXT => MEDIA_TYPE_TTXT, } @@ -584,6 +589,12 @@ pub struct Vp9Config { pub height: u16, } +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Av1Config { + pub width: u16, + pub height: u16, +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct AacConfig { pub bitrate: u32, @@ -611,6 +622,7 @@ pub enum MediaConfig { AvcConfig(AvcConfig), HevcConfig(HevcConfig), Vp9Config(Vp9Config), + Av1Config(Av1Config), AacConfig(AacConfig), TtxtConfig(TtxtConfig), } diff --git a/tests/lib.rs b/tests/lib.rs index 7c81f95f..2c0bd291 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,3 +1,4 @@ +use mp4::Av01Box; use mp4::{ AudioObjectType, AvcProfile, ChannelConfig, MediaType, Metadata, Mp4Reader, SampleFreqIndex, TrackType, @@ -209,3 +210,49 @@ fn test_read_fragments() { let eos = mp4_fragment.read_sample(1, 2); assert!(eos.is_err()); } + +#[test] +fn test_read_av1_mp4() { + let mut mp4 = get_reader("tests/samples/spbtv_sample_bipbop_av1_960x540_25fps.mp4"); + + assert_eq!(245747, mp4.size()); + assert_eq!(mp4.tracks().len(), 2); + + // track #1 — AAC audio + let track1 = mp4.tracks().get(&1).unwrap(); + assert_eq!(track1.track_type().unwrap(), TrackType::Audio); + assert_eq!(track1.media_type().unwrap(), MediaType::AAC); + + // track #2 — AV1 video + let track2 = mp4.tracks().get(&2).unwrap(); + assert_eq!(track2.track_id(), 2); + assert_eq!(track2.track_type().unwrap(), TrackType::Video); + assert_eq!(track2.media_type().unwrap(), MediaType::AV1); + assert_eq!(track2.width(), 960); + assert_eq!(track2.height(), 540); + + // Verify av01 sample entry was parsed + let av01: &Av01Box = track2.trak.mdia.minf.stbl.stsd.av01.as_ref().unwrap(); + assert_eq!(av01.width, 960); + assert_eq!(av01.height, 540); + + // Verify av1C config was parsed correctly + assert_eq!(av01.av1c.seq_profile, 0); + assert_eq!(av01.av1c.seq_level_idx_0, 4); + assert!(!av01.av1c.seq_tier_0); + assert!(!av01.av1c.high_bitdepth); + assert!(!av01.av1c.twelve_bit); + assert!(!av01.av1c.monochrome); + assert!(av01.av1c.chroma_subsampling_x); + assert!(av01.av1c.chroma_subsampling_y); + assert_eq!(av01.av1c.chroma_sample_position, 0); + assert!(!av01.av1c.initial_presentation_delay_present); + assert_eq!(av01.av1c.config_obus.len(), 13); + + // Verify we can read video samples + let sample_count = mp4.sample_count(2).unwrap(); + assert!(sample_count > 0); + let sample_1 = mp4.read_sample(2, 1).unwrap().unwrap(); + assert!(!sample_1.bytes.is_empty()); + assert!(sample_1.is_sync); +} diff --git a/tests/samples/spbtv_sample_bipbop_av1_960x540_25fps.mp4 b/tests/samples/spbtv_sample_bipbop_av1_960x540_25fps.mp4 new file mode 100644 index 00000000..bc2e9632 Binary files /dev/null and b/tests/samples/spbtv_sample_bipbop_av1_960x540_25fps.mp4 differ