diff --git a/mp4parse/src/boxes.rs b/mp4parse/src/boxes.rs index 7a9f22be..40becc69 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 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 7d4a292a..955e902f 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\]. 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,36 @@ fn read_pasp(src: &mut BMFFBox) -> Result { }) } +/// Parse mastering display colour volume box (ISO 14496-12). +fn read_mdcv(src: &mut BMFFBox) -> Result { + // 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)?; + 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 +5652,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()? { @@ -5718,14 +5778,23 @@ 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 => { + 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 +5814,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..087ba956 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -1389,3 +1389,48 @@ 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. 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 // 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) + // 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/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; 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)?; }