Skip to content

Commit 12f7ef3

Browse files
committed
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.
1 parent 952f517 commit 12f7ef3

4 files changed

Lines changed: 166 additions & 0 deletions

File tree

mp4parse/src/boxes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ box_database!(
123123
ItemPropertyContainerBox 0x6970_636f, // "ipco"
124124
ItemPropertyAssociationBox 0x6970_6d61, // "ipma"
125125
ColourInformationBox 0x636f_6c72, // "colr"
126+
MasteringDisplayColourVolumeBox 0x6d646376, // "mdcv"
127+
ContentLightLevelBox 0x636c6c69, // "clli"
126128
ImageSpatialExtentsProperty 0x6973_7065, // "ispe"
127129
PixelAspectRatioBox 0x7061_7370, // "pasp"
128130
PixelInformationBox 0x7069_7869, // "pixi"

mp4parse/src/lib.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,30 @@ pub enum VideoCodecSpecific {
11701170
HEVCConfig(TryVec<u8>),
11711171
}
11721172

1173+
/// Mastering display colour volume from an `mdcv` box (ISO 14496-12).
1174+
/// Primary indices are R[0], G[1], B[2]. Raw fixed-point values: divide chromaticity
1175+
/// values by 50000 and luminance values by 10000 to obtain physical units.
1176+
#[derive(Debug, Clone)]
1177+
pub struct MasteringDisplayColourVolume {
1178+
pub display_primaries_x: [u16; 3],
1179+
pub display_primaries_y: [u16; 3],
1180+
pub white_point_x: u16,
1181+
pub white_point_y: u16,
1182+
/// In units of 0.0001 cd/m²
1183+
pub max_display_mastering_luminance: u32,
1184+
/// In units of 0.0001 cd/m²
1185+
pub min_display_mastering_luminance: u32,
1186+
}
1187+
1188+
/// Content light level from a `clli` box (ISO 14496-12).
1189+
#[derive(Debug, Clone)]
1190+
pub struct ContentLightLevel {
1191+
/// Maximum content light level in cd/m²
1192+
pub max_content_light_level: u16,
1193+
/// Maximum picture average light level in cd/m²
1194+
pub max_pic_average_light_level: u16,
1195+
}
1196+
11731197
#[derive(Debug)]
11741198
pub struct VideoSampleEntry {
11751199
pub codec_type: CodecType,
@@ -1183,6 +1207,10 @@ pub struct VideoSampleEntry {
11831207
/// Only `ColourInformation::Nclx` is currently surfaced through the C API;
11841208
/// `ColourInformation::Icc` is stored but not exposed to C consumers.
11851209
pub colour_info: Option<ColourInformation>,
1210+
/// Mastering display colour volume from the `mdcv` box (ISO 14496-12).
1211+
pub hdr_mastering_display: Option<MasteringDisplayColourVolume>,
1212+
/// Content light level from the `clli` box (ISO 14496-12).
1213+
pub hdr_content_light_level: Option<ContentLightLevel>,
11861214
}
11871215

11881216
/// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each
@@ -3701,6 +3729,32 @@ fn read_pasp<T: Read>(src: &mut BMFFBox<T>) -> Result<PixelAspectRatio> {
37013729
})
37023730
}
37033731

3732+
/// Parse mastering display colour volume box (ISO 14496-12).
3733+
fn read_mdcv<T: Read>(src: &mut BMFFBox<T>) -> Result<MasteringDisplayColourVolume> {
3734+
let display_primaries_x = [be_u16(src)?, be_u16(src)?, be_u16(src)?];
3735+
let display_primaries_y = [be_u16(src)?, be_u16(src)?, be_u16(src)?];
3736+
let white_point_x = be_u16(src)?;
3737+
let white_point_y = be_u16(src)?;
3738+
let max_display_mastering_luminance = be_u32(src)?;
3739+
let min_display_mastering_luminance = be_u32(src)?;
3740+
Ok(MasteringDisplayColourVolume {
3741+
display_primaries_x,
3742+
display_primaries_y,
3743+
white_point_x,
3744+
white_point_y,
3745+
max_display_mastering_luminance,
3746+
min_display_mastering_luminance,
3747+
})
3748+
}
3749+
3750+
/// Parse content light level box (ISO 14496-12).
3751+
fn read_clli<T: Read>(src: &mut BMFFBox<T>) -> Result<ContentLightLevel> {
3752+
Ok(ContentLightLevel {
3753+
max_content_light_level: be_u16(src)?,
3754+
max_pic_average_light_level: be_u16(src)?,
3755+
})
3756+
}
3757+
37043758
#[derive(Debug)]
37053759
pub struct PixelInformation {
37063760
bits_per_channel: TryVec<u8>,
@@ -5594,6 +5648,8 @@ fn read_video_sample_entry<T: Read>(
55945648
let mut codec_specific = None;
55955649
let mut pixel_aspect_ratio = None;
55965650
let mut colour_info = None;
5651+
let mut hdr_mastering_display = None;
5652+
let mut hdr_content_light_level = None;
55975653
let mut protection_info = TryVec::new();
55985654
let mut iter = src.box_iter();
55995655
while let Some(mut b) = iter.next_box()? {
@@ -5726,6 +5782,16 @@ fn read_video_sample_entry<T: Read>(
57265782
}
57275783
}
57285784
}
5785+
BoxType::MasteringDisplayColourVolumeBox => {
5786+
let mdcv = read_mdcv(&mut b)?;
5787+
debug!("Parsed mdcv box: {mdcv:?}");
5788+
hdr_mastering_display = Some(mdcv);
5789+
}
5790+
BoxType::ContentLightLevelBox => {
5791+
let clli = read_clli(&mut b)?;
5792+
debug!("Parsed clli box: {clli:?}");
5793+
hdr_content_light_level = Some(clli);
5794+
}
57295795
_ => {
57305796
debug!("Unsupported video codec, box {:?} found", b.head.name);
57315797
skip_box_content(&mut b)?;
@@ -5745,6 +5811,8 @@ fn read_video_sample_entry<T: Read>(
57455811
protection_info,
57465812
pixel_aspect_ratio,
57475813
colour_info,
5814+
hdr_mastering_display,
5815+
hdr_content_light_level,
57485816
})
57495817
}),
57505818
)

mp4parse/src/tests.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,49 @@ fn read_to_end_oom() {
13891389
let mut src = b"1234567890".take(isize::MAX.try_into().expect("isize < u64"));
13901390
assert!(src.read_into_try_vec().is_err());
13911391
}
1392+
1393+
#[test]
1394+
fn read_mdcv() {
1395+
// Synthetic mdcv box: 3 primaries (R, G, B) x/y as u16, white point x/y,
1396+
// max/min luminance as u32. Values chosen to be distinct and easy to verify.
1397+
let mut stream = make_box(BoxSize::Auto, b"mdcv", |s| {
1398+
s // display_primaries_x[0..2] (R, G, B)
1399+
.B16(35400)
1400+
.B16(14600)
1401+
.B16(7500)
1402+
// display_primaries_y[0..2] (R, G, B)
1403+
.B16(14600)
1404+
.B16(59210)
1405+
.B16(3000)
1406+
// white_point_x, white_point_y
1407+
.B16(15635)
1408+
.B16(16450)
1409+
// max_display_mastering_luminance, min_display_mastering_luminance
1410+
.B32(10000000)
1411+
.B32(50)
1412+
});
1413+
let mut iter = super::BoxIter::new(&mut stream);
1414+
let mut stream = iter.next_box().unwrap().unwrap();
1415+
assert_eq!(
1416+
stream.head.name,
1417+
super::BoxType::MasteringDisplayColourVolumeBox
1418+
);
1419+
let mdcv = super::read_mdcv(&mut stream).unwrap();
1420+
assert_eq!(mdcv.display_primaries_x, [35400, 14600, 7500]);
1421+
assert_eq!(mdcv.display_primaries_y, [14600, 59210, 3000]);
1422+
assert_eq!(mdcv.white_point_x, 15635);
1423+
assert_eq!(mdcv.white_point_y, 16450);
1424+
assert_eq!(mdcv.max_display_mastering_luminance, 10000000);
1425+
assert_eq!(mdcv.min_display_mastering_luminance, 50);
1426+
}
1427+
1428+
#[test]
1429+
fn read_clli() {
1430+
let mut stream = make_box(BoxSize::Auto, b"clli", |s| s.B16(1000).B16(400));
1431+
let mut iter = super::BoxIter::new(&mut stream);
1432+
let mut stream = iter.next_box().unwrap().unwrap();
1433+
assert_eq!(stream.head.name, super::BoxType::ContentLightLevelBox);
1434+
let clli = super::read_clli(&mut stream).unwrap();
1435+
assert_eq!(clli.max_content_light_level, 1000);
1436+
assert_eq!(clli.max_pic_average_light_level, 400);
1437+
}

mp4parse_capi/src/lib.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,32 @@ impl Default for Mp4parseTrackAudioInfo {
256256
}
257257
}
258258

259+
/// Mastering display colour volume from an `mdcv` box (ISO 14496-12).
260+
/// Primary indices are R[0], G[1], B[2]. Divide chromaticity values by 50000.0
261+
/// and luminance values by 10000.0 to obtain physical units (chromaticity, cd/m²).
262+
#[repr(C)]
263+
#[derive(Default, Debug)]
264+
pub struct Mp4parseMasteringDisplayColourVolume {
265+
pub display_primaries_x: [u16; 3],
266+
pub display_primaries_y: [u16; 3],
267+
pub white_point_x: u16,
268+
pub white_point_y: u16,
269+
/// In units of 0.0001 cd/m²
270+
pub max_display_mastering_luminance: u32,
271+
/// In units of 0.0001 cd/m²
272+
pub min_display_mastering_luminance: u32,
273+
}
274+
275+
/// Content light level from a `clli` box (ISO 14496-12).
276+
#[repr(C)]
277+
#[derive(Default, Debug)]
278+
pub struct Mp4parseContentLightLevel {
279+
/// Maximum content light level in cd/m²
280+
pub max_content_light_level: u16,
281+
/// Maximum picture average light level in cd/m²
282+
pub max_pic_average_light_level: u16,
283+
}
284+
259285
#[repr(C)]
260286
#[derive(Default, Debug)]
261287
pub struct Mp4parseTrackVideoSampleInfo {
@@ -276,6 +302,12 @@ pub struct Mp4parseTrackVideoSampleInfo {
276302
pub matrix_coefficients: u8,
277303
/// Full range flag from the colr nclx box. Valid only when `has_colour_info`.
278304
pub full_range_flag: bool,
305+
/// True when an `mdcv` box was present. When false, `mastering_display` must not be read.
306+
pub has_mastering_display: bool,
307+
pub mastering_display: Mp4parseMasteringDisplayColourVolume,
308+
/// True when a `clli` box was present. When false, `content_light_level` must not be read.
309+
pub has_content_light_level: bool,
310+
pub content_light_level: Mp4parseContentLightLevel,
279311
}
280312

281313
#[repr(C)]
@@ -1139,6 +1171,24 @@ fn mp4parse_get_track_video_info_safe(
11391171
sample_info.matrix_coefficients = nclx.matrix_coefficients;
11401172
sample_info.full_range_flag = nclx.full_range_flag;
11411173
}
1174+
if let Some(ref mdcv) = video.hdr_mastering_display {
1175+
sample_info.has_mastering_display = true;
1176+
sample_info.mastering_display = Mp4parseMasteringDisplayColourVolume {
1177+
display_primaries_x: mdcv.display_primaries_x,
1178+
display_primaries_y: mdcv.display_primaries_y,
1179+
white_point_x: mdcv.white_point_x,
1180+
white_point_y: mdcv.white_point_y,
1181+
max_display_mastering_luminance: mdcv.max_display_mastering_luminance,
1182+
min_display_mastering_luminance: mdcv.min_display_mastering_luminance,
1183+
};
1184+
}
1185+
if let Some(ref clli) = video.hdr_content_light_level {
1186+
sample_info.has_content_light_level = true;
1187+
sample_info.content_light_level = Mp4parseContentLightLevel {
1188+
max_content_light_level: clli.max_content_light_level,
1189+
max_pic_average_light_level: clli.max_pic_average_light_level,
1190+
};
1191+
}
11421192

11431193
video_sample_infos.push(sample_info)?;
11441194
}

0 commit comments

Comments
 (0)