From 0b7fc2b7cee1a8768620c3aea11a9aec99e25ef0 Mon Sep 17 00:00:00 2001 From: Alastor Wu Date: Mon, 20 Apr 2026 15:13:10 -0700 Subject: [PATCH 1/3] Parse mdcv and clli boxes in video sample entries Add support for MasteringDisplayColourVolumeBox ('mdcv') and ContentLightLevelBox ('clli') as defined in ISO/IEC 14496-12. Both boxes are codec-agnostic children of VisualSampleEntry and carry HDR10 static metadata (SMPTE ST 2086 mastering display + CTA-861.3 content light level). Parsed values are exposed through the C API via new boolean-flagged fields on Mp4parseTrackVideoSampleInfo, following the same pattern as the existing has_colour_info/colour_primaries fields. --- mp4parse/src/boxes.rs | 2 ++ mp4parse/src/lib.rs | 68 ++++++++++++++++++++++++++++++++++++++++ mp4parse/src/tests.rs | 46 +++++++++++++++++++++++++++ mp4parse_capi/src/lib.rs | 50 +++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+) diff --git a/mp4parse/src/boxes.rs b/mp4parse/src/boxes.rs index 7a9f22be..0fb687f3 100644 --- a/mp4parse/src/boxes.rs +++ b/mp4parse/src/boxes.rs @@ -123,6 +123,8 @@ box_database!( ItemPropertyContainerBox 0x6970_636f, // "ipco" ItemPropertyAssociationBox 0x6970_6d61, // "ipma" ColourInformationBox 0x636f_6c72, // "colr" + MasteringDisplayColourVolumeBox 0x6d646376, // "mdcv" + ContentLightLevelBox 0x636c6c69, // "clli" ImageSpatialExtentsProperty 0x6973_7065, // "ispe" PixelAspectRatioBox 0x7061_7370, // "pasp" PixelInformationBox 0x7069_7869, // "pixi" diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 7d4a292a..489bc7a6 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -1170,6 +1170,30 @@ pub enum VideoCodecSpecific { HEVCConfig(TryVec), } +/// Mastering display colour volume from an `mdcv` box (ISO 14496-12). +/// Primary indices are R\[0\], G\[1\], B\[2\]. Raw fixed-point values: divide chromaticity +/// values by 50000 and luminance values by 10000 to obtain physical units. +#[derive(Debug, Clone)] +pub struct MasteringDisplayColourVolume { + pub display_primaries_x: [u16; 3], + pub display_primaries_y: [u16; 3], + pub white_point_x: u16, + pub white_point_y: u16, + /// In units of 0.0001 cd/m² + pub max_display_mastering_luminance: u32, + /// In units of 0.0001 cd/m² + pub min_display_mastering_luminance: u32, +} + +/// Content light level from a `clli` box (ISO 14496-12). +#[derive(Debug, Clone)] +pub struct ContentLightLevel { + /// Maximum content light level in cd/m² + pub max_content_light_level: u16, + /// Maximum picture average light level in cd/m² + pub max_pic_average_light_level: u16, +} + #[derive(Debug)] pub struct VideoSampleEntry { pub codec_type: CodecType, @@ -1183,6 +1207,10 @@ pub struct VideoSampleEntry { /// Only `ColourInformation::Nclx` is currently surfaced through the C API; /// `ColourInformation::Icc` is stored but not exposed to C consumers. pub colour_info: Option, + /// Mastering display colour volume from the `mdcv` box (ISO 14496-12). + pub hdr_mastering_display: Option, + /// Content light level from the `clli` box (ISO 14496-12). + pub hdr_content_light_level: Option, } /// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each @@ -3701,6 +3729,32 @@ fn read_pasp(src: &mut BMFFBox) -> Result { }) } +/// Parse mastering display colour volume box (ISO 14496-12). +fn read_mdcv(src: &mut BMFFBox) -> Result { + let display_primaries_x = [be_u16(src)?, be_u16(src)?, be_u16(src)?]; + let display_primaries_y = [be_u16(src)?, be_u16(src)?, be_u16(src)?]; + let white_point_x = be_u16(src)?; + let white_point_y = be_u16(src)?; + let max_display_mastering_luminance = be_u32(src)?; + let min_display_mastering_luminance = be_u32(src)?; + Ok(MasteringDisplayColourVolume { + display_primaries_x, + display_primaries_y, + white_point_x, + white_point_y, + max_display_mastering_luminance, + min_display_mastering_luminance, + }) +} + +/// Parse content light level box (ISO 14496-12). +fn read_clli(src: &mut BMFFBox) -> Result { + Ok(ContentLightLevel { + max_content_light_level: be_u16(src)?, + max_pic_average_light_level: be_u16(src)?, + }) +} + #[derive(Debug)] pub struct PixelInformation { bits_per_channel: TryVec, @@ -5594,6 +5648,8 @@ fn read_video_sample_entry( let mut codec_specific = None; let mut pixel_aspect_ratio = None; let mut colour_info = None; + let mut hdr_mastering_display = None; + let mut hdr_content_light_level = None; let mut protection_info = TryVec::new(); let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { @@ -5726,6 +5782,16 @@ fn read_video_sample_entry( } } } + BoxType::MasteringDisplayColourVolumeBox => { + let mdcv = read_mdcv(&mut b)?; + debug!("Parsed mdcv box: {mdcv:?}"); + hdr_mastering_display = Some(mdcv); + } + BoxType::ContentLightLevelBox => { + let clli = read_clli(&mut b)?; + debug!("Parsed clli box: {clli:?}"); + hdr_content_light_level = Some(clli); + } _ => { debug!("Unsupported video codec, box {:?} found", b.head.name); skip_box_content(&mut b)?; @@ -5745,6 +5811,8 @@ fn read_video_sample_entry( protection_info, pixel_aspect_ratio, colour_info, + hdr_mastering_display, + hdr_content_light_level, }) }), ) diff --git a/mp4parse/src/tests.rs b/mp4parse/src/tests.rs index 52edf661..b745b787 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -1389,3 +1389,49 @@ fn read_to_end_oom() { let mut src = b"1234567890".take(isize::MAX.try_into().expect("isize < u64")); assert!(src.read_into_try_vec().is_err()); } + +#[test] +fn read_mdcv() { + // Synthetic mdcv box: 3 primaries (R, G, B) x/y as u16, white point x/y, + // max/min luminance as u32. Values chosen to be distinct and easy to verify. + let mut stream = make_box(BoxSize::Auto, b"mdcv", |s| { + s // display_primaries_x[0..2] (R, G, B) + .B16(35400) + .B16(14600) + .B16(7500) + // display_primaries_y[0..2] (R, G, B) + .B16(14600) + .B16(59210) + .B16(3000) + // white_point_x, white_point_y + .B16(15635) + .B16(16450) + // max_display_mastering_luminance, min_display_mastering_luminance + .B32(10000000) + .B32(50) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!( + stream.head.name, + super::BoxType::MasteringDisplayColourVolumeBox + ); + let mdcv = super::read_mdcv(&mut stream).unwrap(); + assert_eq!(mdcv.display_primaries_x, [35400, 14600, 7500]); + assert_eq!(mdcv.display_primaries_y, [14600, 59210, 3000]); + assert_eq!(mdcv.white_point_x, 15635); + assert_eq!(mdcv.white_point_y, 16450); + assert_eq!(mdcv.max_display_mastering_luminance, 10000000); + assert_eq!(mdcv.min_display_mastering_luminance, 50); +} + +#[test] +fn read_clli() { + let mut stream = make_box(BoxSize::Auto, b"clli", |s| s.B16(1000).B16(400)); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, super::BoxType::ContentLightLevelBox); + let clli = super::read_clli(&mut stream).unwrap(); + assert_eq!(clli.max_content_light_level, 1000); + assert_eq!(clli.max_pic_average_light_level, 400); +} diff --git a/mp4parse_capi/src/lib.rs b/mp4parse_capi/src/lib.rs index 1b51d0cf..af104a31 100644 --- a/mp4parse_capi/src/lib.rs +++ b/mp4parse_capi/src/lib.rs @@ -256,6 +256,32 @@ impl Default for Mp4parseTrackAudioInfo { } } +/// Mastering display colour volume from an `mdcv` box (ISO 14496-12). +/// Primary indices are R\[0\], G\[1\], B\[2\]. Divide chromaticity values by 50000.0 +/// and luminance values by 10000.0 to obtain physical units (chromaticity, cd/m²). +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseMasteringDisplayColourVolume { + pub display_primaries_x: [u16; 3], + pub display_primaries_y: [u16; 3], + pub white_point_x: u16, + pub white_point_y: u16, + /// In units of 0.0001 cd/m² + pub max_display_mastering_luminance: u32, + /// In units of 0.0001 cd/m² + pub min_display_mastering_luminance: u32, +} + +/// Content light level from a `clli` box (ISO 14496-12). +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseContentLightLevel { + /// Maximum content light level in cd/m² + pub max_content_light_level: u16, + /// Maximum picture average light level in cd/m² + pub max_pic_average_light_level: u16, +} + #[repr(C)] #[derive(Default, Debug)] pub struct Mp4parseTrackVideoSampleInfo { @@ -276,6 +302,12 @@ pub struct Mp4parseTrackVideoSampleInfo { pub matrix_coefficients: u8, /// Full range flag from the colr nclx box. Valid only when `has_colour_info`. pub full_range_flag: bool, + /// True when an `mdcv` box was present. When false, `mastering_display` must not be read. + pub has_mastering_display: bool, + pub mastering_display: Mp4parseMasteringDisplayColourVolume, + /// True when a `clli` box was present. When false, `content_light_level` must not be read. + pub has_content_light_level: bool, + pub content_light_level: Mp4parseContentLightLevel, } #[repr(C)] @@ -1139,6 +1171,24 @@ fn mp4parse_get_track_video_info_safe( sample_info.matrix_coefficients = nclx.matrix_coefficients; sample_info.full_range_flag = nclx.full_range_flag; } + if let Some(ref mdcv) = video.hdr_mastering_display { + sample_info.has_mastering_display = true; + sample_info.mastering_display = Mp4parseMasteringDisplayColourVolume { + display_primaries_x: mdcv.display_primaries_x, + display_primaries_y: mdcv.display_primaries_y, + white_point_x: mdcv.white_point_x, + white_point_y: mdcv.white_point_y, + max_display_mastering_luminance: mdcv.max_display_mastering_luminance, + min_display_mastering_luminance: mdcv.min_display_mastering_luminance, + }; + } + if let Some(ref clli) = video.hdr_content_light_level { + sample_info.has_content_light_level = true; + sample_info.content_light_level = Mp4parseContentLightLevel { + max_content_light_level: clli.max_content_light_level, + max_pic_average_light_level: clli.max_pic_average_light_level, + }; + } video_sample_infos.push(sample_info)?; } From 27cb26f76457b0a72ee367148291d004c40c5ba4 Mon Sep 17 00:00:00 2001 From: Alastor Wu Date: Tue, 21 Apr 2026 11:11:54 -0700 Subject: [PATCH 2/3] Fix pre-existing clippy failures --- mp4parse/src/lib.rs | 11 +++++------ mp4parse/src/unstable.rs | 10 ++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 489bc7a6..a67c0a4e 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -5774,12 +5774,11 @@ fn read_video_sample_entry( Status::ColrBadQuantityBMFF, )?; skip_box_content(&mut b)?; - } else { - if let ParsedColourInformation::Supported(colr) = read_colr(&mut b, strictness)? - { - debug!("Parsed colr box: {colr:?}"); - colour_info = Some(colr); - } + } else if let ParsedColourInformation::Supported(colr) = + read_colr(&mut b, strictness)? + { + debug!("Parsed colr box: {colr:?}"); + colour_info = Some(colr); } } BoxType::MasteringDisplayColourVolumeBox => { diff --git a/mp4parse/src/unstable.rs b/mp4parse/src/unstable.rs index 2dfc23c0..a1b12a5d 100644 --- a/mp4parse/src/unstable.rs +++ b/mp4parse/src/unstable.rs @@ -239,14 +239,8 @@ pub fn create_sample_table( let start_decode = decode_time; - let start_composition_val: i64 = match start_composition { - Some(sc) => sc.0, - None => return None, - }; - let end_composition_val: i64 = match end_composition { - Some(ec) => ec.0, - None => return None, - }; + let start_composition_val: i64 = start_composition?.0; + let end_composition_val: i64 = end_composition?.0; let track_offset: i64 = track_offset_time.0; From 5fe5578d1ea27a8965e777434280c1559b757a9c Mon Sep 17 00:00:00 2001 From: Alastor Wu Date: Tue, 21 Apr 2026 11:12:26 -0700 Subject: [PATCH 3/3] Address review feedback on mdcv/clli parsing Fix hex literal formatting in BoxType constants to use '_' separators, and fix read_mdcv to correctly remap wire order G, B, R to struct indices R[0], G[1], B[2] per ISO 14496-12. --- mp4parse/src/boxes.rs | 4 ++-- mp4parse/src/lib.rs | 12 ++++++++---- mp4parse/src/tests.rs | 19 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/mp4parse/src/boxes.rs b/mp4parse/src/boxes.rs index 0fb687f3..40becc69 100644 --- a/mp4parse/src/boxes.rs +++ b/mp4parse/src/boxes.rs @@ -123,8 +123,8 @@ box_database!( ItemPropertyContainerBox 0x6970_636f, // "ipco" ItemPropertyAssociationBox 0x6970_6d61, // "ipma" ColourInformationBox 0x636f_6c72, // "colr" - MasteringDisplayColourVolumeBox 0x6d646376, // "mdcv" - ContentLightLevelBox 0x636c6c69, // "clli" + MasteringDisplayColourVolumeBox 0x6d64_6376, // "mdcv" + ContentLightLevelBox 0x636c_6c69, // "clli" ImageSpatialExtentsProperty 0x6973_7065, // "ispe" PixelAspectRatioBox 0x7061_7370, // "pasp" PixelInformationBox 0x7069_7869, // "pixi" diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index a67c0a4e..955e902f 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -1171,8 +1171,8 @@ pub enum VideoCodecSpecific { } /// Mastering display colour volume from an `mdcv` box (ISO 14496-12). -/// Primary indices are R\[0\], G\[1\], B\[2\]. Raw fixed-point values: divide chromaticity -/// values by 50000 and luminance values by 10000 to obtain physical units. +/// Primary indices are R\[0\], G\[1\], B\[2\]. Divide chromaticity values by 50000 +/// and luminance values by 10000 to obtain physical units. #[derive(Debug, Clone)] pub struct MasteringDisplayColourVolume { pub display_primaries_x: [u16; 3], @@ -3731,8 +3731,12 @@ fn read_pasp(src: &mut BMFFBox) -> Result { /// Parse mastering display colour volume box (ISO 14496-12). fn read_mdcv(src: &mut BMFFBox) -> Result { - let display_primaries_x = [be_u16(src)?, be_u16(src)?, be_u16(src)?]; - let display_primaries_y = [be_u16(src)?, be_u16(src)?, be_u16(src)?]; + // Wire order is G, B, R (per ISO 14496-12); remap to R[0], G[1], B[2]. + let (gx, gy) = (be_u16(src)?, be_u16(src)?); + let (bx, by) = (be_u16(src)?, be_u16(src)?); + let (rx, ry) = (be_u16(src)?, be_u16(src)?); + let display_primaries_x = [rx, gx, bx]; + let display_primaries_y = [ry, gy, by]; let white_point_x = be_u16(src)?; let white_point_y = be_u16(src)?; let max_display_mastering_luminance = be_u32(src)?; diff --git a/mp4parse/src/tests.rs b/mp4parse/src/tests.rs index b745b787..087ba956 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -1392,17 +1392,16 @@ fn read_to_end_oom() { #[test] fn read_mdcv() { - // Synthetic mdcv box: 3 primaries (R, G, B) x/y as u16, white point x/y, - // max/min luminance as u32. Values chosen to be distinct and easy to verify. + // Synthetic mdcv box. Wire order is G, B, R (x then y for each primary), + // remapped by read_mdcv to struct indices R[0], G[1], B[2]. let mut stream = make_box(BoxSize::Auto, b"mdcv", |s| { - s // display_primaries_x[0..2] (R, G, B) - .B16(35400) - .B16(14600) - .B16(7500) - // display_primaries_y[0..2] (R, G, B) - .B16(14600) - .B16(59210) - .B16(3000) + s // Gx, Gy, Bx, By, Rx, Ry (wire order) + .B16(14600) // Gx + .B16(59210) // Gy + .B16(7500) // Bx + .B16(3000) // By + .B16(35400) // Rx + .B16(14600) // Ry // white_point_x, white_point_y .B16(15635) .B16(16450)