From df6e102953af13f2b3fe00529e650092433c90f8 Mon Sep 17 00:00:00 2001 From: fcancela Date: Tue, 18 Nov 2025 17:21:48 -0300 Subject: [PATCH 1/5] Initial avc3 support --- src/any.rs | 2 +- .../trak/mdia/minf/stbl/stsd/h264/avc3.rs | 158 ++++++++++++++++++ src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs | 2 + src/moov/trak/mdia/minf/stbl/stsd/mod.rs | 5 + 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs diff --git a/src/any.rs b/src/any.rs index e675001..5c90aea 100644 --- a/src/any.rs +++ b/src/any.rs @@ -268,7 +268,7 @@ any! { Minf, Stbl, Stsd, - Avc1, + Avc1, Avc3, Avcc, Btrt, Ccst, diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs new file mode 100644 index 0000000..9311f61 --- /dev/null +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs @@ -0,0 +1,158 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Avc3 { + pub visual: Visual, + pub avcc: Avcc, + pub btrt: Option, + pub colr: Option, + pub pasp: Option, + pub taic: Option, +} + +impl Atom for Avc3 { + const KIND: FourCC = FourCC::new(b"avc3"); + + fn decode_body(buf: &mut B) -> Result { + let visual = Visual::decode(buf)?; + + let mut avcc = None; + let mut btrt = None; + let mut colr = None; + let mut pasp = None; + let mut taic = None; + while let Some(atom) = Any::decode_maybe(buf)? { + match atom { + Any::Avcc(atom) => avcc = atom.into(), + Any::Btrt(atom) => btrt = atom.into(), + Any::Colr(atom) => colr = atom.into(), + Any::Pasp(atom) => pasp = atom.into(), + Any::Taic(atom) => taic = atom.into(), + _ => tracing::warn!("unknown atom: {:?}", atom), + } + } + + Ok(Avc3 { + visual, + avcc: avcc.ok_or(Error::MissingBox(Avcc::KIND))?, + btrt, + colr, + pasp, + taic, + }) + } + + fn encode_body(&self, buf: &mut B) -> Result<()> { + self.visual.encode(buf)?; + self.avcc.encode(buf)?; + if self.btrt.is_some() { + self.btrt.encode(buf)?; + } + if self.colr.is_some() { + self.colr.encode(buf)?; + } + if self.pasp.is_some() { + self.pasp.encode(buf)?; + } + if self.taic.is_some() { + self.taic.encode(buf)? + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_avc3() { + let expected = Avc3 { + visual: Visual { + data_reference_index: 1, + width: 320, + height: 240, + horizresolution: 0x48.into(), + vertresolution: 0x48.into(), + frame_count: 1, + compressor: "ya boy".into(), + depth: 24, + }, + avcc: Avcc { + configuration_version: 1, + avc_profile_indication: 100, + profile_compatibility: 0, + avc_level_indication: 13, + length_size: 4, + sequence_parameter_sets: vec![vec![ + 0x67, 0x64, 0x00, 0x0D, 0xAC, 0xD9, 0x41, 0x41, 0xFA, 0x10, 0x00, 0x00, 0x03, + 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0x20, 0xF1, 0x42, 0x99, 0x60, + ]], + picture_parameter_sets: vec![vec![0x68, 0xEB, 0xE3, 0xCB, 0x22, 0xC0]], + ..Default::default() + }, + btrt: None, + colr: None, + pasp: None, + taic: None, + }; + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let mut buf = buf.as_ref(); + let decoded = Avc3::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_avc3_with_extras() { + let expected = Avc3 { + visual: Visual { + data_reference_index: 1, + width: 320, + height: 240, + horizresolution: 0x48.into(), + vertresolution: 0x48.into(), + frame_count: 1, + compressor: "they".into(), + depth: 24, + }, + avcc: Avcc { + configuration_version: 1, + avc_profile_indication: 100, + profile_compatibility: 0, + avc_level_indication: 13, + length_size: 4, + sequence_parameter_sets: vec![vec![ + 0x67, 0x64, 0x00, 0x0D, 0xAC, 0xD9, 0x41, 0x41, 0xFA, 0x10, 0x00, 0x00, 0x03, + 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0x20, 0xF1, 0x42, 0x99, 0x60, + ]], + picture_parameter_sets: vec![vec![0x68, 0xEB, 0xE3, 0xCB, 0x22, 0xC0]], + ..Default::default() + }, + btrt: Some(Btrt { + buffer_size_db: 14075, + max_bitrate: 374288, + avg_bitrate: 240976, + }), + colr: Some(Colr::default()), + pasp: Some(Pasp { + h_spacing: 4, + v_spacing: 3, + }), + taic: Some(Taic { + time_uncertainty: u64::MAX, + clock_resolution: 1000, + clock_drift_rate: i32::MAX, + clock_type: ClockType::CanSync, + }), + }; + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let mut buf = buf.as_ref(); + let decoded = Avc3::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } +} diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs index 001a378..c4ec719 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs @@ -1,5 +1,6 @@ mod avc1; mod avcc; +mod avc3; // Incomplete H264 decoder, saved for possible future use //mod golomb; @@ -8,3 +9,4 @@ mod avcc; pub use avc1::*; pub use avcc::*; +pub use avc3::*; \ No newline at end of file diff --git a/src/moov/trak/mdia/minf/stbl/stsd/mod.rs b/src/moov/trak/mdia/minf/stbl/stsd/mod.rs index afa9d78..02635d9 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/mod.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/mod.rs @@ -52,6 +52,9 @@ pub enum Codec { // H264 Avc1(Avc1), + // H264: SPS/PPS/VPS is inline + Avc3(Avc3), + // HEVC: SPS/PPS/VPS is inline Hev1(Hev1), @@ -97,6 +100,7 @@ impl Decode for Codec { let atom = Any::decode(buf)?; Ok(match atom { Any::Avc1(atom) => atom.into(), + Any::Avc3(atom) => atom.into(), Any::Hev1(atom) => atom.into(), Any::Hvc1(atom) => atom.into(), Any::Vp08(atom) => atom.into(), @@ -120,6 +124,7 @@ impl Encode for Codec { match self { Self::Unknown(kind) => kind.encode(buf), Self::Avc1(atom) => atom.encode(buf), + Self::Avc3(atom) => atom.encode(buf), Self::Hev1(atom) => atom.encode(buf), Self::Hvc1(atom) => atom.encode(buf), Self::Vp08(atom) => atom.encode(buf), From bca2902ce121586b8d8d43f0b22daa395f07e2c1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 18 Nov 2025 15:23:17 -0800 Subject: [PATCH 2/5] formatting --- src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs index c4ec719..605f1df 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs @@ -1,6 +1,6 @@ mod avc1; -mod avcc; mod avc3; +mod avcc; // Incomplete H264 decoder, saved for possible future use //mod golomb; @@ -8,5 +8,5 @@ mod avc3; //mod sps; pub use avc1::*; +pub use avc3::*; pub use avcc::*; -pub use avc3::*; \ No newline at end of file From 347a02ad5acf88640a1aecdcddf3e24b464c6c55 Mon Sep 17 00:00:00 2001 From: fcancela Date: Thu, 20 Nov 2025 11:00:17 -0300 Subject: [PATCH 3/5] feat(avc): refactor Avc1 and Avc3 to use a generic AvcSampleEntry type - Introduced a new AvcSampleEntry struct to encapsulate common fields for AVC sample entries. - Updated Avc1 and Avc3 to utilize the new AvcSampleEntry type, improving code reusability. - Added a from_u32 method to the FourCC struct for better FourCC initialization from u32 values. --- .../trak/mdia/minf/stbl/stsd/h264/avc1.rs | 12 +- .../trak/mdia/minf/stbl/stsd/h264/avc3.rs | 152 ++++-------------- src/types.rs | 4 + 3 files changed, 47 insertions(+), 121 deletions(-) diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs index ad0417e..d10e8f6 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs @@ -1,8 +1,10 @@ use crate::*; +const AVC1_CODE: u32 = u32::from_be_bytes(*b"avc1"); + #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Avc1 { +pub struct AvcSampleEntry { pub visual: Visual, pub avcc: Avcc, pub btrt: Option, @@ -11,8 +13,10 @@ pub struct Avc1 { pub taic: Option, } -impl Atom for Avc1 { - const KIND: FourCC = FourCC::new(b"avc1"); +pub type Avc1 = AvcSampleEntry<{ AVC1_CODE }>; + +impl Atom for AvcSampleEntry { + const KIND: FourCC = FourCC::from_u32(KIND_CODE); fn decode_body(buf: &mut B) -> Result { let visual = Visual::decode(buf)?; @@ -33,7 +37,7 @@ impl Atom for Avc1 { } } - Ok(Avc1 { + Ok(Self { visual, avcc: avcc.ok_or(Error::MissingBox(Avcc::KIND))?, btrt, diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs index 9311f61..6ca31f6 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs @@ -1,74 +1,16 @@ -use crate::*; +use super::avc1::AvcSampleEntry; -#[derive(Debug, Clone, PartialEq, Eq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Avc3 { - pub visual: Visual, - pub avcc: Avcc, - pub btrt: Option, - pub colr: Option, - pub pasp: Option, - pub taic: Option, -} - -impl Atom for Avc3 { - const KIND: FourCC = FourCC::new(b"avc3"); - - fn decode_body(buf: &mut B) -> Result { - let visual = Visual::decode(buf)?; - - let mut avcc = None; - let mut btrt = None; - let mut colr = None; - let mut pasp = None; - let mut taic = None; - while let Some(atom) = Any::decode_maybe(buf)? { - match atom { - Any::Avcc(atom) => avcc = atom.into(), - Any::Btrt(atom) => btrt = atom.into(), - Any::Colr(atom) => colr = atom.into(), - Any::Pasp(atom) => pasp = atom.into(), - Any::Taic(atom) => taic = atom.into(), - _ => tracing::warn!("unknown atom: {:?}", atom), - } - } - - Ok(Avc3 { - visual, - avcc: avcc.ok_or(Error::MissingBox(Avcc::KIND))?, - btrt, - colr, - pasp, - taic, - }) - } +const AVC3_CODE: u32 = u32::from_be_bytes(*b"avc3"); - fn encode_body(&self, buf: &mut B) -> Result<()> { - self.visual.encode(buf)?; - self.avcc.encode(buf)?; - if self.btrt.is_some() { - self.btrt.encode(buf)?; - } - if self.colr.is_some() { - self.colr.encode(buf)?; - } - if self.pasp.is_some() { - self.pasp.encode(buf)?; - } - if self.taic.is_some() { - self.taic.encode(buf)? - } - Ok(()) - } -} +pub type Avc3 = AvcSampleEntry<{ AVC3_CODE }>; #[cfg(test)] mod tests { use super::*; + use crate::*; - #[test] - fn test_avc3() { - let expected = Avc3 { + fn base(compressor: &str) -> Avc3 { + Avc3 { visual: Visual { data_reference_index: 1, width: 320, @@ -76,7 +18,7 @@ mod tests { horizresolution: 0x48.into(), vertresolution: 0x48.into(), frame_count: 1, - compressor: "ya boy".into(), + compressor: compressor.into(), depth: 24, }, avcc: Avcc { @@ -92,11 +34,11 @@ mod tests { picture_parameter_sets: vec![vec![0x68, 0xEB, 0xE3, 0xCB, 0x22, 0xC0]], ..Default::default() }, - btrt: None, - colr: None, - pasp: None, - taic: None, - }; + ..Default::default() + } + } + + fn roundtrip(expected: Avc3) { let mut buf = Vec::new(); expected.encode(&mut buf).unwrap(); @@ -106,53 +48,29 @@ mod tests { } #[test] - fn test_avc3_with_extras() { - let expected = Avc3 { - visual: Visual { - data_reference_index: 1, - width: 320, - height: 240, - horizresolution: 0x48.into(), - vertresolution: 0x48.into(), - frame_count: 1, - compressor: "they".into(), - depth: 24, - }, - avcc: Avcc { - configuration_version: 1, - avc_profile_indication: 100, - profile_compatibility: 0, - avc_level_indication: 13, - length_size: 4, - sequence_parameter_sets: vec![vec![ - 0x67, 0x64, 0x00, 0x0D, 0xAC, 0xD9, 0x41, 0x41, 0xFA, 0x10, 0x00, 0x00, 0x03, - 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0x20, 0xF1, 0x42, 0x99, 0x60, - ]], - picture_parameter_sets: vec![vec![0x68, 0xEB, 0xE3, 0xCB, 0x22, 0xC0]], - ..Default::default() - }, - btrt: Some(Btrt { - buffer_size_db: 14075, - max_bitrate: 374288, - avg_bitrate: 240976, - }), - colr: Some(Colr::default()), - pasp: Some(Pasp { - h_spacing: 4, - v_spacing: 3, - }), - taic: Some(Taic { - time_uncertainty: u64::MAX, - clock_resolution: 1000, - clock_drift_rate: i32::MAX, - clock_type: ClockType::CanSync, - }), - }; - let mut buf = Vec::new(); - expected.encode(&mut buf).unwrap(); + fn test_avc3() { + roundtrip(base("ya boy")); + } - let mut buf = buf.as_ref(); - let decoded = Avc3::decode(&mut buf).unwrap(); - assert_eq!(decoded, expected); + #[test] + fn test_avc3_with_extras() { + let mut avc3 = base("they"); + avc3.btrt = Some(Btrt { + buffer_size_db: 14075, + max_bitrate: 374288, + avg_bitrate: 240976, + }); + avc3.colr = Some(Colr::default()); + avc3.pasp = Some(Pasp { + h_spacing: 4, + v_spacing: 3, + }); + avc3.taic = Some(Taic { + time_uncertainty: u64::MAX, + clock_resolution: 1000, + clock_drift_rate: i32::MAX, + clock_type: ClockType::CanSync, + }); + roundtrip(avc3); } } diff --git a/src/types.rs b/src/types.rs index 098da9f..4ee3581 100644 --- a/src/types.rs +++ b/src/types.rs @@ -18,6 +18,10 @@ impl FourCC { pub const fn new(value: &[u8; 4]) -> Self { FourCC(*value) } + + pub const fn from_u32(value: u32) -> Self { + FourCC(value.to_be_bytes()) + } } impl From for FourCC { From ed6cedb4063adb6d244a8b9d49214792df07eb6f Mon Sep 17 00:00:00 2001 From: fcancela Date: Thu, 27 Nov 2025 15:05:30 -0300 Subject: [PATCH 4/5] feat(avc3): add BBC AVC3 sample data and decoding test - Introduced a constant for BBC AVC3 sample data extracted from a test stream. - Implemented a function to create an expected Avc3 structure for comparison. - Added a test to validate the decoding of the BBC AVC3 sample, ensuring correct structure and child atom handling. - Refactored AVC1 code to use a more explicit byte array initialization. --- .../trak/mdia/minf/stbl/stsd/h264/avc1.rs | 2 +- .../trak/mdia/minf/stbl/stsd/h264/avc3.rs | 68 ++++++++++++++++++ tests/data/bbc_avc3.bin | Bin 0 -> 136 bytes 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/data/bbc_avc3.bin diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs index d10e8f6..bbf1299 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs @@ -1,6 +1,6 @@ use crate::*; -const AVC1_CODE: u32 = u32::from_be_bytes(*b"avc1"); +const AVC1_CODE: u32 = u32::from_be_bytes([b'a', b'v', b'c', b'1']); #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs index 6ca31f6..9fc5dda 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs @@ -38,6 +38,48 @@ mod tests { } } + // Extracted from the initialization segment (`IS.mp4`) of the BBC Testcard + // HLS stream: https://vs-dash-ww-rd-live.akamaized.net/pl/testcard2020/192x108p25/media.m3u8 + const BBC_AVC3_SAMPLE: &[u8; 136] = + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/bbc_avc3.bin")); + + fn bbc_expected() -> Avc3 { + Avc3 { + visual: Visual { + data_reference_index: 1, + width: 192, + height: 108, + horizresolution: 0x48.into(), + vertresolution: 0x48.into(), + frame_count: 1, + compressor: "\x04h264".into(), + depth: 24, + }, + avcc: Avcc { + configuration_version: 1, + avc_profile_indication: 0x42, + profile_compatibility: 0xC0, + avc_level_indication: 0x15, + length_size: 4, + sequence_parameter_sets: Vec::new(), + picture_parameter_sets: Vec::new(), + ext: None, + }, + btrt: None, + colr: Some(Colr::Nclx { + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + full_range_flag: false, + }), + pasp: Some(Pasp { + h_spacing: 1, + v_spacing: 1, + }), + taic: None, + } + } + fn roundtrip(expected: Avc3) { let mut buf = Vec::new(); expected.encode(&mut buf).unwrap(); @@ -73,4 +115,30 @@ mod tests { }); roundtrip(avc3); } + + #[test] + fn test_avc3_decodes_real_bbc_stream() { + assert_eq!(BBC_AVC3_SAMPLE.len(), 136); + // Sanity-check the extracted box still contains the expected children. + let mut inspect = BBC_AVC3_SAMPLE.as_slice(); + let header = Header::decode(&mut inspect).unwrap(); + assert_eq!(header.kind, Avc3::KIND); + let mut body = inspect; + Visual::decode(&mut body).unwrap(); + assert_eq!(body.remaining(), 50); + let mut child_kinds = Vec::new(); + while let Some(atom) = Any::decode_maybe(&mut body).unwrap() { + child_kinds.push(atom.kind()); + } + assert_eq!( + child_kinds, + vec![Avcc::KIND, Pasp::KIND, Colr::KIND], + "unexpected children: {:?}", + child_kinds + ); + + let mut buf = BBC_AVC3_SAMPLE.as_slice(); + let decoded = Avc3::decode(&mut buf).unwrap(); + assert_eq!(decoded, bbc_expected()); + } } diff --git a/tests/data/bbc_avc3.bin b/tests/data/bbc_avc3.bin new file mode 100644 index 0000000000000000000000000000000000000000..fcec2c6f5ab6f5a31a1542fc4f87c3ea702a8087 GIT binary patch literal 136 zcmZQzVCYCJOEzYJ07fW{pbjwPFn9pDAPfSGEEz^-CJ;7uQsV!ApaehARA)w~1ET*Q dfRqUoBo-F{X^;gVES#L5Q Date: Fri, 28 Nov 2025 10:07:56 +1100 Subject: [PATCH 5/5] chore(avc3): formatting fix --- src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs index 9fc5dda..8431e22 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs @@ -40,8 +40,10 @@ mod tests { // Extracted from the initialization segment (`IS.mp4`) of the BBC Testcard // HLS stream: https://vs-dash-ww-rd-live.akamaized.net/pl/testcard2020/192x108p25/media.m3u8 - const BBC_AVC3_SAMPLE: &[u8; 136] = - include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/bbc_avc3.bin")); + const BBC_AVC3_SAMPLE: &[u8; 136] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/bbc_avc3.bin" + )); fn bbc_expected() -> Avc3 { Avc3 {