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/avc1.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc1.rs index ad0417e..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,8 +1,10 @@ use crate::*; +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))] -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 new file mode 100644 index 0000000..8431e22 --- /dev/null +++ b/src/moov/trak/mdia/minf/stbl/stsd/h264/avc3.rs @@ -0,0 +1,146 @@ +use super::avc1::AvcSampleEntry; + +const AVC3_CODE: u32 = u32::from_be_bytes(*b"avc3"); + +pub type Avc3 = AvcSampleEntry<{ AVC3_CODE }>; + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + + fn base(compressor: &str) -> Avc3 { + Avc3 { + visual: Visual { + data_reference_index: 1, + width: 320, + height: 240, + horizresolution: 0x48.into(), + vertresolution: 0x48.into(), + frame_count: 1, + compressor: compressor.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() + }, + ..Default::default() + } + } + + // 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(); + + let mut buf = buf.as_ref(); + let decoded = Avc3::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_avc3() { + roundtrip(base("ya boy")); + } + + #[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); + } + + #[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/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs b/src/moov/trak/mdia/minf/stbl/stsd/h264/mod.rs index 001a378..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,4 +1,5 @@ mod avc1; +mod avc3; mod avcc; // Incomplete H264 decoder, saved for possible future use @@ -7,4 +8,5 @@ mod avcc; //mod sps; pub use avc1::*; +pub use avc3::*; pub use avcc::*; 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), 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 { diff --git a/tests/data/bbc_avc3.bin b/tests/data/bbc_avc3.bin new file mode 100644 index 0000000..fcec2c6 Binary files /dev/null and b/tests/data/bbc_avc3.bin differ