use super::*;
use ffmpeg_next::ffi::AVColorRange;
#[test]
fn map_range_for_forces_full_for_every_yuvj_variant() {
let yuvj = [
PixelFormat::Yuvj411p,
PixelFormat::Yuvj420p,
PixelFormat::Yuvj422p,
PixelFormat::Yuvj440p,
PixelFormat::Yuvj444p,
];
let unspecified = AVColorRange::AVCOL_RANGE_UNSPECIFIED as i32;
let mpeg = AVColorRange::AVCOL_RANGE_MPEG as i32;
let jpeg = AVColorRange::AVCOL_RANGE_JPEG as i32;
for pf in &yuvj {
assert_eq!(
map_range_for(pf, unspecified),
ColorRange::Full,
"{pf:?} with UNSPECIFIED color_range must deliver Full"
);
assert_eq!(
map_range_for(pf, mpeg),
ColorRange::Full,
"{pf:?} is full-range by definition even if color_range says MPEG"
);
assert_eq!(map_range_for(pf, jpeg), ColorRange::Full);
}
}
#[test]
fn map_range_for_defers_for_non_yuvj() {
let unspecified = AVColorRange::AVCOL_RANGE_UNSPECIFIED as i32;
let jpeg = AVColorRange::AVCOL_RANGE_JPEG as i32;
let mpeg = AVColorRange::AVCOL_RANGE_MPEG as i32;
assert_eq!(
map_range_for(&PixelFormat::Yuv420p, unspecified),
ColorRange::Unspecified
);
assert_eq!(map_range_for(&PixelFormat::Yuv420p, jpeg), ColorRange::Full);
assert_eq!(
map_range_for(&PixelFormat::Yuv420p, mpeg),
ColorRange::Limited
);
assert_eq!(
map_range_for(&PixelFormat::Rgb24, unspecified),
ColorRange::Unspecified
);
assert_eq!(
map_range_for(&PixelFormat::Rgba, unspecified),
ColorRange::Unspecified
);
}
#[test]
fn is_yuvj_covers_exactly_the_five_variants() {
for pf in &[
PixelFormat::Yuvj411p,
PixelFormat::Yuvj420p,
PixelFormat::Yuvj422p,
PixelFormat::Yuvj440p,
PixelFormat::Yuvj444p,
] {
assert!(is_yuvj(pf), "{pf:?} should be recognized as yuvj");
}
for pf in &[
PixelFormat::Yuv411p,
PixelFormat::Yuv420p,
PixelFormat::Yuv422p,
PixelFormat::Yuv440p,
PixelFormat::Yuv444p,
PixelFormat::Nv12,
PixelFormat::Rgb24,
PixelFormat::Gray8,
] {
assert!(!is_yuvj(pf), "{pf:?} must not be classified yuvj");
}
}
#[test]
fn yuvj420p_unspecified_range_delivers_full() {
let mut frame = ffmpeg_next::frame::Video::new(ffmpeg_next::format::Pixel::YUVJ420P, 64, 48);
unsafe {
(*frame.as_mut_ptr()).color_range = AVColorRange::AVCOL_RANGE_UNSPECIFIED;
}
let out = video_frame_from(&frame, Timebase::default(), FrameLimits::default())
.expect("YUVJ420P frame should convert to a VideoFrame");
assert_eq!(*out.pixel_format(), PixelFormat::Yuvj420p);
assert_eq!(
out.color().range(),
ColorRange::Full,
"YUVJ420P with UNSPECIFIED color_range must be delivered as Full"
);
}
fn frame_with_raw_format(raw: i32) -> ffmpeg_next::frame::Video {
let mut frame = ffmpeg_next::frame::Video::empty();
unsafe {
let frame_ptr = frame.as_mut_ptr();
(*frame_ptr).format = raw;
(*frame_ptr).width = 64;
(*frame_ptr).height = 48;
}
frame
}
#[test]
fn an_unsupported_format_is_named_in_the_error() {
use ffmpeg_next::ffi::AVPixelFormat;
let raw = AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32;
let frame = frame_with_raw_format(raw);
let Err(err) = video_frame_from(&frame, Timebase::default(), FrameLimits::default()) else {
panic!("a hardware surface carries no deliverable CPU layout");
};
let ConvertError::UnsupportedPixelFormat(p) = &err else {
panic!("expected UnsupportedPixelFormat, got {err:?}");
};
assert_eq!(*p.format(), PixelFormat::None);
assert_eq!(p.raw(), raw);
assert_eq!(p.name(), Some("videotoolbox_vld"));
let rendered = err.to_string();
assert!(
rendered.contains(&raw.to_string()),
"the raw id is missing from {rendered:?}"
);
assert!(
rendered.contains("videotoolbox_vld"),
"the FFmpeg name is missing from {rendered:?}"
);
}
#[test]
fn an_unnameable_format_still_reports_its_raw_id() {
let raw = 99_999;
let frame = frame_with_raw_format(raw);
let Err(err) = video_frame_from(&frame, Timebase::default(), FrameLimits::default()) else {
panic!("an unmappable format integer has no deliverable layout");
};
let ConvertError::UnsupportedPixelFormat(p) = &err else {
panic!("expected UnsupportedPixelFormat, got {err:?}");
};
assert_eq!(*p.format(), PixelFormat::None);
assert_eq!(p.raw(), raw);
assert_eq!(p.name(), None);
let rendered = err.to_string();
assert!(
rendered.contains("99999"),
"the raw id is missing from {rendered:?}"
);
assert!(
rendered.contains("unnamed by libavutil"),
"the absent-name case is not spelled out in {rendered:?}"
);
}
#[allow(clippy::too_many_arguments)]
fn mastering_display_bytes(
r_x: (i32, i32),
r_y: (i32, i32),
g_x: (i32, i32),
g_y: (i32, i32),
b_x: (i32, i32),
b_y: (i32, i32),
wp_x: (i32, i32),
wp_y: (i32, i32),
min_luminance: (i32, i32),
max_luminance: (i32, i32),
has_primaries: i32,
has_luminance: i32,
) -> Vec<u8> {
let mut out = Vec::with_capacity(MASTERING_DISPLAY_METADATA_BYTES);
for (num, den) in [
r_x,
r_y,
g_x,
g_y,
b_x,
b_y,
wp_x,
wp_y,
min_luminance,
max_luminance,
] {
out.extend_from_slice(&num.to_ne_bytes());
out.extend_from_slice(&den.to_ne_bytes());
}
out.extend_from_slice(&has_primaries.to_ne_bytes());
out.extend_from_slice(&has_luminance.to_ne_bytes());
out
}
const ZERO_RATIONAL: (i32, i32) = (0, 1);
#[test]
fn parse_mastering_display_reads_a_real_hdr10_payload() {
let bytes = mastering_display_bytes(
(34_000, 50_000), (16_000, 50_000), (13_250, 50_000), (34_500, 50_000), (7_500, 50_000), (3_000, 50_000), (15_635, 50_000), (16_450, 50_000), (1, 10_000), (10_000_000, 10_000), 1,
1,
);
let md = parse_mastering_display(&bytes).expect("a full 88-byte payload parses");
assert_eq!(
md.display_primaries(),
[(34_000, 16_000), (13_250, 34_500), (7_500, 3_000)],
"R, G, B chromaticities in ST 2086 fixed-point units"
);
assert_eq!(md.white_point(), (15_635, 16_450));
assert_eq!(md.min_luminance(), (1, 10_000));
assert_eq!(md.max_luminance(), (10_000_000, 10_000));
}
#[test]
fn parse_mastering_display_rescales_a_non_standard_chroma_denominator() {
let bytes = mastering_display_bytes(
(17, 200), ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
1,
1,
);
let md = parse_mastering_display(&bytes).expect("parses");
assert_eq!(md.display_primaries()[0].0, 4_250, "17/200 == 4250/50000");
}
#[test]
fn parse_mastering_display_refuses_a_short_payload() {
let bytes = mastering_display_bytes(
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
1,
1,
);
assert_eq!(bytes.len(), MASTERING_DISPLAY_METADATA_BYTES);
assert!(parse_mastering_display(&bytes[..bytes.len() - 1]).is_none());
assert!(parse_mastering_display(&[]).is_none());
}
#[test]
fn parse_mastering_display_refuses_a_negative_component() {
let negative_primary = mastering_display_bytes(
(-1, 50_000), ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
1,
1,
);
assert_eq!(negative_primary.len(), MASTERING_DISPLAY_METADATA_BYTES);
assert!(parse_mastering_display(&negative_primary).is_none());
let negative_luminance = mastering_display_bytes(
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
(-1, 10_000), ZERO_RATIONAL,
1,
1,
);
assert_eq!(negative_luminance.len(), MASTERING_DISPLAY_METADATA_BYTES);
assert!(parse_mastering_display(&negative_luminance).is_none());
}
#[test]
fn parse_mastering_display_refuses_the_flags_unset_default_record() {
let bytes = mastering_display_bytes(
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
0, 0, );
assert_eq!(bytes.len(), MASTERING_DISPLAY_METADATA_BYTES);
assert!(
parse_mastering_display(&bytes).is_none(),
"an unset-flags record must not be reported as real HDR metadata"
);
}
#[test]
fn parse_mastering_display_refuses_every_partial_flag_combination() {
let real = (
(34_000, 50_000),
(16_000, 50_000),
(13_250, 50_000),
(34_500, 50_000),
(7_500, 50_000),
(3_000, 50_000),
(15_635, 50_000),
(16_450, 50_000),
(1, 10_000),
(10_000_000, 10_000),
);
for (has_primaries, has_luminance) in [(1, 0), (0, 1)] {
let bytes = mastering_display_bytes(
real.0,
real.1,
real.2,
real.3,
real.4,
real.5,
real.6,
real.7,
real.8,
real.9,
has_primaries,
has_luminance,
);
assert!(
parse_mastering_display(&bytes).is_none(),
"has_primaries={has_primaries} has_luminance={has_luminance} must refuse, \
real numbers behind an unset flag notwithstanding"
);
}
let bytes = mastering_display_bytes(
real.0, real.1, real.2, real.3, real.4, real.5, real.6, real.7, real.8, real.9, 1, 1,
);
assert!(parse_mastering_display(&bytes).is_some());
}
#[test]
fn parse_mastering_display_refuses_a_non_positive_luminance_denominator() {
let zero_min_den = mastering_display_bytes(
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
(1, 0), ZERO_RATIONAL,
1,
1,
);
assert!(parse_mastering_display(&zero_min_den).is_none());
let negative_max_den = mastering_display_bytes(
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
ZERO_RATIONAL,
(1, -1), 1,
1,
);
assert!(parse_mastering_display(&negative_max_den).is_none());
}
#[test]
fn parse_content_light_level_reads_max_cll_and_max_fall() {
let mut bytes = Vec::with_capacity(CONTENT_LIGHT_METADATA_BYTES);
bytes.extend_from_slice(&1000u32.to_ne_bytes());
bytes.extend_from_slice(&400u32.to_ne_bytes());
let cll = parse_content_light_level(&bytes).expect("an 8-byte payload parses");
assert_eq!(cll.max_cll(), 1000);
assert_eq!(cll.max_fall(), 400);
}
#[test]
fn parse_content_light_level_refuses_a_short_payload() {
assert!(parse_content_light_level(&[0u8; 7]).is_none());
assert!(parse_content_light_level(&[]).is_none());
}
#[test]
fn absent_side_data_answers_none_for_both_hdr_seats() {
let side_data: Vec<SideDataEntry> = Vec::new();
assert!(find_mastering_display(&side_data).is_none());
assert!(find_content_light_level(&side_data).is_none());
}
#[test]
fn hdr_seats_are_found_by_kind_among_unrelated_side_data() {
let cll_bytes = {
let mut b = Vec::with_capacity(CONTENT_LIGHT_METADATA_BYTES);
b.extend_from_slice(&500u32.to_ne_bytes());
b.extend_from_slice(&100u32.to_ne_bytes());
b
};
let side_data = vec![
SideDataEntry::new(
0xDEAD_BEEFu32 as i32,
FfmpegBytes::copy_from_slice(&[1, 2, 3]),
),
SideDataEntry::new(
AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL as i32,
FfmpegBytes::copy_from_slice(&cll_bytes),
),
];
assert!(find_mastering_display(&side_data).is_none());
let cll = find_content_light_level(&side_data).expect("the CLL entry is present");
assert_eq!(cll.max_cll(), 500);
assert_eq!(cll.max_fall(), 100);
}