From 89c11b400f9aefcc74d390c81b62d328f4c60565 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Fri, 9 Jan 2026 14:24:04 +1100 Subject: [PATCH 01/16] spike on testing a bunch of files from the MPEG file format set --- .gitmodules | 3 ++ FileFormatConformance | 1 + src/test/mod.rs | 1 + src/test/mpeg_file_format_conformance.rs | 42 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 .gitmodules create mode 160000 FileFormatConformance create mode 100644 src/test/mpeg_file_format_conformance.rs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4e5ee9d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "FileFormatConformance"] + path = FileFormatConformance + url = https://github.com/MPEGGroup/FileFormatConformance.git diff --git a/FileFormatConformance b/FileFormatConformance new file mode 160000 index 0000000..767a5bc --- /dev/null +++ b/FileFormatConformance @@ -0,0 +1 @@ +Subproject commit 767a5bcb9fa4841d1b3bb0ca112a6e7d4c6bf067 diff --git a/src/test/mod.rs b/src/test/mod.rs index 70b5800..9b80f55 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -5,5 +5,6 @@ mod flac; mod h264; mod hevc; mod image; +mod mpeg_file_format_conformance; mod uncompressed; mod vp9; diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs new file mode 100644 index 0000000..a95eb8e --- /dev/null +++ b/src/test/mpeg_file_format_conformance.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use crate::{Any, ReadFrom}; + +#[test] +fn test_published() { + let suppressed: Vec = vec![ + "FileFormatConformance/data/file_features/published/isobmff/fragment_random_access-2.mp4" + .into(), + "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/FX-VY-9436R.3_qhd-variant.mp4" + .into(), + "FileFormatConformance/data/file_features/published/isobmff/FX-VY-9436R.3_qhd.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/timed-metadata.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/fragment-random-access-1+AF8-rev1.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/sg-tl-st.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/09_text.mp4".into(), + ]; + + let paths = + std::fs::read_dir("FileFormatConformance/data/file_features/published/isobmff/").unwrap(); + for path in paths { + let direntry = path.unwrap(); + let path = direntry.path().into_os_string().into_string().unwrap(); + if path.ends_with(".mp4") && !suppressed.contains(&path) { + println!("checking {:?}", direntry); + check_one_file(&direntry.path()); + } + } +} + +fn check_one_file(path: &PathBuf) { + let mut input = std::fs::File::open(path).unwrap(); + while let Some(atom) = Option::::read_from(&mut input).unwrap() { + if let Any::Unknown(kind, data) = atom { + panic!("Unknown {{ kind: {:?}, size: {:?} }}", kind, data.len()); + } + } +} From ea8d15f058449407ccd7539a9f942de4f0f6d23c Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Fri, 9 Jan 2026 14:32:27 +1100 Subject: [PATCH 02/16] checkout submodule in CI --- .github/workflows/pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b946deb..44b8a7a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + submodules: true # Install Just to run CI scripts - uses: extractions/setup-just@v2 From b6eeebf26aa0a9149b87ccbf181c854d773ed183 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Fri, 9 Jan 2026 14:36:48 +1100 Subject: [PATCH 03/16] add LFS support --- .github/workflows/pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 44b8a7a..4f9de92 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,6 +15,7 @@ jobs: - uses: actions/checkout@v3 with: submodules: true + lfs: true # Install Just to run CI scripts - uses: extractions/setup-just@v2 From cd8d752f48d83849c44bd3fa5dfbfb11337110bf Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Sat, 10 Jan 2026 20:56:00 +1100 Subject: [PATCH 04/16] improve test handling --- src/test/mpeg_file_format_conformance.rs | 43 ++++++++++++++++-------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index a95eb8e..225458a 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -4,19 +4,12 @@ use crate::{Any, ReadFrom}; #[test] fn test_published() { - let suppressed: Vec = vec![ - "FileFormatConformance/data/file_features/published/isobmff/fragment_random_access-2.mp4" - .into(), + let expected_fails: Vec = vec![ "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/FX-VY-9436R.3_qhd-variant.mp4" - .into(), - "FileFormatConformance/data/file_features/published/isobmff/FX-VY-9436R.3_qhd.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/timed-metadata.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/fragment-random-access-1+AF8-rev1.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/sg-tl-st.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/09_text.mp4".into(), ]; @@ -25,18 +18,40 @@ fn test_published() { for path in paths { let direntry = path.unwrap(); let path = direntry.path().into_os_string().into_string().unwrap(); - if path.ends_with(".mp4") && !suppressed.contains(&path) { + if path.ends_with(".mp4") { println!("checking {:?}", direntry); - check_one_file(&direntry.path()); + match check_one_file(&direntry.path()) { + true => assert!( + !expected_fails.contains(&path), + "expected {path} to fail, but it unexpectedly passed" + ), + false => assert!( + expected_fails.contains(&path), + "expected {path} to pass, but it unexpectedly failed" + ), + } } } } -fn check_one_file(path: &PathBuf) { +fn check_one_file(path: &PathBuf) -> bool { let mut input = std::fs::File::open(path).unwrap(); - while let Some(atom) = Option::::read_from(&mut input).unwrap() { - if let Any::Unknown(kind, data) = atom { - panic!("Unknown {{ kind: {:?}, size: {:?} }}", kind, data.len()); + let mut full_parse = true; + loop { + let parse_result = Option::::read_from(&mut input); + match parse_result { + Ok(maybe_atom) => match maybe_atom { + Some(_) => {} + None => { + break; + } + }, + Err(err) => { + println!("{err:#?}"); + full_parse = false; + break; + } } } + full_parse } From 79cd6604c28b89be93ae96eeb7e48c1aa1b2f6cb Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Sat, 10 Jan 2026 21:31:31 +1100 Subject: [PATCH 05/16] recurse full published directory --- src/test/mpeg_file_format_conformance.rs | 61 +++++++++++++++++------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 225458a..2727c95 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::{Any, ReadFrom}; @@ -11,24 +11,51 @@ fn test_published() { "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/09_text.mp4".into(), + "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), + "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C041.heic".into(), + "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-1.iso3" + .into(), + "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-2.iso3" + .into(), + "FileFormatConformance/data/file_features/published/isobmff/mbms-fec.iso3".into(), ]; - let paths = - std::fs::read_dir("FileFormatConformance/data/file_features/published/isobmff/").unwrap(); - for path in paths { - let direntry = path.unwrap(); - let path = direntry.path().into_os_string().into_string().unwrap(); - if path.ends_with(".mp4") { - println!("checking {:?}", direntry); - match check_one_file(&direntry.path()) { - true => assert!( - !expected_fails.contains(&path), - "expected {path} to fail, but it unexpectedly passed" - ), - false => assert!( - expected_fails.contains(&path), - "expected {path} to pass, but it unexpectedly failed" - ), + for entry in std::fs::read_dir("FileFormatConformance/data/file_features/published").unwrap() { + let direntry = entry.unwrap(); + let path = direntry.path(); + if path.is_dir() { + check_directory(&expected_fails, &path); + } + } +} + +fn check_directory(expected_fails: &Vec, directory: &Path) { + for entry in std::fs::read_dir(directory).unwrap() { + let direntry = entry.unwrap(); + let path = direntry.path(); + if path.is_dir() { + check_directory(expected_fails, &path); + } else { + let filepath = direntry.path().into_os_string().into_string().unwrap(); + if !filepath.ends_with(".json") + && !filepath.ends_with(".dat") + && !filepath.ends_with(".zip") + && !filepath.ends_with(".txt") + && !filepath.ends_with(".xml") + { + println!("checking {:?}", direntry); + match check_one_file(&direntry.path()) { + true => assert!( + !expected_fails.contains(&filepath), + "expected {filepath} to fail, but it unexpectedly passed" + ), + false => assert!( + expected_fails.contains(&filepath), + "expected {filepath} to pass, but it unexpectedly failed" + ), + } } } } From 3625389889d15395b6c7f6cecb771fab6da0e82b Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Mon, 12 Jan 2026 11:46:29 +1100 Subject: [PATCH 06/16] remove suppression for tx3g tests that now work --- src/test/mpeg_file_format_conformance.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 2727c95..0c9e152 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -7,10 +7,8 @@ fn test_published() { let expected_fails: Vec = vec![ "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/timed-metadata.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/09_text.mp4".into(), "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), From 7e2624664d6caca44b33d3f6e06f8f75b1082a48 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Wed, 14 Jan 2026 10:56:36 +1100 Subject: [PATCH 07/16] add support for stz2, make stsz optional --- src/any.rs | 1 + src/atom.rs | 50 ++++++ src/moov/mod.rs | 1 + src/moov/trak/mdia/minf/stbl/mod.rs | 25 ++- src/moov/trak/mdia/minf/stbl/stz2.rs | 209 +++++++++++++++++++++++ src/test/av1.rs | 3 +- src/test/bbb.rs | 8 +- src/test/esds.rs | 8 +- src/test/flac.rs | 3 +- src/test/h264.rs | 6 +- src/test/hevc.rs | 1 + src/test/mpeg_file_format_conformance.rs | 6 +- src/test/uncompressed.rs | 5 +- src/test/vp9.rs | 3 +- 14 files changed, 304 insertions(+), 25 deletions(-) create mode 100644 src/moov/trak/mdia/minf/stbl/stz2.rs diff --git a/src/any.rs b/src/any.rs index 295c137..9f63ce4 100644 --- a/src/any.rs +++ b/src/any.rs @@ -304,6 +304,7 @@ any! { Stts, Stsc, Stsz, + Stz2, Stss, Stco, Co64, diff --git a/src/atom.rs b/src/atom.rs index 7c1b4b2..87632b6 100644 --- a/src/atom.rs +++ b/src/atom.rs @@ -210,6 +210,56 @@ macro_rules! nested { }) } + fn encode_body(&self, buf: &mut B) -> Result<()> { + $( self.[<$required:lower>].encode(buf)?; )* + $( self.[<$optional:lower>].encode(buf)?; )* + $( self.[<$multiple:lower>].iter().map(|x| x.encode(buf)).collect::>()?; )* + + Ok(()) + } + } + }; + (required: [$($required:ident),*$(,)?], optional: [$($optional:ident),*$(,)?], multiple: [$($multiple:ident),*$(,)?], post_parse: $some_fn:ident, ) => { + paste::paste! { + fn decode_body(buf: &mut B) -> Result { + $( let mut [<$required:lower>] = None;)* + $( let mut [<$optional:lower>] = None;)* + $( let mut [<$multiple:lower>] = Vec::new();)* + + while let Some(atom) = Any::decode_maybe(buf)? { + match atom { + $(Any::$required(atom) => { + if [<$required:lower>].is_some() { + return Err(Error::DuplicateBox($required::KIND)); + } + [<$required:lower>] = Some(atom); + },)* + $(Any::$optional(atom) => { + if [<$optional:lower>].is_some() { + return Err(Error::DuplicateBox($optional::KIND)); + } + [<$optional:lower>] = Some(atom); + },)* + $(Any::$multiple(atom) => { + [<$multiple:lower>].push(atom.into()); + },)* + Any::Skip(atom) => tracing::debug!(size = atom.zeroed.size, "skipping skip box"), + Any::Free(atom) => tracing::debug!(size = atom.zeroed.size, "skipping free box"), + unknown => Self::decode_unknown(&unknown)?, + } + } + + let result = Self { + $([<$required:lower>]: [<$required:lower>].ok_or(Error::MissingBox($required::KIND))? ,)* + $([<$optional:lower>],)* + $([<$multiple:lower>],)* + }; + + result.[<$some_fn>]()?; + + Ok(result) + } + fn encode_body(&self, buf: &mut B) -> Result<()> { $( self.[<$required:lower>].encode(buf)?; )* $( self.[<$optional:lower>].encode(buf)?; )* diff --git a/src/moov/mod.rs b/src/moov/mod.rs index 3d7137c..07e1825 100644 --- a/src/moov/mod.rs +++ b/src/moov/mod.rs @@ -224,6 +224,7 @@ mod test { .into()], }, stco: Some(Stco::default()), + stsz: Some(Stsz::default()), ..Default::default() } } diff --git a/src/moov/trak/mdia/minf/stbl/mod.rs b/src/moov/trak/mdia/minf/stbl/mod.rs index 6c02aaf..da0dac1 100644 --- a/src/moov/trak/mdia/minf/stbl/mod.rs +++ b/src/moov/trak/mdia/minf/stbl/mod.rs @@ -10,6 +10,7 @@ mod stsd; mod stss; mod stsz; mod stts; +mod stz2; mod subs; pub use co64::*; @@ -24,6 +25,7 @@ pub use stsd::*; pub use stss::*; pub use stsz::*; pub use stts::*; +pub use stz2::*; pub use subs::*; use crate::*; @@ -36,7 +38,8 @@ pub struct Stbl { pub ctts: Option, pub stss: Option, pub stsc: Stsc, - pub stsz: Stsz, + pub stsz: Option, + pub stz2: Option, pub stco: Option, pub co64: Option, pub sbgp: Vec, @@ -47,12 +50,28 @@ pub struct Stbl { pub cslg: Option, } +impl Stbl { + fn do_validation(&self) -> Result<()> { + if self.stsz.is_none() && self.stz2.is_none() { + return Err(Error::MissingContent( + "one of stsz or stz2 is required in stbl box", + )); + } + if self.stsz.is_some() && self.stz2.is_some() { + // TODO: some kind of better error + return Err(Error::UnexpectedBox(Stz2::KIND)); + } + Ok(()) + } +} + impl Atom for Stbl { const KIND: FourCC = FourCC::new(b"stbl"); nested! { - required: [ Stsd, Stts, Stsc, Stsz ], - optional: [ Ctts, Stss, Stco, Co64, Cslg ], + required: [ Stsd, Stts, Stsc ], + optional: [ Ctts, Stss, Stco, Co64, Cslg, Stsz, Stz2 ], multiple: [ Sbgp, Sgpd, Subs, Saiz, Saio ], + post_parse: do_validation, } } diff --git a/src/moov/trak/mdia/minf/stbl/stz2.rs b/src/moov/trak/mdia/minf/stbl/stz2.rs new file mode 100644 index 0000000..0b04262 --- /dev/null +++ b/src/moov/trak/mdia/minf/stbl/stz2.rs @@ -0,0 +1,209 @@ +use num::Integer; + +use crate::*; + +/// Compact Sample Size Box (stz2) +/// +/// Lists the size of each sample in the track. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Stz2 { + pub entry_sizes: Vec, +} + +impl AtomExt for Stz2 { + type Ext = (); + + const KIND_EXT: FourCC = FourCC::new(b"stz2"); + + fn decode_body_ext(buf: &mut B, _ext: ()) -> Result { + buf.advance(3); // reserved = 0 + let field_size = u8::decode(buf)?; + let sample_count = u32::decode(buf)?; + let mut entry_sizes = Vec::::with_capacity(std::cmp::min(128, sample_count as usize)); + + match field_size { + 4 => { + // This is to handle the case where there are an odd number entry_sizes, and + // we don't want to add the last one. + let mut remaining = 0u16; + for i in 0..sample_count { + if i.is_even() { + let entry_pair = u8::decode(buf)? as u16; + entry_sizes.push(entry_pair >> 4); + remaining = entry_pair & 0x0f; + } else { + entry_sizes.push(remaining); + } + } + } + 8 => { + for _ in 0..sample_count { + entry_sizes.push(u8::decode(buf)? as u16); + } + } + 16 => { + for _ in 0..sample_count { + entry_sizes.push(u16::decode(buf)?); + } + } + _ => { + return Err(Error::InvalidSize); + } + } + + Ok(Stz2 { entry_sizes }) + } + + fn encode_body_ext(&self, buf: &mut B) -> Result<()> { + // field_size is u8, but treating it as u32 is a convenient way + // to handle the 3 reserved bytes. + let mut field_size = 4u32; + for entry_size in &self.entry_sizes { + if *entry_size >= 16 { + // need more than 4 bits + field_size = 8u32; + } + if *entry_size > u8::MAX.into() { + field_size = 16u32; + break; + } + } + field_size.encode(buf)?; + let sample_count: u32 = self + .entry_sizes + .len() + .try_into() + .map_err(|_| Error::TooLarge(Self::KIND))?; + sample_count.encode(buf)?; + match field_size { + 16u32 => { + for entry_size in &self.entry_sizes { + entry_size.encode(buf)?; + } + } + 8u32 => { + for entry_size in &self.entry_sizes { + let entry_size_u8: u8 = *entry_size as u8; + entry_size_u8.encode(buf)?; + } + } + 4u32 => { + let mut packed_entries = + Vec::::with_capacity(self.entry_sizes.len().div_ceil(2)); + for i in 0..self.entry_sizes.len() { + let sample_size = self + .entry_sizes + .get(i) + .expect("there should be a value given we are iterating over the length"); + if (sample_size & 0b1111) != *sample_size { + // There is no way this should be able to happen. + return Err(Error::InvalidSize); + } + let sample_size_u4 = (sample_size & 0b1111) as u8; + if i.is_even() { + packed_entries.push(sample_size_u4 << 4); + } else { + let i = packed_entries.get_mut(i / 2).expect( + "there should be a value given we are iterating over the length", + ); + *i |= sample_size_u4; + } + } + for packed_entry in packed_entries { + packed_entry.encode(buf)?; + } + } + _ => { + // There is no way this should be able to happen - only ever assign those 3 values, and those are the only valid values. + return Err(Error::InvalidSize); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stz2_8() { + let expected = Stz2 { + entry_sizes: vec![15, 16, 3], + }; + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let mut buf = buf.as_ref(); + assert_eq!( + buf, + vec![ + 0x00, 0x00, 0x00, 23, b's', b't', b'z', b'2', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, 0x0f, 0x10, 0x03 + ] + ); + let decoded = Stz2::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_stz2_4() { + let expected = Stz2 { + entry_sizes: vec![15, 3, 6], + }; + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let mut buf = buf.as_ref(); + assert_eq!( + buf, + vec![ + 0x00, 0x00, 0x00, 22, b's', b't', b'z', b'2', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0xf3, 0x60 + ] + ); + let decoded = Stz2::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_stz2_4_even() { + let expected = Stz2 { + entry_sizes: vec![15, 3, 6, 8], + }; + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let mut buf = buf.as_ref(); + assert_eq!( + buf, + vec![ + 0x00, 0x00, 0x00, 22, b's', b't', b'z', b'2', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0xf3, 0x68 + ] + ); + let decoded = Stz2::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_stz2_16() { + let expected = Stz2 { + entry_sizes: vec![255, 256, 65535], + }; + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let mut buf = buf.as_ref(); + assert_eq!( + buf, + vec![ + 0x00, 0x00, 0x00, 26, b's', b't', b'z', b'2', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x03, 0x00, 0xff, 0x01, 0x00, 0xff, 0xff, + ] + ); + let decoded = Stz2::decode(&mut buf).unwrap(); + assert_eq!(decoded, expected); + } +} diff --git a/src/test/av1.rs b/src/test/av1.rs index 161776e..308b9a6 100644 --- a/src/test/av1.rs +++ b/src/test/av1.rs @@ -154,7 +154,8 @@ fn av1() { ctts: None, stss: None, stsc: Stsc::default(), - stsz: Stsz::default(), + stsz: Some(Stsz::default()), + stz2: None, stco: Some(Stco::default()), co64: None, sbgp: vec![], diff --git a/src/test/bbb.rs b/src/test/bbb.rs index 0deea90..f480018 100644 --- a/src/test/bbb.rs +++ b/src/test/bbb.rs @@ -114,9 +114,7 @@ fn bbb() { stsc: Stsc { ..Default::default() }, - stsz: Stsz { - ..Default::default() - }, + stsz: Some(Stsz::default()), stco: Some(Stco { ..Default::default() }), ..Default::default() }, @@ -191,9 +189,7 @@ fn bbb() { stsc: Stsc { ..Default::default() }, - stsz: Stsz { - ..Default::default() - }, + stsz: Some(Stsz::default()), stco: Some(Stco { ..Default::default() }), ..Default::default() }, diff --git a/src/test/esds.rs b/src/test/esds.rs index c5c9ec2..69dc43e 100644 --- a/src/test/esds.rs +++ b/src/test/esds.rs @@ -114,9 +114,7 @@ fn esds() { stsc: Stsc { ..Default::default() }, - stsz: Stsz { - ..Default::default() - }, + stsz: Some(Stsz::default()), stco: Some(Stco { ..Default::default() }), ..Default::default() }, @@ -191,9 +189,7 @@ fn esds() { stsc: Stsc { ..Default::default() }, - stsz: Stsz { - ..Default::default() - }, + stsz: Some(Stsz::default()), stco: Some(Stco { ..Default::default() }), ..Default::default() }, diff --git a/src/test/flac.rs b/src/test/flac.rs index 15b113c..7677ea3 100644 --- a/src/test/flac.rs +++ b/src/test/flac.rs @@ -142,7 +142,8 @@ fn flac() { ctts: None, stss: None, stsc: Stsc { entries: vec![] }, - stsz: Stsz::default(), + stsz: Some(Stsz::default()), + stz2: None, stco: Some(Stco { entries: [].into() }), co64: None, sbgp: vec![], diff --git a/src/test/h264.rs b/src/test/h264.rs index 8e74366..9db5b42 100644 --- a/src/test/h264.rs +++ b/src/test/h264.rs @@ -169,7 +169,8 @@ fn avcc_ext() { ctts: None, stss: None, stsc: Stsc::default(), - stsz: Stsz::default(), + stsz: Some(Stsz::default()), + stz2: None, stco: Some(Stco::default()), co64: None, sbgp: vec![], @@ -271,7 +272,8 @@ fn avcc_ext() { ctts: None, stss: None, stsc: Stsc::default(), - stsz: Stsz::default(), + stsz: Some(Stsz::default()), + stz2: None, stco: Some(Stco::default()), co64: None, sbgp: vec![], diff --git a/src/test/hevc.rs b/src/test/hevc.rs index 1078922..02b9a53 100644 --- a/src/test/hevc.rs +++ b/src/test/hevc.rs @@ -379,6 +379,7 @@ fn hevc() { .into()], }, stco: Some(Stco { entries: vec![] }), + stsz: Some(Stsz::default()), ..Default::default() } } diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 0c9e152..2155fba 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -5,10 +5,10 @@ use crate::{Any, ReadFrom}; #[test] fn test_published() { let expected_fails: Vec = vec![ - "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), + // "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/timed-metadata.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), + // "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), + // "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), diff --git a/src/test/uncompressed.rs b/src/test/uncompressed.rs index 51ab967..bf39b19 100644 --- a/src/test/uncompressed.rs +++ b/src/test/uncompressed.rs @@ -199,9 +199,10 @@ fn uncompressed() { ] .into() }, - stsz: Stsz { + stsz: Some(Stsz { samples: StszSamples::Identical { count: 2, size: 6 }, - }, + }), + stz2: None, stco: Some(Stco { entries: [856, 862].into() }), diff --git a/src/test/vp9.rs b/src/test/vp9.rs index 218e4d6..456ed8e 100644 --- a/src/test/vp9.rs +++ b/src/test/vp9.rs @@ -164,7 +164,8 @@ fn vp9() { ctts: None, stss: None, stsc: Stsc { entries: vec![] }, - stsz: Stsz::default(), + stsz: Some(Stsz::default()), + stz2: None, stco: Some(Stco { entries: vec![] }), co64: None, sbgp: vec![], From 0fc40c5ec96bda007d3ff6abacef50d7560c972a Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 12:57:09 +1100 Subject: [PATCH 08/16] additional suppressions required now we have strict mode --- src/test/mpeg_file_format_conformance.rs | 104 ++++++++++++++++++++++- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 2155fba..bd89f47 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -5,19 +5,117 @@ use crate::{Any, ReadFrom}; #[test] fn test_published() { let expected_fails: Vec = vec![ - // "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev2_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev1_lhe1_multiple_tracks_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_hvc2_multiple_tracks_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hvc2_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/3gp/pdin_example.3gp".into(), + "FileFormatConformance/data/file_features/published/3gp/female_amr67DTX_hinted.3gp".into(), + "FileFormatConformance/data/file_features/published/3gp/female_amr67_hinted.3gp".into(), + "FileFormatConformance/data/file_features/published/3gp/male_amr122.3gp".into(), + "FileFormatConformance/data/file_features/published/3gp/male_amr122DTX.3gp".into(), + "FileFormatConformance/data/file_features/published/3gp/rs_example_r1.3gp".into(), + "FileFormatConformance/data/file_features/published/mpeg-audio-conformance/ac01.mp4".into(), + "FileFormatConformance/data/file_features/published/mpeg-audio-conformance/sls2100_aot02_048_16.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/01_simple.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/02_dref_edts_img.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/03_hinted.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/06_bifs.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/07_bifs_sprite.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/09_text.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/timed-metadata.mp4".into(), - // "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), - // "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a5-foreman-AVC.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a6_tone_multifile.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a7-tone-oddities.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/04_bifs_video.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/10_fragments.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/12_metas_v2.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/13_long.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/FX-VY-9436R.3_qhd-variant.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/FX-VY-9436R.3_qhd.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/05_bifs_video_protected_v2.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/20_stxt.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a4-tone-fragmented.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/21_segment.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/sg-tl-st.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/restricted.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/17_negative_ctso.mp4".into(), + "FileFormatConformance/data/file_features/published/green/meta_2500000bps_0.mp4m".into(), "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), + "FileFormatConformance/data/file_features/published/heif/C001.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C026.heic".into(), "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C029.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C030.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C031.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C032.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C036.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C037.heic".into(), + "FileFormatConformance/data/file_features/published/heif/C038.heic".into(), "FileFormatConformance/data/file_features/published/heif/C041.heic".into(), "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-1.iso3" .into(), "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-2.iso3" .into(), "FileFormatConformance/data/file_features/published/isobmff/mbms-fec.iso3".into(), + "FileFormatConformance/data/file_features/published/isobmff/fragment_random_access-2.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/fragment-random-access-1+AF8-rev1.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a9-aac-samplegroups-edit.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a1-foreman-QCIF.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a2-foreman-QCIF-hinted.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a10-foreman_QCIF-raw.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a3-tone-protected.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a3b-tone-deprot.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/f1.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/f2.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/08_bifs_carousel_v2.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/16_vtt.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/19_ttml.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/18_pssh_v2.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/a8-foreman_QCIF_edit.mp4".into(), + "FileFormatConformance/data/file_features/published/isobmff/rtp_rtcp_reception_hint_tracks_v2.mp4".into(), + "FileFormatConformance/data/file_features/published/maf/vsaf/1.mp4".into(), + "FileFormatConformance/data/file_features/published/uvvu/Solekai002_1280_23_1x1_v7clear.uvvu".into(), + "FileFormatConformance/data/file_features/published/uvvu/Solekai007_1920_29_1x1_v7clear.uvu".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_single_track_trif_full_picture.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/alst_hvc1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_multiple_tracks.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_single_track_nalm_rle.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/trgr_hvc1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hvc2_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/aggr_hvc1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hvc1_hvc2_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hvc1_hvc2_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hev1_clg1_header.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hev1_hev2_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hev1_hev2_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_single_track_nalm.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/subs_tile_hvc1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/subs_slice_hvc1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_single_track_nalm_all_intra.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_multiple_tracks_empty_base.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/svc/mp4-live-LastTime-depRep.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/mvc/DDF_10s_25fps.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/mvc/DDF_10s_25fps-dynamic.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc2_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hvc1_hvc2_multiple_tracks_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev2_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/lhevc_avc3_lhe1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_lhv1_multiple_tracks_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/lhevc_avc3_lhv1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev1_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hvc1_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/lhevc_avc1_lhe1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev1_lhe1_multiple_tracks_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hvc1_lhv1_multiple_tracks_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev1_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev1_hev2_multiple_tracks_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev1_hev2_multiple_tracks_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/lhevc_avc1_lhv1.mp4".into(), + ]; for entry in std::fs::read_dir("FileFormatConformance/data/file_features/published").unwrap() { From 64d5e7825e389210d29ae209ce6094fb1ef73d3f Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 13:28:44 +1100 Subject: [PATCH 09/16] wip on ccst for the heif tests --- src/test/mpeg_file_format_conformance.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index bd89f47..771b14f 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -44,17 +44,17 @@ fn test_published() { "FileFormatConformance/data/file_features/published/isobmff/17_negative_ctso.mp4".into(), "FileFormatConformance/data/file_features/published/green/meta_2500000bps_0.mp4m".into(), "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), - "FileFormatConformance/data/file_features/published/heif/C001.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C026.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C001.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C026.heic".into(), "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C029.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C030.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C031.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C029.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C030.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C031.heic".into(), "FileFormatConformance/data/file_features/published/heif/C032.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C036.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C037.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C038.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C036.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C037.heic".into(), + // "FileFormatConformance/data/file_features/published/heif/C038.heic".into(), "FileFormatConformance/data/file_features/published/heif/C041.heic".into(), "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-1.iso3" .into(), From 5eba93a46357223d5023ca58e98a6df8c26a1983 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 13:30:12 +1100 Subject: [PATCH 10/16] add support for ccst child in hvc1 This is required in HEIF image sequences (i.e. a track with `pict` as the handler), per ISO/IEC 23008-12. --- .../trak/mdia/minf/stbl/stsd/hevc/hvc1.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvc1.rs b/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvc1.rs index ebd9648..9991344 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvc1.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/hevc/hvc1.rs @@ -11,6 +11,7 @@ pub struct Hvc1 { pub pasp: Option, pub taic: Option, pub fiel: Option, + pub ccst: Option, } impl Atom for Hvc1 { @@ -25,6 +26,7 @@ impl Atom for Hvc1 { let mut pasp = None; let mut taic = None; let mut fiel = None; + let mut ccst = None; while let Some(atom) = Any::decode_maybe(buf)? { match atom { Any::Hvcc(atom) => hvcc = atom.into(), @@ -33,6 +35,7 @@ impl Atom for Hvc1 { Any::Pasp(atom) => pasp = atom.into(), Any::Taic(atom) => taic = atom.into(), Any::Fiel(atom) => fiel = atom.into(), + Any::Ccst(atom) => ccst = atom.into(), unknown => Self::decode_unknown(&unknown)?, } } @@ -45,6 +48,7 @@ impl Atom for Hvc1 { pasp, taic, fiel, + ccst, }) } @@ -56,6 +60,109 @@ impl Atom for Hvc1 { self.pasp.encode(buf)?; self.taic.encode(buf)?; self.fiel.encode(buf)?; + self.ccst.encode(buf)?; Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + // From MPEG File Format Conformance, heif/C001.heif + const ENCODED_HVC1_HEIF: &[u8] = &[ + 0x00, 0x00, 0x00, 0xd2, 0x68, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x05, 0x00, 0x02, 0xd0, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x1f, 0x48, 0x45, 0x56, 0x43, 0x20, 0x43, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x6c, + 0x68, 0x76, 0x63, 0x43, 0x01, 0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x78, 0xf0, 0x00, 0xfc, 0xfd, 0xf8, 0xf8, 0x00, 0x00, 0x0f, 0x03, 0xa0, 0x00, 0x01, + 0x00, 0x18, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xf0, 0x24, 0xa1, 0x00, 0x01, 0x00, + 0x1f, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2d, 0x1f, 0xe5, 0xf9, 0x24, 0x6d, + 0x9e, 0xd9, 0xa2, 0x00, 0x01, 0x00, 0x07, 0x44, 0x01, 0xc1, 0x90, 0x95, 0x81, 0x12, 0x00, + 0x00, 0x00, 0x10, 0x63, 0x63, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + ]; + + #[test] + fn test_hvc1_with_ccst() { + let mut buf = std::io::Cursor::new(ENCODED_HVC1_HEIF); + + let hvc1 = Hvc1::decode(&mut buf).expect("failed to decode hvc1"); + + assert_eq!( + hvc1, + Hvc1 { + visual: Visual { + data_reference_index: 1, + width: 1280, + height: 720, + horizresolution: 72.into(), + vertresolution: 72.into(), + frame_count: 1, + compressor: "\x1fHEVC Coding".into(), + depth: 24 + }, + hvcc: Hvcc { + configuration_version: 1, + general_profile_space: 0, + general_tier_flag: false, + general_profile_idc: 1, + general_profile_compatibility_flags: [96, 0, 0, 0], + general_constraint_indicator_flags: [0, 0, 0, 0, 0, 0], + general_level_idc: 120, + min_spatial_segmentation_idc: 0, + parallelism_type: 0, + chroma_format_idc: 1, + bit_depth_luma_minus8: 0, + bit_depth_chroma_minus8: 0, + avg_frame_rate: 0, + constant_frame_rate: 0, + num_temporal_layers: 1, + temporal_id_nested: true, + length_size_minus_one: 3, + arrays: vec![ + HvcCArray { + completeness: true, + nal_unit_type: 32, + nalus: vec![vec![ + 64, 1, 12, 1, 255, 255, 1, 96, 0, 0, 3, 0, 0, 3, 0, 0, 3, 0, 0, 3, + 0, 120, 240, 36 + ]] + }, + HvcCArray { + completeness: true, + nal_unit_type: 33, + nalus: vec![vec![ + 66, 1, 1, 1, 96, 0, 0, 3, 0, 0, 3, 0, 0, 3, 0, 0, 3, 0, 120, 160, + 2, 128, 128, 45, 31, 229, 249, 36, 109, 158, 217 + ]] + }, + HvcCArray { + completeness: true, + nal_unit_type: 34, + nalus: vec![vec![68, 1, 193, 144, 149, 129, 18]] + } + ] + }, + btrt: None, + colr: None, + pasp: None, + taic: None, + fiel: None, + ccst: Some(Ccst { + all_ref_pics_intra: true, + intra_pred_used: false, + max_ref_per_pic: 0 + }) + } + ); + + let mut encoded = Vec::new(); + hvc1.encode(&mut encoded).expect("failed to encode hvc1"); + assert_eq!(encoded, ENCODED_HVC1_HEIF); + } +} From a5e748f0d8b1a13b4f99b0f5d903f00977e60106 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 14:46:16 +1100 Subject: [PATCH 11/16] wip on nmhd and sthd --- src/test/mpeg_file_format_conformance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 771b14f..02abf75 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -38,7 +38,7 @@ fn test_published() { "FileFormatConformance/data/file_features/published/isobmff/20_stxt.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/a4-tone-fragmented.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/21_segment.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), + // "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/sg-tl-st.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/restricted.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/17_negative_ctso.mp4".into(), From c4f684bedf7f526d155c009b62565363aa7b4e7a Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 14:47:23 +1100 Subject: [PATCH 12/16] add nmhd and sthd boxes nmhd is the null media header box, and is used for metadata and text tracks (14496-12, Sections 8.4.5.2, 12.3.2 and 12.5.2 sthd is the subtitle header box (14496-12, Section 12.6.2) These boxes are very similar, and the test changes would affect both. --- src/any.rs | 2 ++ src/moov/mod.rs | 4 ++-- src/moov/trak/mdia/minf/mod.rs | 8 ++++++- src/moov/trak/mdia/minf/nmhd.rs | 39 +++++++++++++++++++++++++++++++++ src/moov/trak/mdia/minf/sthd.rs | 39 +++++++++++++++++++++++++++++++++ src/test/av1.rs | 4 ++-- src/test/bbb.rs | 2 +- src/test/esds.rs | 2 +- src/test/flac.rs | 4 ++-- src/test/h264.rs | 4 ++-- src/test/hevc.rs | 4 ++-- src/test/uncompressed.rs | 4 ++-- src/test/vp9.rs | 4 ++-- 13 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 src/moov/trak/mdia/minf/nmhd.rs create mode 100644 src/moov/trak/mdia/minf/sthd.rs diff --git a/src/any.rs b/src/any.rs index 9f63ce4..2966bb7 100644 --- a/src/any.rs +++ b/src/any.rs @@ -317,7 +317,9 @@ any! { Saiz, Dinf, Dref, + Nmhd, Smhd, + Sthd, Vmhd, Edts, Elst, diff --git a/src/moov/mod.rs b/src/moov/mod.rs index 07e1825..563298c 100644 --- a/src/moov/mod.rs +++ b/src/moov/mod.rs @@ -180,7 +180,6 @@ mod test { blue: 0 } }), - smhd: None, dinf: Dinf { dref: Dref { urls: vec![Url::default()] @@ -226,7 +225,8 @@ mod test { stco: Some(Stco::default()), stsz: Some(Stsz::default()), ..Default::default() - } + }, + ..Default::default() } }, senc: None, diff --git a/src/moov/trak/mdia/minf/mod.rs b/src/moov/trak/mdia/minf/mod.rs index c57ef11..e3b3f95 100644 --- a/src/moov/trak/mdia/minf/mod.rs +++ b/src/moov/trak/mdia/minf/mod.rs @@ -1,11 +1,15 @@ mod dinf; +mod nmhd; mod smhd; mod stbl; +mod sthd; mod vmhd; pub use dinf::*; +pub use nmhd::*; pub use smhd::*; pub use stbl::*; +pub use sthd::*; pub use vmhd::*; use crate::*; @@ -15,6 +19,8 @@ use crate::*; pub struct Minf { pub vmhd: Option, pub smhd: Option, + pub nmhd: Option, + pub sthd: Option, pub dinf: Dinf, pub stbl: Stbl, } @@ -24,7 +30,7 @@ impl Atom for Minf { nested! { required: [ Dinf, Stbl ], - optional: [ Vmhd, Smhd ], + optional: [ Vmhd, Smhd, Nmhd, Sthd ], multiple: [], } } diff --git a/src/moov/trak/mdia/minf/nmhd.rs b/src/moov/trak/mdia/minf/nmhd.rs new file mode 100644 index 0000000..2786659 --- /dev/null +++ b/src/moov/trak/mdia/minf/nmhd.rs @@ -0,0 +1,39 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Nmhd {} + +impl AtomExt for Nmhd { + type Ext = (); + + const KIND_EXT: FourCC = FourCC::new(b"nmhd"); + + fn decode_body_ext(_buf: &mut B, _ext: ()) -> Result { + Ok(Nmhd {}) + } + + fn encode_body_ext(&self, _buf: &mut B) -> Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nmhd() { + let nmhd = Nmhd {}; + let mut buf = Vec::new(); + nmhd.encode(&mut buf).unwrap(); + assert_eq!( + buf, + vec![0x00, 0x00, 0x00, 0x0c, 0x6e, 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00] + ); + + let mut buf = buf.as_ref(); + let decoded = Nmhd::decode(&mut buf).unwrap(); + assert_eq!(decoded, nmhd); + } +} diff --git a/src/moov/trak/mdia/minf/sthd.rs b/src/moov/trak/mdia/minf/sthd.rs new file mode 100644 index 0000000..4e54495 --- /dev/null +++ b/src/moov/trak/mdia/minf/sthd.rs @@ -0,0 +1,39 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Sthd {} + +impl AtomExt for Sthd { + type Ext = (); + + const KIND_EXT: FourCC = FourCC::new(b"sthd"); + + fn decode_body_ext(_buf: &mut B, _ext: ()) -> Result { + Ok(Sthd {}) + } + + fn encode_body_ext(&self, _buf: &mut B) -> Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sthd() { + let sthd = Sthd {}; + let mut buf = Vec::new(); + sthd.encode(&mut buf).unwrap(); + assert_eq!( + buf, + vec![0x00, 0x00, 0x00, 0x0c, b's', b't', b'h', b'd', 0x00, 0x00, 0x00, 0x00] + ); + + let mut buf = buf.as_ref(); + let decoded = Sthd::decode(&mut buf).unwrap(); + assert_eq!(decoded, sthd); + } +} diff --git a/src/test/av1.rs b/src/test/av1.rs index 308b9a6..801335b 100644 --- a/src/test/av1.rs +++ b/src/test/av1.rs @@ -103,7 +103,6 @@ fn av1() { blue: 0 } }), - smhd: None, dinf: Dinf { dref: Dref { urls: vec![Url { @@ -164,7 +163,8 @@ fn av1() { saio: vec![], saiz: vec![], cslg: None, - } + }, + ..Default::default() } }, senc: None, diff --git a/src/test/bbb.rs b/src/test/bbb.rs index f480018..c3dd9c4 100644 --- a/src/test/bbb.rs +++ b/src/test/bbb.rs @@ -62,7 +62,6 @@ fn bbb() { name: "(C) 2007 Google Inc. v08.13.2007.".into(), }, minf: Minf { - smhd: None, vmhd: Vmhd { ..Default::default() } @@ -118,6 +117,7 @@ fn bbb() { stco: Some(Stco { ..Default::default() }), ..Default::default() }, + ..Default::default() }, }, ..Default::default() diff --git a/src/test/esds.rs b/src/test/esds.rs index 69dc43e..520d3de 100644 --- a/src/test/esds.rs +++ b/src/test/esds.rs @@ -62,7 +62,6 @@ fn esds() { name: "(C) 2007 Google Inc. v08.13.2007.".into(), }, minf: Minf { - smhd: None, vmhd: Vmhd { ..Default::default() } @@ -118,6 +117,7 @@ fn esds() { stco: Some(Stco { ..Default::default() }), ..Default::default() }, + ..Default::default() }, }, ..Default::default() diff --git a/src/test/flac.rs b/src/test/flac.rs index 7677ea3..f7fdb5f 100644 --- a/src/test/flac.rs +++ b/src/test/flac.rs @@ -91,7 +91,6 @@ fn flac() { name: "SoundHandler".into(), }, minf: Minf { - vmhd: None, smhd: Some(Smhd { balance: 0.into() }), dinf: Dinf { dref: Dref { @@ -152,7 +151,8 @@ fn flac() { saio: vec![], saiz: vec![], cslg: None, - } + }, + ..Default::default() } }, senc: None, diff --git a/src/test/h264.rs b/src/test/h264.rs index 9db5b42..eba3778 100644 --- a/src/test/h264.rs +++ b/src/test/h264.rs @@ -108,7 +108,6 @@ fn avcc_ext() { blue: 0, }, }), - smhd: None, dinf: Dinf { dref: Dref { urls: vec![Url::default()], @@ -180,6 +179,7 @@ fn avcc_ext() { saiz: vec![], cslg: None, }, + ..Default::default() }, }, senc: None, @@ -224,7 +224,6 @@ fn avcc_ext() { name: "L-SMASH Audio Handler".into(), }, minf: Minf { - vmhd: None, smhd: Some(Smhd::default()), dinf: Dinf { dref: Dref { @@ -283,6 +282,7 @@ fn avcc_ext() { saiz: vec![], cslg: None, }, + ..Default::default() }, }, senc: None, diff --git a/src/test/hevc.rs b/src/test/hevc.rs index 02b9a53..9d2ee82 100644 --- a/src/test/hevc.rs +++ b/src/test/hevc.rs @@ -98,7 +98,6 @@ fn hevc() { blue: 0 } }), - smhd: None, dinf: Dinf { dref: Dref { urls: vec![Url { @@ -381,7 +380,8 @@ fn hevc() { stco: Some(Stco { entries: vec![] }), stsz: Some(Stsz::default()), ..Default::default() - } + }, + ..Default::default() } }, senc: None, diff --git a/src/test/uncompressed.rs b/src/test/uncompressed.rs index bf39b19..1df6610 100644 --- a/src/test/uncompressed.rs +++ b/src/test/uncompressed.rs @@ -93,7 +93,6 @@ fn uncompressed() { blue: 0 } }), - smhd: None, dinf: Dinf { dref: Dref { urls: vec![Url { @@ -213,7 +212,8 @@ fn uncompressed() { saiz: vec![], saio: vec![], cslg: None, - } + }, + ..Default::default() } }, senc: None, diff --git a/src/test/vp9.rs b/src/test/vp9.rs index 456ed8e..2cca3ef 100644 --- a/src/test/vp9.rs +++ b/src/test/vp9.rs @@ -122,7 +122,6 @@ fn vp9() { blue: 0 } }), - smhd: None, dinf: Dinf { dref: Dref { urls: vec![Url { @@ -174,7 +173,8 @@ fn vp9() { saio: vec![], saiz: vec![], cslg: None, - } + }, + ..Default::default() } }, senc: None, From 8bc309227952709be7a51f985c7a4272b3f240fb Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 16:34:03 +1100 Subject: [PATCH 13/16] udta: add cprt and kind child boxes --- src/any.rs | 2 + src/iso639.rs | 36 ++++++++++++++++ src/lib.rs | 2 + src/moov/trak/mdia/mdhd.rs | 32 -------------- src/moov/udta/cprt.rs | 31 ++++++++++++++ src/moov/udta/kind.rs | 27 ++++++++++++ src/moov/udta/mod.rs | 87 +++++++++++++++++++++++++++++++++++++- src/test/av1.rs | 1 + src/test/bbb.rs | 1 + src/test/esds.rs | 5 +-- src/test/h264.rs | 2 +- src/test/hevc.rs | 1 + src/test/uncompressed.rs | 1 + 13 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 src/iso639.rs create mode 100644 src/moov/udta/cprt.rs create mode 100644 src/moov/udta/kind.rs diff --git a/src/any.rs b/src/any.rs index 2966bb7..45c8510 100644 --- a/src/any.rs +++ b/src/any.rs @@ -261,6 +261,8 @@ any! { Moov, Mvhd, Udta, + Cprt, + Kind, Skip, // Trak, // boxed to avoid large size differences between variants Tkhd, diff --git a/src/iso639.rs b/src/iso639.rs new file mode 100644 index 0000000..9b6e199 --- /dev/null +++ b/src/iso639.rs @@ -0,0 +1,36 @@ +pub fn language_string(language: u16) -> String { + let mut lang: [u16; 3] = [0; 3]; + + lang[0] = ((language >> 10) & 0x1F) + 0x60; + lang[1] = ((language >> 5) & 0x1F) + 0x60; + lang[2] = ((language) & 0x1F) + 0x60; + + // Decode utf-16 encoded bytes into a string. + String::from_utf16_lossy(&lang) +} + +pub fn language_code(language: &str) -> u16 { + let mut lang = language.encode_utf16(); + let mut code = (lang.next().unwrap_or(0) & 0x1F) << 10; + code += (lang.next().unwrap_or(0) & 0x1F) << 5; + code += lang.next().unwrap_or(0) & 0x1F; + code +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_language_code(lang: &str) { + let code = language_code(lang); + let lang2 = language_string(code); + assert_eq!(lang, lang2); + } + + #[test] + fn test_language_codes() { + test_language_code("und"); + test_language_code("eng"); + test_language_code("kor"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 61d6dcf..9d51eea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,6 +148,7 @@ mod free; mod ftyp; mod header; mod io; +mod iso639; mod mdat; mod meta; mod mfra; @@ -169,6 +170,7 @@ pub use free::*; pub use ftyp::*; pub use header::*; pub use io::*; +pub(crate) use iso639::*; pub use mdat::*; pub use meta::*; pub use mfra::*; diff --git a/src/moov/trak/mdia/mdhd.rs b/src/moov/trak/mdia/mdhd.rs index 846ea61..fdd78a4 100644 --- a/src/moov/trak/mdia/mdhd.rs +++ b/src/moov/trak/mdia/mdhd.rs @@ -65,42 +65,10 @@ impl AtomExt for Mdhd { } } -fn language_string(language: u16) -> String { - let mut lang: [u16; 3] = [0; 3]; - - lang[0] = ((language >> 10) & 0x1F) + 0x60; - lang[1] = ((language >> 5) & 0x1F) + 0x60; - lang[2] = ((language) & 0x1F) + 0x60; - - // Decode utf-16 encoded bytes into a string. - String::from_utf16_lossy(&lang) -} - -fn language_code(language: &str) -> u16 { - let mut lang = language.encode_utf16(); - let mut code = (lang.next().unwrap_or(0) & 0x1F) << 10; - code += (lang.next().unwrap_or(0) & 0x1F) << 5; - code += lang.next().unwrap_or(0) & 0x1F; - code -} - #[cfg(test)] mod tests { use super::*; - fn test_language_code(lang: &str) { - let code = language_code(lang); - let lang2 = language_string(code); - assert_eq!(lang, lang2); - } - - #[test] - fn test_language_codes() { - test_language_code("und"); - test_language_code("eng"); - test_language_code("kor"); - } - #[test] fn test_mdhd32() { let expected = Mdhd { diff --git a/src/moov/udta/cprt.rs b/src/moov/udta/cprt.rs new file mode 100644 index 0000000..9d46cdf --- /dev/null +++ b/src/moov/udta/cprt.rs @@ -0,0 +1,31 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Cprt { + pub language: String, + pub notice: String, +} + +impl AtomExt for Cprt { + type Ext = (); + + const KIND_EXT: FourCC = FourCC::new(b"cprt"); + + fn decode_body_ext(buf: &mut B, _ext: ()) -> Result { + let language_code = u16::decode(buf)?; + let language = language_string(language_code); + + Ok(Cprt { + language, + notice: String::decode(buf)?, + }) + } + + fn encode_body_ext(&self, buf: &mut B) -> Result<()> { + let language_code = language_code(&self.language); + (language_code).encode(buf)?; + self.notice.as_str().encode(buf)?; + Ok(()) + } +} diff --git a/src/moov/udta/kind.rs b/src/moov/udta/kind.rs new file mode 100644 index 0000000..fb1c65d --- /dev/null +++ b/src/moov/udta/kind.rs @@ -0,0 +1,27 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Kind { + pub scheme_uri: String, + pub value: String, +} + +impl AtomExt for Kind { + type Ext = (); + + const KIND_EXT: FourCC = FourCC::new(b"kind"); + + fn decode_body_ext(buf: &mut B, _ext: ()) -> Result { + Ok(Kind { + scheme_uri: String::decode(buf)?, + value: String::decode(buf)?, + }) + } + + fn encode_body_ext(&self, buf: &mut B) -> Result<()> { + self.scheme_uri.as_str().encode(buf)?; + self.value.as_str().encode(buf)?; + Ok(()) + } +} diff --git a/src/moov/udta/mod.rs b/src/moov/udta/mod.rs index b4caf99..65b3f0a 100644 --- a/src/moov/udta/mod.rs +++ b/src/moov/udta/mod.rs @@ -1,5 +1,9 @@ +mod cprt; +mod kind; mod skip; +pub use cprt::*; +pub use kind::*; pub use skip::*; use crate::*; @@ -7,6 +11,8 @@ use crate::*; #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Udta { + pub cprt: Option, + pub kind: Option, pub meta: Option, } @@ -15,7 +21,7 @@ impl Atom for Udta { nested! { required: [ ], - optional: [ Meta ], + optional: [ Cprt, Meta, Kind], multiple: [ ], } } @@ -26,7 +32,11 @@ mod tests { #[test] fn test_udta_empty() { - let expected = Udta { meta: None }; + let expected = Udta { + cprt: None, + meta: None, + kind: None, + }; let mut buf = Vec::new(); expected.encode(&mut buf).unwrap(); @@ -39,6 +49,10 @@ mod tests { #[test] fn test_udta() { let expected = Udta { + cprt: Some(Cprt { + language: "und".into(), + notice: "MIT or Apache".into(), + }), meta: Some(Meta { hdlr: Hdlr { handler: FourCC::new(b"fake"), @@ -46,6 +60,10 @@ mod tests { }, items: vec![], }), + kind: Some(Kind { + scheme_uri: "http://www.w3.org/TR/html5/".into(), + value: "".into(), + }), }; let mut buf = Vec::new(); @@ -55,4 +73,69 @@ mod tests { let output = Udta::decode(&mut buf).unwrap(); assert_eq!(output, expected); } + + // From MPEG File Format Conformance, isobmff/02_dref_edts_img.mp4 + const ENCODED_UDTA_WITH_CPRT: &[u8] = &[ + 0x00, 0x00, 0x00, 0x70, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x68, 0x63, 0x70, 0x72, + 0x74, 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x45, 0x4e, 0x53, 0x54, 0x20, 0x49, 0x73, 0x6f, + 0x4d, 0x65, 0x64, 0x69, 0x61, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x20, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x20, 0x2d, 0x20, 0x45, 0x4e, 0x53, 0x54, + 0x20, 0x28, 0x63, 0x29, 0x20, 0x32, 0x30, 0x30, 0x36, 0x20, 0x2d, 0x20, 0x52, 0x69, 0x67, + 0x68, 0x74, 0x73, 0x20, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x20, 0x66, 0x6f, + 0x72, 0x20, 0x49, 0x53, 0x4f, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x20, 0x75, 0x73, 0x65, 0x00, + ]; + + #[test] + fn test_udta_cprt() { + let mut buf = std::io::Cursor::new(ENCODED_UDTA_WITH_CPRT); + + let udta = Udta::decode(&mut buf).expect("failed to decode udta"); + + assert_eq!( + udta, + Udta { + cprt: Some(Cprt { language: "und".into(), notice: "ENST IsoMedia Conformance Files - ENST (c) 2006 - Rights released for ISO Conformance use".into() }), + meta: None, + kind: None, + } + ); + + let mut buf = Vec::new(); + udta.encode(&mut buf).unwrap(); + + assert_eq!(buf, ENCODED_UDTA_WITH_CPRT); + } + + // From MPEG File Format Conformance, nalu/hevc/hev1_clg1_header.mp4 + const ENCODED_UDTA_WITH_KIND: &[u8] = &[ + 0x00, 0x00, 0x00, 0x31, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x29, 0x6b, 0x69, 0x6e, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x75, 0x72, 0x6e, 0x3a, 0x6d, 0x70, 0x65, 0x67, 0x3a, 0x64, + 0x61, 0x73, 0x68, 0x3a, 0x72, 0x6f, 0x6c, 0x65, 0x3a, 0x32, 0x30, 0x31, 0x31, 0x00, 0x6d, + 0x61, 0x69, 0x6e, 0x00, + ]; + + #[test] + fn test_udta_kind() { + let mut buf = std::io::Cursor::new(ENCODED_UDTA_WITH_KIND); + + let udta = Udta::decode(&mut buf).expect("failed to decode udta"); + + assert_eq!( + udta, + Udta { + cprt: None, + meta: None, + kind: Some(Kind { + scheme_uri: "urn:mpeg:dash:role:2011".into(), + value: "main".into() + }) + } + ); + + let mut buf = Vec::new(); + udta.encode(&mut buf).unwrap(); + + assert_eq!(buf, ENCODED_UDTA_WITH_KIND); + } } diff --git a/src/test/av1.rs b/src/test/av1.rs index 801335b..11afbc1 100644 --- a/src/test/av1.rs +++ b/src/test/av1.rs @@ -189,6 +189,7 @@ fn av1() { } .into(),], }), + ..Default::default() }) } ); diff --git a/src/test/bbb.rs b/src/test/bbb.rs index c3dd9c4..f213b45 100644 --- a/src/test/bbb.rs +++ b/src/test/bbb.rs @@ -203,6 +203,7 @@ fn bbb() { hdlr: Hdlr{ handler: FourCC::new(b"mdir"), name: "".into() }, items: vec![Ilst { name: None, year: None, covr: None, desc: None, ctoo: Some(Tool { country_indicator: 0, language_indicator: 0, text: "Lavf61.1.100".into()}) }.into(),], }), + ..Default::default() }), ..Default::default() diff --git a/src/test/esds.rs b/src/test/esds.rs index 520d3de..cc2ff58 100644 --- a/src/test/esds.rs +++ b/src/test/esds.rs @@ -198,10 +198,7 @@ fn esds() { }, ..Default::default() }], - udta: Some(Udta { - meta: None, - }), - + udta: Some(Udta::default()), ..Default::default() }, ); diff --git a/src/test/h264.rs b/src/test/h264.rs index eba3778..44556bf 100644 --- a/src/test/h264.rs +++ b/src/test/h264.rs @@ -289,7 +289,7 @@ fn avcc_ext() { udta: None, }, ], - udta: Some(Udta { meta: None }), + udta: Some(Udta::default()), }; assert_eq!(moov, expected, "different decoded result"); diff --git a/src/test/hevc.rs b/src/test/hevc.rs index 9d2ee82..a248a24 100644 --- a/src/test/hevc.rs +++ b/src/test/hevc.rs @@ -406,6 +406,7 @@ fn hevc() { } .into(),], }), + ..Default::default() }) } ); diff --git a/src/test/uncompressed.rs b/src/test/uncompressed.rs index 1df6610..d4e243b 100644 --- a/src/test/uncompressed.rs +++ b/src/test/uncompressed.rs @@ -239,6 +239,7 @@ fn uncompressed() { } .into(),], }), + ..Default::default() }) } ); From 591f7d9710e306d7934fb163e02f22782fd3f869 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Thu, 15 Jan 2026 16:38:58 +1100 Subject: [PATCH 14/16] wip with cprt and kind boxes --- src/test/mpeg_file_format_conformance.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 02abf75..561f0bc 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -5,11 +5,7 @@ use crate::{Any, ReadFrom}; #[test] fn test_published() { let expected_fails: Vec = vec![ - "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_single_track.mp4".into(), - "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev2_single_track.mp4".into(), - "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev1_lhe1_multiple_tracks_implicit.mp4".into(), - "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_hvc2_multiple_tracks_extractors.mp4".into(), - "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hvc2_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/3gp/pdin_example.3gp".into(), "FileFormatConformance/data/file_features/published/3gp/female_amr67DTX_hinted.3gp".into(), "FileFormatConformance/data/file_features/published/3gp/female_amr67_hinted.3gp".into(), @@ -23,7 +19,6 @@ fn test_published() { "FileFormatConformance/data/file_features/published/isobmff/03_hinted.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/06_bifs.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/07_bifs_sprite.mp4".into(), - "FileFormatConformance/data/file_features/published/isobmff/09_text.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/timed-metadata.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/a5-foreman-AVC.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/a6_tone_multifile.mp4".into(), @@ -38,23 +33,14 @@ fn test_published() { "FileFormatConformance/data/file_features/published/isobmff/20_stxt.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/a4-tone-fragmented.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/21_segment.mp4".into(), - // "FileFormatConformance/data/file_features/published/isobmff/22_tx3g.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/sg-tl-st.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/restricted.mp4".into(), "FileFormatConformance/data/file_features/published/isobmff/17_negative_ctso.mp4".into(), "FileFormatConformance/data/file_features/published/green/meta_2500000bps_0.mp4m".into(), "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), - // "FileFormatConformance/data/file_features/published/heif/C001.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C026.heic".into(), "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C029.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C030.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C031.heic".into(), "FileFormatConformance/data/file_features/published/heif/C032.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C036.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C037.heic".into(), - // "FileFormatConformance/data/file_features/published/heif/C038.heic".into(), "FileFormatConformance/data/file_features/published/heif/C041.heic".into(), "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-1.iso3" .into(), @@ -89,7 +75,6 @@ fn test_published() { "FileFormatConformance/data/file_features/published/nalu/hevc/aggr_hvc1.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hvc1_hvc2_extractors.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hvc1_hvc2_implicit.mp4".into(), - "FileFormatConformance/data/file_features/published/nalu/hevc/hev1_clg1_header.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hev1_hev2_extractors.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_hev1_hev2_implicit.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/hevc/hevc_tiles_single_track_nalm.mp4".into(), @@ -115,6 +100,11 @@ fn test_published() { "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev1_hev2_multiple_tracks_extractors.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev1_hev2_multiple_tracks_extractors.mp4".into(), "FileFormatConformance/data/file_features/published/nalu/l-hevc/lhevc_avc1_lhv1.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hev2_single_track.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hev1_lhe1_multiple_tracks_implicit.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/shvc_hvc1_hvc2_multiple_tracks_extractors.mp4".into(), + "FileFormatConformance/data/file_features/published/nalu/l-hevc/mhvc_hvc2_single_track.mp4".into(), ]; From fb189241532605ed043bbd3577a3746c271ae483 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Fri, 16 Jan 2026 14:32:17 +1100 Subject: [PATCH 15/16] wip on over-decode for sgpd --- src/test/mpeg_file_format_conformance.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/mpeg_file_format_conformance.rs b/src/test/mpeg_file_format_conformance.rs index 561f0bc..67fee16 100644 --- a/src/test/mpeg_file_format_conformance.rs +++ b/src/test/mpeg_file_format_conformance.rs @@ -38,10 +38,7 @@ fn test_published() { "FileFormatConformance/data/file_features/published/isobmff/17_negative_ctso.mp4".into(), "FileFormatConformance/data/file_features/published/green/meta_2500000bps_0.mp4m".into(), "FileFormatConformance/data/file_features/published/green/video_2500000bps_0.mp4".into(), - "FileFormatConformance/data/file_features/published/heif/C027.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C028.heic".into(), "FileFormatConformance/data/file_features/published/heif/C032.heic".into(), - "FileFormatConformance/data/file_features/published/heif/C041.heic".into(), "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-1.iso3" .into(), "FileFormatConformance/data/file_features/published/isobmff/compact-no-code-fec-2.iso3" From c000b7ac3a3893d06fc845f17c08c15b7a4d2190 Mon Sep 17 00:00:00 2001 From: Brad Hards Date: Fri, 16 Jan 2026 14:40:53 +1100 Subject: [PATCH 16/16] sgpd: add support for the refs sample groups This is from ISO/IEC 23008-12:2025 Section 7.4. Its used in HEIF image sequences. This fixes the parsing of the several of the old HEIF samples in the MPEG File Format Conformance suite. Without this change, we get OverDecode errors for `sgpd`. --- src/moov/trak/mdia/minf/stbl/sgpd.rs | 72 +++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/moov/trak/mdia/minf/stbl/sgpd.rs b/src/moov/trak/mdia/minf/stbl/sgpd.rs index 194043d..04b0982 100644 --- a/src/moov/trak/mdia/minf/stbl/sgpd.rs +++ b/src/moov/trak/mdia/minf/stbl/sgpd.rs @@ -129,19 +129,49 @@ impl AtomExt for Sgpd { } } +const REFS_4CC: FourCC = FourCC::new(b"refs"); + #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum AnySampleGroupEntry { + DirectReferenceSampleList(u32, Vec), UnknownGroupingType(FourCC, Vec), } impl AnySampleGroupEntry { fn decode(grouping_type: FourCC, buf: &mut B) -> Result { - Ok(Self::UnknownGroupingType(grouping_type, Vec::decode(buf)?)) + match grouping_type { + REFS_4CC => { + let sample_id = u32::decode(buf)?; + let num_direct_reference_samples = u8::decode(buf)? as usize; + let mut direct_reference_samples = + Vec::with_capacity(std::cmp::min(num_direct_reference_samples, 16)); + for _ in 0..num_direct_reference_samples { + direct_reference_samples.push(u32::decode(buf)?); + } + Ok(Self::DirectReferenceSampleList( + sample_id, + direct_reference_samples, + )) + } + _ => Ok(Self::UnknownGroupingType(grouping_type, Vec::decode(buf)?)), + } } fn encode(&self, buf: &mut B) -> Result<()> { match self { + Self::DirectReferenceSampleList(sample_id, direct_reference_samples) => { + sample_id.encode(buf)?; + let num_direct_reference_samples: u8 = direct_reference_samples + .len() + .try_into() + .map_err(|_| Error::TooLarge(REFS_4CC))?; + num_direct_reference_samples.encode(buf)?; + for direct_reference_sample in direct_reference_samples { + direct_reference_sample.encode(buf)?; + } + Ok(()) + } Self::UnknownGroupingType(_, bytes) => bytes.encode(buf), } } @@ -206,4 +236,44 @@ mod tests { sgpd.encode(&mut buf).expect("encode should be successful"); assert_eq!(SIMPLE_SGPD, &buf); } + + // From the MPEG File Format Conformance suite, heif/C041.heic + const SGPD_ENCODED_C041: &[u8] = &[ + 0x00, 0x00, 0x00, 0x2e, 0x73, 0x67, 0x70, 0x64, 0x01, 0x00, 0x00, 0x00, 0x72, 0x65, 0x66, + 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, + 0x00, + ]; + + #[test] + fn sgpd_c041_decode() { + let mut buf = Cursor::new(SGPD_ENCODED_C041); + let sgpd = Sgpd::decode(&mut buf).expect("sgpd should decode successfully"); + assert_eq!( + sgpd, + Sgpd { + grouping_type: FourCC::from(b"refs"), + default_length: Some(0), + default_group_description_index: None, + static_group_description: false, + static_mapping: false, + essential: false, + entries: vec![ + SgpdEntry { + description_length: Some(9), + entry: AnySampleGroupEntry::DirectReferenceSampleList(0, vec![1]) + }, + SgpdEntry { + description_length: Some(5), + entry: AnySampleGroupEntry::DirectReferenceSampleList(1, vec![]) + } + ], + } + ); + + let mut encoded = Vec::new(); + sgpd.encode(&mut encoded).unwrap(); + + assert_eq!(encoded, SGPD_ENCODED_C041); + } }