Skip to content

Commit c049244

Browse files
committed
Make duplicate colr detection per colour_type in video sample entries
Follow-up to #450. ISOBMFF permits multiple colr boxes in a VisualSampleEntry, at most one for a given colour_type, so a valid nclx + ICC pairing should not trigger ColrBadQuantityBMFF. Track the colour types seen and flag only a repeated colour_type, matching the HEIF item-property handling of ColrBadQuantity. Also reword the ColrBadQuantityBMFF description, which still claimed a sample entry shall have at most one colr box, contradicting the spec text cited at the check site, and add regression tests pinning the Strict-only rejection of repeated colour types.
1 parent ac4ed53 commit c049244

4 files changed

Lines changed: 98 additions & 22 deletions

File tree

mp4parse/src/lib.rs

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,8 @@ impl From<Status> for &str {
467467
per HEIF (ISO/IEC DIS 23008-12) § 6.5.5.1"
468468
}
469469
Status::ColrBadQuantityBMFF => {
470-
"Each sample entry shall have at most one ColourInformationBox (colr) \
471-
per ISOBMFF (ISO 14496-12:2020) § 12.1.5"
470+
"Each sample entry should have at most one ColourInformationBox (colr) \
471+
for a given value of colour_type per ISOBMFF (ISO 14496-12:2020) § 12.1.5"
472472
}
473473
Status::ColrBadSize => {
474474
"Unexpected size for colr box"
@@ -3636,13 +3636,16 @@ fn read_ipco<T: Read>(
36363636
let property = match b.head.name {
36373637
BoxType::AuxiliaryTypeProperty => ItemProperty::AuxiliaryType(read_auxc(&mut b)?),
36383638
BoxType::AV1CodecConfigurationBox => ItemProperty::AV1Config(read_av1c(&mut b)?),
3639-
BoxType::ColourInformationBox => match read_colr(&mut b, strictness)? {
3640-
ParsedColourInformation::Supported(colr) => ItemProperty::Colour(colr),
3641-
ParsedColourInformation::Unsupported(colour_type) => {
3642-
error!("read_colr colour_type: {colour_type:?}");
3643-
return Status::ColrBadType.into();
3639+
BoxType::ColourInformationBox => {
3640+
let colour_type = be_u32(&mut b)?.to_be_bytes();
3641+
match read_colr(&mut b, colour_type, strictness)? {
3642+
ParsedColourInformation::Supported(colr) => ItemProperty::Colour(colr),
3643+
ParsedColourInformation::Unsupported(colour_type) => {
3644+
error!("read_colr colour_type: {colour_type:?}");
3645+
return Status::ColrBadType.into();
3646+
}
36443647
}
3645-
},
3648+
}
36463649
BoxType::ImageMirror => ItemProperty::Mirroring(read_imir(&mut b)?),
36473650
BoxType::ImageRotation => ItemProperty::Rotation(read_irot(&mut b)?),
36483651
BoxType::ImageSpatialExtentsProperty => {
@@ -3837,12 +3840,12 @@ enum ParsedColourInformation {
38373840

38383841
/// Parse colour information
38393842
/// See ISOBMFF (ISO 14496-12:2020) § 12.1.5
3843+
/// The caller is expected to have already read the colour_type field.
38403844
fn read_colr<T: Read>(
38413845
src: &mut BMFFBox<T>,
3846+
colour_type: [u8; 4],
38423847
strictness: ParseStrictness,
38433848
) -> Result<ParsedColourInformation> {
3844-
let colour_type = be_u32(src)?.to_be_bytes();
3845-
38463849
match &colour_type {
38473850
b"nclx" => {
38483851
const NUM_RESERVED_BITS: u8 = 7;
@@ -5683,6 +5686,7 @@ fn read_video_sample_entry<T: Read>(
56835686
let mut codec_specific = None;
56845687
let mut pixel_aspect_ratio = None;
56855688
let mut colour_info = None;
5689+
let mut colr_types_seen = TryVec::<FourCC>::new();
56865690
let mut hdr_mastering_display = None;
56875691
let mut hdr_content_light_level = None;
56885692
let mut protection_info = TryVec::new();
@@ -5802,22 +5806,32 @@ fn read_video_sample_entry<T: Read>(
58025806
debug!("Parsed pasp box: {pasp:?}, PAR {pixel_aspect_ratio:?}");
58035807
}
58045808
BoxType::ColourInformationBox => {
5805-
if colour_info.is_some() {
5806-
// ISO/IEC 14496-12:2015 § 12.1.5.1 permits one or more colr boxes
5807-
// in a VisualSampleEntry and assigns them no normative behaviour;
5808-
// a reader may keep the first (most accurate) and ignore the rest.
5809-
// Only reject under Strict.
5810-
warn!("Multiple colr boxes in video sample entry, keeping first");
5809+
// ISO/IEC 14496-12:2015 § 12.1.5.1 permits one or more colr boxes
5810+
// in a VisualSampleEntry (at most one for a given colour_type) and
5811+
// assigns them no normative behaviour; a reader may keep the first
5812+
// (most accurate) and ignore the rest. Only reject a repeated
5813+
// colour_type, and only under Strict.
5814+
let colour_type = FourCC::from(be_u32(&mut b)?.to_be_bytes());
5815+
if colr_types_seen.contains(&colour_type) {
5816+
warn!(
5817+
"Multiple {colour_type:?} colr boxes in video sample entry, keeping first"
5818+
);
58115819
fail_with_status_if(
58125820
strictness == ParseStrictness::Strict,
58135821
Status::ColrBadQuantityBMFF,
58145822
)?;
5815-
skip_box_content(&mut b)?;
5816-
} else if let ParsedColourInformation::Supported(colr) =
5817-
read_colr(&mut b, strictness)?
5818-
{
5819-
debug!("Parsed colr box: {colr:?}");
5820-
colour_info = Some(colr);
5823+
skip_box_remain(&mut b)?;
5824+
} else {
5825+
colr_types_seen.push(colour_type.clone())?;
5826+
if colour_info.is_some() {
5827+
debug!("Already have colour info, skipping {colour_type:?} colr box");
5828+
skip_box_remain(&mut b)?;
5829+
} else if let ParsedColourInformation::Supported(colr) =
5830+
read_colr(&mut b, colour_type.value, strictness)?
5831+
{
5832+
debug!("Parsed colr box: {colr:?}");
5833+
colour_info = Some(colr);
5834+
}
58215835
}
58225836
}
58235837
BoxType::MasteringDisplayColourVolumeBox => {

mp4parse/tests/public.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,15 @@ static VIDEO_MP4V_MP4: &str = "tests/bbb_sunflower_QCIF_30fps_mp4v_noaudio_1f.mp
237237
// ffmpeg -f lavfi -i color=c=white:s=640x480 -c:v libx264 -frames:v 1 -pix_fmt yuv420p -vf "setsar=16/9" h264_white_frame_sar_16_9.mp4
238238
static VIDEO_H264_PASP_MP4: &str = "tests/h264_white_frame_sar_16_9.mp4";
239239

240+
// An h264 video sample entry with two nclx colr boxes (HLG transfer=18 followed
241+
// by a BT.2020 transfer=14 fallback), the backward-compatible HLG signaling
242+
// convention. Copy of mp4parse_capi/tests/video_colr_nclx_two_colr.mp4.
243+
static VIDEO_TWO_NCLX_COLR_MP4: &str = "tests/video_colr_nclx_two_colr.mp4";
244+
// As above, but with the second colr box's colour_type patched from nclx to
245+
// rICC, giving one colr box of each colour_type as intended by
246+
// ISOBMFF (ISO 14496-12:2020) § 12.1.5.
247+
static VIDEO_NCLX_AND_ICC_COLR_MP4: &str = "tests/video_colr_nclx_and_icc.mp4";
248+
240249
// Adapted from https://github.com/GuillaumeGomez/audio-video-metadata/blob/9dff40f565af71d5502e03a2e78ae63df95cfd40/src/metadata.rs#L53
241250
#[test]
242251
fn public_api() {
@@ -890,6 +899,59 @@ fn public_mp4_ctts_overflow() {
890899
);
891900
}
892901

902+
fn assert_video_nclx_colour_info(
903+
result: mp4::Result<mp4::MediaContext>,
904+
expected_transfer_characteristics: u8,
905+
) {
906+
let context = result.expect("read_mp4 failed");
907+
let track = context.tracks.first().expect("expected a track");
908+
let stsd = track.stsd.as_ref().expect("expected an stsd");
909+
let v = match stsd.descriptions.first().expect("expected a SampleEntry") {
910+
mp4::SampleEntry::Video(v) => v,
911+
_ => panic!("expected a VideoSampleEntry"),
912+
};
913+
match &v.colour_info {
914+
Some(mp4::ColourInformation::Nclx(nclx)) => {
915+
assert_eq!(
916+
nclx.transfer_characteristics,
917+
expected_transfer_characteristics
918+
);
919+
}
920+
other => panic!("expected nclx colour info, got {:?}", other),
921+
}
922+
}
923+
924+
/// Two colr boxes with the same colour_type in a video sample entry are
925+
/// accepted (keeping the first) except under Strict parsing, which yields
926+
/// ColrBadQuantityBMFF; see ISOBMFF (ISO 14496-12:2020) § 12.1.5.
927+
#[test]
928+
fn public_video_two_nclx_colr_boxes() {
929+
for strictness in [ParseStrictness::Permissive, ParseStrictness::Normal] {
930+
let input = &mut File::open(VIDEO_TWO_NCLX_COLR_MP4).expect("Unknown file");
931+
assert_video_nclx_colour_info(mp4::read_mp4(input, strictness), 18);
932+
}
933+
let input = &mut File::open(VIDEO_TWO_NCLX_COLR_MP4).expect("Unknown file");
934+
assert_eq!(
935+
Status::from(mp4::read_mp4(input, ParseStrictness::Strict)),
936+
Status::ColrBadQuantityBMFF
937+
);
938+
}
939+
940+
/// One colr box of each colour_type (nclx and rICC) in a video sample entry
941+
/// is valid per ISOBMFF (ISO 14496-12:2020) § 12.1.5 and accepted at all
942+
/// strictness levels, surfacing the first box.
943+
#[test]
944+
fn public_video_nclx_and_icc_colr_boxes() {
945+
for strictness in [
946+
ParseStrictness::Permissive,
947+
ParseStrictness::Normal,
948+
ParseStrictness::Strict,
949+
] {
950+
let input = &mut File::open(VIDEO_NCLX_AND_ICC_COLR_MP4).expect("Unknown file");
951+
assert_video_nclx_colour_info(mp4::read_mp4(input, strictness), 18);
952+
}
953+
}
954+
893955
#[test]
894956
fn public_avif_primary_item() {
895957
let input = &mut File::open(IMAGE_AVIF).expect("Unknown file");
1.65 KB
Binary file not shown.
1.65 KB
Binary file not shown.

0 commit comments

Comments
 (0)