Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 64 additions & 20 deletions mp4parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ pub enum Status {
BoxBadWideSize,
CheckParserStateErr,
ColrBadQuantity,
ColrBadQuantityBMFF,
ColrBadSize,
ColrBadType,
ColrReservedNonzero,
Expand Down Expand Up @@ -465,6 +466,10 @@ impl From<Status> for &str {
ColourInformationBox (colr) for a given value of colour_type \
per HEIF (ISO/IEC DIS 23008-12) § 6.5.5.1"
}
Status::ColrBadQuantityBMFF => {
"Each sample entry shall have at most one ColourInformationBox (colr) \
per ISOBMFF (ISO 14496-12:2020) § 12.1.5"
}
Status::ColrBadSize => {
"Unexpected size for colr box"
}
Expand Down Expand Up @@ -1175,6 +1180,9 @@ pub struct VideoSampleEntry {
pub codec_specific: VideoCodecSpecific,
pub protection_info: TryVec<ProtectionSchemeInfoBox>,
pub pixel_aspect_ratio: Option<f32>,
/// Only `ColourInformation::Nclx` is currently surfaced through the C API;
/// `ColourInformation::Icc` is stored but not exposed to C consumers.
pub colour_info: Option<ColourInformation>,
}

/// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each
Expand Down Expand Up @@ -3600,7 +3608,13 @@ fn read_ipco<T: Read>(
let property = match b.head.name {
BoxType::AuxiliaryTypeProperty => ItemProperty::AuxiliaryType(read_auxc(&mut b)?),
BoxType::AV1CodecConfigurationBox => ItemProperty::AV1Config(read_av1c(&mut b)?),
BoxType::ColourInformationBox => ItemProperty::Colour(read_colr(&mut b, strictness)?),
BoxType::ColourInformationBox => match read_colr(&mut b, strictness)? {
ParsedColourInformation::Supported(colr) => ItemProperty::Colour(colr),
ParsedColourInformation::Unsupported(colour_type) => {
error!("read_colr colour_type: {colour_type:?}");
return Status::ColrBadType.into();
}
},
BoxType::ImageMirror => ItemProperty::Mirroring(read_imir(&mut b)?),
BoxType::ImageRotation => ItemProperty::Rotation(read_irot(&mut b)?),
BoxType::ImageSpatialExtentsProperty => {
Expand Down Expand Up @@ -3722,10 +3736,10 @@ fn read_pixi<T: Read>(src: &mut BMFFBox<T>) -> Result<PixelInformation> {
#[repr(C)]
#[derive(Debug)]
pub struct NclxColourInformation {
colour_primaries: u8,
transfer_characteristics: u8,
matrix_coefficients: u8,
full_range_flag: bool,
pub colour_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
pub full_range_flag: bool,
}

/// The raw bytes of the ICC profile
Expand Down Expand Up @@ -3758,12 +3772,17 @@ impl ColourInformation {
}
}

enum ParsedColourInformation {
Supported(ColourInformation),
Unsupported(FourCC),
}

/// Parse colour information
/// See ISOBMFF (ISO 14496-12:2020) § 12.1.5
fn read_colr<T: Read>(
src: &mut BMFFBox<T>,
strictness: ParseStrictness,
) -> Result<ColourInformation> {
) -> Result<ParsedColourInformation> {
let colour_type = be_u32(src)?.to_be_bytes();

match &colour_type {
Expand All @@ -3790,22 +3809,26 @@ fn read_colr<T: Read>(
)?;
}

Ok(ColourInformation::Nclx(NclxColourInformation {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range_flag,
}))
Ok(ParsedColourInformation::Supported(ColourInformation::Nclx(
NclxColourInformation {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range_flag,
},
)))
}
b"rICC" | b"prof" => Ok(ColourInformation::Icc(
b"rICC" | b"prof" => Ok(ParsedColourInformation::Supported(ColourInformation::Icc(
IccColourInformation {
bytes: src.read_into_try_vec()?,
},
FourCC::from(colour_type),
)),
))),
_ => {
error!("read_colr colour_type: {colour_type:?}");
Status::ColrBadType.into()
let four_cc = FourCC::from(colour_type);
warn!("read_colr: unsupported colour_type {four_cc:?}, skipping");
skip_box_remain(src)?;
Ok(ParsedColourInformation::Unsupported(four_cc))
}
}
}
Expand Down Expand Up @@ -5533,7 +5556,10 @@ fn read_hdlr<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> Resu
}

/// Parse an video description inside an stsd box.
fn read_video_sample_entry<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry> {
fn read_video_sample_entry<T: Read>(
src: &mut BMFFBox<T>,
strictness: ParseStrictness,
) -> Result<SampleEntry> {
let name = src.get_header().name;
let codec_type = match name {
BoxType::AVCSampleEntry | BoxType::AVC3SampleEntry => CodecType::H264,
Expand Down Expand Up @@ -5567,6 +5593,7 @@ fn read_video_sample_entry<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry>
// Skip clap/pasp/etc. for now.
let mut codec_specific = None;
let mut pixel_aspect_ratio = None;
let mut colour_info = None;
let mut protection_info = TryVec::new();
let mut iter = src.box_iter();
while let Some(mut b) = iter.next_box()? {
Expand Down Expand Up @@ -5683,6 +5710,22 @@ fn read_video_sample_entry<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry>
}
debug!("Parsed pasp box: {pasp:?}, PAR {pixel_aspect_ratio:?}");
}
BoxType::ColourInformationBox => {
if colour_info.is_some() {
warn!("Multiple colr boxes in video sample entry, keeping first");
Comment thread
alastor0325 marked this conversation as resolved.
fail_with_status_if(
strictness != ParseStrictness::Permissive,
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);
}
}
}
_ => {
debug!("Unsupported video codec, box {:?} found", b.head.name);
skip_box_content(&mut b)?;
Expand All @@ -5701,6 +5744,7 @@ fn read_video_sample_entry<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry>
codec_specific,
protection_info,
pixel_aspect_ratio,
colour_info,
})
}),
)
Expand Down Expand Up @@ -5908,9 +5952,9 @@ fn read_stsd<T: Read>(
while descriptions.len() < description_count {
if let Some(mut b) = iter.next_box()? {
let description = match track.track_type {
TrackType::Video => read_video_sample_entry(&mut b),
TrackType::Picture => read_video_sample_entry(&mut b),
TrackType::AuxiliaryVideo => read_video_sample_entry(&mut b),
TrackType::Video => read_video_sample_entry(&mut b, strictness),
TrackType::Picture => read_video_sample_entry(&mut b, strictness),
TrackType::AuxiliaryVideo => read_video_sample_entry(&mut b, strictness),
TrackType::Audio => read_audio_sample_entry(&mut b, strictness),
TrackType::Metadata => Err(Error::Unsupported("metadata track")),
TrackType::Unknown => Err(Error::Unsupported("unknown track type")),
Expand Down
5 changes: 3 additions & 2 deletions mp4parse/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,8 @@ fn read_stsd_mp4v() {
let mut iter = super::BoxIter::new(&mut stream);
let mut stream = iter.next_box().unwrap().unwrap();

let sample_entry = super::read_video_sample_entry(&mut stream).unwrap();
let sample_entry =
super::read_video_sample_entry(&mut stream, super::ParseStrictness::Normal).unwrap();

match sample_entry {
super::SampleEntry::Video(v) => {
Expand Down Expand Up @@ -1245,7 +1246,7 @@ fn unknown_video_sample_entry() {
});
let mut iter = super::BoxIter::new(&mut stream);
let mut stream = iter.next_box().unwrap().unwrap();
match super::read_video_sample_entry(&mut stream) {
match super::read_video_sample_entry(&mut stream, super::ParseStrictness::Normal) {
Ok(super::SampleEntry::Unknown) => (),
_ => panic!("expected a different error result"),
}
Expand Down
20 changes: 20 additions & 0 deletions mp4parse_capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ pub struct Mp4parseTrackVideoSampleInfo {
pub image_height: u16,
pub extra_data: Mp4parseByteData,
pub protected_data: Mp4parseSinfInfo,
/// True when a `colr` box with `colour_type = 'nclx'` was present. When false,
/// the CICP fields below are all zero and must not be interpreted.
pub has_colour_info: bool,
/// CICP colour primaries (ISO 23091-2 § 8.1). Valid only when `has_colour_info`.
pub colour_primaries: u8,
/// CICP transfer characteristics (ISO 23091-2 § 8.2). Valid only when `has_colour_info`.
pub transfer_characteristics: u8,
/// CICP matrix coefficients (ISO 23091-2 § 8.3). Valid only when `has_colour_info`.
/// Note: value 0 is a valid CICP value (Identity/GBR), not an absence indicator.
pub matrix_coefficients: u8,
/// Full range flag from the colr nclx box. Valid only when `has_colour_info`.
pub full_range_flag: bool,
}

#[repr(C)]
Expand Down Expand Up @@ -1120,6 +1132,14 @@ fn mp4parse_get_track_video_info_safe(
};
}
}
if let Some(mp4parse::ColourInformation::Nclx(ref nclx)) = video.colour_info {
sample_info.has_colour_info = true;
sample_info.colour_primaries = nclx.colour_primaries;
sample_info.transfer_characteristics = nclx.transfer_characteristics;
sample_info.matrix_coefficients = nclx.matrix_coefficients;
sample_info.full_range_flag = nclx.full_range_flag;
}

video_sample_infos.push(sample_info)?;
}

Expand Down
158 changes: 158 additions & 0 deletions mp4parse_capi/tests/test_colour_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use mp4parse_capi::*;
use std::io::Read;

extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize {
let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) };
let buf = unsafe { std::slice::from_raw_parts_mut(buf, size) };
match input.read(buf) {
Ok(n) => n as isize,
Err(_) => -1,
}
}

unsafe fn open_parser(path: &str) -> *mut Mp4parseParser {
let mut file = std::fs::File::open(path).expect("file not found");
let io = Mp4parseIo {
read: Some(buf_read),
userdata: &mut file as *mut _ as *mut std::os::raw::c_void,
};
let mut parser = std::ptr::null_mut();
let rv = mp4parse_new(&io, &mut parser);
assert_eq!(rv, Mp4parseStatus::Ok);
assert!(!parser.is_null());
parser
}

/// HDR10 (PQ): colour_primaries=9, transfer=16, matrix=9, full_range=false
#[test]
fn video_colr_nclx_hdr10() {
unsafe {
let parser = open_parser("tests/video_colr_nclx_hdr10.mp4");

let mut video = Mp4parseTrackVideoInfo::default();
let rv = mp4parse_get_track_video_info(parser, 0, &mut video);
assert_eq!(rv, Mp4parseStatus::Ok);

assert_eq!(video.sample_info_count, 1);
let sample = &*video.sample_info;
assert!(sample.has_colour_info);
assert_eq!(sample.colour_primaries, 9);
assert_eq!(sample.transfer_characteristics, 16);
assert_eq!(sample.matrix_coefficients, 9);
assert!(!sample.full_range_flag);

mp4parse_free(parser);
}
}

/// HDR10 full-range: colour_primaries=9, transfer=16, matrix=9, full_range=true
#[test]
fn video_colr_nclx_hdr10_full_range() {
unsafe {
let parser = open_parser("tests/video_colr_nclx_hdr10_full_range.mp4");

let mut video = Mp4parseTrackVideoInfo::default();
let rv = mp4parse_get_track_video_info(parser, 0, &mut video);
assert_eq!(rv, Mp4parseStatus::Ok);

assert_eq!(video.sample_info_count, 1);
let sample = &*video.sample_info;
assert!(sample.has_colour_info);
assert_eq!(sample.colour_primaries, 9);
assert_eq!(sample.transfer_characteristics, 16);
assert_eq!(sample.matrix_coefficients, 9);
assert!(sample.full_range_flag);

mp4parse_free(parser);
}
}

/// HLG: colour_primaries=9, transfer=18, matrix=9, full_range=false
#[test]
fn video_colr_nclx_hlg() {
unsafe {
let parser = open_parser("tests/video_colr_nclx_hlg.mp4");

let mut video = Mp4parseTrackVideoInfo::default();
let rv = mp4parse_get_track_video_info(parser, 0, &mut video);
assert_eq!(rv, Mp4parseStatus::Ok);

assert_eq!(video.sample_info_count, 1);
let sample = &*video.sample_info;
assert!(sample.has_colour_info);
assert_eq!(sample.colour_primaries, 9);
assert_eq!(sample.transfer_characteristics, 18);
assert_eq!(sample.matrix_coefficients, 9);
assert!(!sample.full_range_flag);

mp4parse_free(parser);
}
}

/// HLG full-range: colour_primaries=9, transfer=18, matrix=9, full_range=true
#[test]
fn video_colr_nclx_hlg_full_range() {
unsafe {
let parser = open_parser("tests/video_colr_nclx_hlg_full_range.mp4");

let mut video = Mp4parseTrackVideoInfo::default();
let rv = mp4parse_get_track_video_info(parser, 0, &mut video);
assert_eq!(rv, Mp4parseStatus::Ok);

assert_eq!(video.sample_info_count, 1);
let sample = &*video.sample_info;
assert!(sample.has_colour_info);
assert_eq!(sample.colour_primaries, 9);
assert_eq!(sample.transfer_characteristics, 18);
assert_eq!(sample.matrix_coefficients, 9);
assert!(sample.full_range_flag);

mp4parse_free(parser);
}
}

/// RGB (Identity matrix, MC=0): has_colour_info=true but matrix_coefficients=0.
/// Validates that has_colour_info correctly distinguishes "colr box present with MC=0"
/// from "no colr box" (where matrix_coefficients is also 0).
#[test]
fn video_colr_nclx_rgb_identity_matrix() {
unsafe {
let parser = open_parser("tests/video_colr_nclx_rgb.mp4");

let mut video = Mp4parseTrackVideoInfo::default();
let rv = mp4parse_get_track_video_info(parser, 0, &mut video);
assert_eq!(rv, Mp4parseStatus::Ok);

assert_eq!(video.sample_info_count, 1);
let sample = &*video.sample_info;
assert!(sample.has_colour_info);
assert_eq!(sample.colour_primaries, 1);
assert_eq!(sample.transfer_characteristics, 1);
assert_eq!(sample.matrix_coefficients, 0); // Identity/GBR — valid CICP value, not "absent"
assert!(!sample.full_range_flag);

mp4parse_free(parser);
}
}

/// No colr box: has_colour_info=false, CICP fields are zero
#[test]
fn video_no_colr_box() {
unsafe {
let parser = open_parser("tests/white.mp4");

let mut video = Mp4parseTrackVideoInfo::default();
let rv = mp4parse_get_track_video_info(parser, 0, &mut video);
assert_eq!(rv, Mp4parseStatus::Ok);

assert_eq!(video.sample_info_count, 1);
let sample = &*video.sample_info;
assert!(!sample.has_colour_info);
assert_eq!(sample.colour_primaries, 0);
assert_eq!(sample.transfer_characteristics, 0);
assert_eq!(sample.matrix_coefficients, 0);
assert!(!sample.full_range_flag);

mp4parse_free(parser);
}
}
Binary file added mp4parse_capi/tests/video_colr_nclx_hdr10.mp4
Binary file not shown.
Binary file not shown.
Binary file added mp4parse_capi/tests/video_colr_nclx_hlg.mp4
Binary file not shown.
Binary file not shown.
Binary file added mp4parse_capi/tests/video_colr_nclx_rgb.mp4
Binary file not shown.
Loading