use ffmpeg_next::ffi::{
AVFrame, AVFrameSideDataType, av_frame_alloc, av_frame_free, av_frame_new_side_data,
};
use super::{
CachePlan, ScaledOutput, StageKey, ThreadOwner, cache_plan, is_acceptable_request, is_stale,
scaled_sample_aspect_ratio, side_data_forbids_scaling,
};
fn key() -> StageKey {
StageKey {
source_cv_format: 0x78343230,
source: (1920, 1080),
fitted: (512, 288),
sw_format: 23,
}
}
#[test]
fn an_empty_cache_builds() {
assert_eq!(cache_plan(None, key()), CachePlan::Build);
}
#[test]
fn the_same_key_reuses_the_standing_session() {
assert_eq!(cache_plan(Some(key()), key()), CachePlan::Reuse);
}
#[test]
fn a_different_requested_size_retires_the_standing_session() {
let mut want = key();
want.fitted = (256, 144);
assert_eq!(cache_plan(Some(key()), want), CachePlan::Build);
}
#[test]
fn a_mid_stream_change_in_the_stream_itself_retires_the_session() {
for want in [
StageKey {
source: (1280, 720),
..key()
},
StageKey {
source_cv_format: 0x78343230 + 1,
..key()
},
StageKey {
sw_format: key().sw_format + 1,
..key()
},
] {
assert_eq!(
cache_plan(Some(key()), want),
CachePlan::Build,
"a session built for {:?} must not be reused for {want:?}",
key()
);
}
}
#[test]
fn a_thread_owner_recognises_only_its_own_thread() {
let here = ThreadOwner::current();
assert!(here.is_current(), "the creating thread owns it");
let elsewhere = std::thread::spawn(move || {
let seen_from_there = here.is_current();
let theirs = ThreadOwner::current();
(seen_from_there, theirs.is_current(), theirs)
})
.join()
.expect("the probe thread finished");
assert!(
!elsewhere.0,
"a session created on the parent thread is not owned by a child one"
);
assert!(elsewhere.1, "the child thread owns what it took there");
assert!(
!elsewhere.2.is_current(),
"and back on the parent thread, the child's owner is foreign again"
);
assert!(here.is_current(), "the parent's own claim is unchanged");
}
#[test]
fn an_owner_whose_thread_has_exited_never_matches_a_later_one() {
let first = std::thread::spawn(ThreadOwner::current)
.join()
.expect("the first probe thread finished");
for _ in 0..64 {
let matched = std::thread::spawn(move || first.is_current())
.join()
.expect("a later probe thread finished");
assert!(
!matched,
"an owner taken on a thread that has since exited must never match a later thread"
);
}
}
fn with_side_data<R>(
entries: &[AVFrameSideDataType],
probe: impl FnOnce(*const AVFrame) -> R,
) -> R {
unsafe {
let frame = av_frame_alloc();
assert!(!frame.is_null(), "av_frame_alloc");
for kind in entries {
let entry = av_frame_new_side_data(frame, *kind, 16);
assert!(!entry.is_null(), "av_frame_new_side_data");
}
let answer = probe(frame.cast_const());
let mut frame = frame;
av_frame_free(&mut frame);
answer
}
}
#[test]
fn a_frame_with_no_side_data_is_scalable() {
assert!(!with_side_data(&[], |frame| unsafe {
side_data_forbids_scaling(frame)
}));
}
#[test]
fn side_data_a_resize_would_not_strand_is_scalable() {
for kind in [
AVFrameSideDataType::AV_FRAME_DATA_MASTERING_DISPLAY_METADATA,
AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL,
AVFrameSideDataType::AV_FRAME_DATA_DISPLAYMATRIX,
AVFrameSideDataType::AV_FRAME_DATA_A53_CC,
] {
let forbids = with_side_data(&[kind], |frame| unsafe { side_data_forbids_scaling(frame) });
assert!(!forbids, "{kind:?} does not describe the picture's grid");
}
}
#[test]
fn side_data_expressed_in_the_pictures_grid_forbids_scaling() {
for kind in [
AVFrameSideDataType::AV_FRAME_DATA_PANSCAN,
AVFrameSideDataType::AV_FRAME_DATA_SPHERICAL,
AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST,
] {
let forbids = with_side_data(&[kind], |frame| unsafe { side_data_forbids_scaling(frame) });
assert!(forbids, "{kind:?} is expressed in the source grid");
}
let mixed = [
AVFrameSideDataType::AV_FRAME_DATA_MASTERING_DISPLAY_METADATA,
AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL,
AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST,
];
assert!(with_side_data(&mixed, |frame| unsafe {
side_data_forbids_scaling(frame)
}));
}
#[test]
fn malformed_side_data_bookkeeping_forbids_scaling() {
for hostile in [
-1,
i32::MIN,
crate::decoder::HW_COPY_SIDE_DATA_MAX_ENTRIES as i32 + 1,
i32::MAX,
] {
let forbids = with_side_data(
&[AVFrameSideDataType::AV_FRAME_DATA_A53_CC],
|frame| unsafe {
let raw = frame.cast_mut();
let real = (*raw).nb_side_data;
(*raw).nb_side_data = hostile;
let answer = side_data_forbids_scaling(frame);
(*raw).nb_side_data = real;
answer
},
);
assert!(forbids, "nb_side_data = {hostile} is not walkable");
}
}
#[test]
fn a_positive_count_with_no_array_forbids_scaling() {
let forbids = with_side_data(
&[AVFrameSideDataType::AV_FRAME_DATA_A53_CC],
|frame| unsafe {
let raw = frame.cast_mut();
let real = (*raw).side_data;
(*raw).side_data = core::ptr::null_mut();
let answer = side_data_forbids_scaling(frame);
(*raw).side_data = real;
answer
},
);
assert!(forbids, "a positive count with no array is malformed");
}
#[test]
fn a_null_entry_inside_the_array_forbids_scaling() {
let forbids = with_side_data(
&[
AVFrameSideDataType::AV_FRAME_DATA_A53_CC,
AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL,
],
|frame| unsafe {
let raw = frame.cast_mut();
let slot = (*raw).side_data.add(1);
let real = *slot;
*slot = core::ptr::null_mut();
let answer = side_data_forbids_scaling(frame);
*slot = real;
answer
},
);
assert!(
forbids,
"a null entry inside a non-empty array is malformed"
);
}
#[test]
fn a_downscale_is_acceptable_and_an_upscale_is_not() {
assert!(is_acceptable_request((512, 288), (1920, 1080)));
assert!(is_acceptable_request((1, 1), (1920, 1080)));
assert!(is_acceptable_request((1920, 1080), (1920, 1080)));
assert!(!is_acceptable_request((1921, 1080), (1920, 1080)));
assert!(!is_acceptable_request((1920, 1081), (1920, 1080)));
assert!(!is_acceptable_request((3840, 2160), (1920, 1080)));
}
#[test]
fn a_zero_extent_is_never_acceptable() {
assert!(!is_acceptable_request((0, 288), (1920, 1080)));
assert!(!is_acceptable_request((512, 0), (1920, 1080)));
assert!(!is_acceptable_request((0, 0), (1920, 1080)));
assert!(!is_acceptable_request((0, 0), (0, 0)));
}
#[test]
fn a_uniform_scale_leaves_the_sample_aspect_ratio_alone() {
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (1920, 1080), (960, 540)),
Some((1, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((40, 33), (720, 480), (360, 240)),
Some((40, 33))
);
}
#[test]
fn the_answer_is_reduced_even_when_the_source_ratio_was_not() {
assert_eq!(
scaled_sample_aspect_ratio((2, 2), (1920, 1080), (960, 540)),
Some((1, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((80, 66), (720, 480), (360, 240)),
Some((40, 33))
);
}
#[test]
fn a_non_uniform_scale_corrects_the_sample_aspect_ratio() {
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (1920, 1080), (480, 540)),
Some((2, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (1920, 1080), (1920, 270)),
Some((1, 4))
);
}
#[test]
fn an_unspecified_sample_aspect_ratio_stays_unspecified() {
assert_eq!(
scaled_sample_aspect_ratio((0, 1), (1920, 1080), (480, 540)),
Some((0, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((1, 0), (1920, 1080), (480, 540)),
Some((1, 0))
);
assert_eq!(
scaled_sample_aspect_ratio((-1, 1), (1920, 1080), (480, 540)),
Some((-1, 1))
);
}
#[test]
fn a_degenerate_extent_leaves_the_sample_aspect_ratio_alone() {
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (0, 1080), (480, 540)),
Some((1, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (1920, 0), (480, 540)),
Some((1, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (1920, 1080), (0, 540)),
Some((1, 1))
);
assert_eq!(
scaled_sample_aspect_ratio((1, 1), (1920, 1080), (480, 0)),
Some((1, 1))
);
}
#[test]
fn a_ratio_that_will_not_fit_the_pair_refuses_rather_than_lying() {
let huge = (i32::MAX, i32::MAX - 1);
assert_eq!(
scaled_sample_aspect_ratio(huge, (1920, 1080), (479, 541)),
None
);
}
#[test]
fn the_widest_inputs_the_types_admit_neither_overflow_nor_panic() {
let widest = (i32::MAX, i32::MAX);
assert_eq!(
scaled_sample_aspect_ratio(widest, (u32::MAX, u32::MAX), (u32::MAX, u32::MAX)),
Some((1, 1)),
"a no-op scale answers the same ratio in reduced form"
);
assert_eq!(
scaled_sample_aspect_ratio(widest, (u32::MAX, 1), (1, u32::MAX)),
None,
"a result too wide for the `c_int` pair refuses rather than lying"
);
}
#[test]
fn latching_a_download_failure_leaves_the_request_standing() {
let mut stage = ScaledOutput::new();
stage.latch_failure();
assert_eq!(stage.requested(), None);
assert!(stage.request((512, 288), (1920, 1080)).is_supported());
stage.latch_failure();
assert_eq!(
stage.requested(),
Some((512, 288)),
"the caller's request outlives a download failure"
);
assert!(stage.request((256, 144), (1920, 1080)).is_supported());
assert_eq!(stage.requested(), Some((256, 144)));
}
#[test]
fn a_broken_promise_is_observable_and_a_new_request_renews_it() {
let mut stage = ScaledOutput::new();
assert!(
stage.promise_stands(),
"nothing has been promised, so nothing is broken"
);
assert!(stage.request((512, 288), (1920, 1080)).is_supported());
assert!(stage.promise_stands());
stage.latch_failure();
assert!(
!stage.promise_stands(),
"a frame went out unfitted, so the capability word must stop promising"
);
assert_eq!(
stage.requested(),
Some((512, 288)),
"the request itself outlives the broken promise"
);
assert!(stage.request((256, 144), (1920, 1080)).is_supported());
assert!(
stage.promise_stands(),
"a fresh request buys a fresh promise"
);
}
#[test]
fn a_refused_request_renews_nothing_and_returns_to_full_size() {
let mut stage = ScaledOutput::new();
assert!(stage.request((512, 288), (1920, 1080)).is_supported());
stage.latch_failure();
assert!(!stage.promise_stands());
assert!(!stage.request((3840, 2160), (1920, 1080)).is_supported());
assert!(
!stage.promise_stands(),
"a refused request must not renew a broken promise"
);
assert_eq!(stage.requested(), None);
}
#[test]
fn a_cache_built_for_another_stream_shape_is_stale() {
let built = StageKey {
source_cv_format: 0x78343230,
source: (3840, 2160),
fitted: (512, 288),
sw_format: 23,
};
assert!(!is_stale(
None,
built.source_cv_format,
built.source,
built.sw_format
));
assert!(!is_stale(
Some(built),
built.source_cv_format,
built.source,
built.sw_format
));
for (cv, extent, sw) in [
(built.source_cv_format, (1280, 720), built.sw_format),
(built.source_cv_format + 1, built.source, built.sw_format),
(built.source_cv_format, built.source, built.sw_format + 1),
] {
assert!(
is_stale(Some(built), cv, extent, sw),
"state built for {built:?} must not survive a source of {cv:?} {extent:?} {sw:?}"
);
}
let mut resized = built;
resized.fitted = (256, 144);
assert!(!is_stale(
Some(resized),
built.source_cv_format,
built.source,
built.sw_format
));
assert_eq!(cache_plan(Some(resized), built), CachePlan::Build);
}
#[test]
fn a_broken_promise_stops_the_staging_too() {
let mut stage = ScaledOutput::new();
assert!(stage.request((512, 288), (1920, 1080)).is_supported());
assert!(stage.promise_stands());
assert!(stage.staging_armed());
stage.latch_failure();
assert!(!stage.promise_stands(), "the caller is told");
assert!(
!stage.staging_armed(),
"and the stage stays off, so a later scalable frame is not fitted behind that answer"
);
assert_eq!(
stage.requested(),
Some((512, 288)),
"the request itself is remembered, so an explicit renewal knows what to renew"
);
assert!(stage.request((512, 288), (1920, 1080)).is_supported());
assert!(stage.promise_stands());
assert!(stage.staging_armed());
}
#[test]
fn a_refused_request_does_not_rearm_the_stage() {
let mut stage = ScaledOutput::new();
assert!(stage.request((512, 288), (1920, 1080)).is_supported());
stage.latch_failure();
assert!(!stage.staging_armed());
assert!(!stage.request((3840, 2160), (1920, 1080)).is_supported());
assert!(
!stage.staging_armed(),
"an upscale is refused, so it renews nothing"
);
assert!(!stage.promise_stands());
}
#[test]
fn cancelling_clears_the_request_the_way_a_refusal_does() {
let mut cancelled = ScaledOutput::new();
assert!(cancelled.request((512, 288), (1920, 1080)).is_supported());
cancelled.cancel();
let mut refused = ScaledOutput::new();
assert!(refused.request((512, 288), (1920, 1080)).is_supported());
assert!(!refused.request((3840, 2160), (1920, 1080)).is_supported());
assert_eq!(cancelled.requested(), None);
assert_eq!(
cancelled.requested(),
refused.requested(),
"the two refusal roads must leave the session in the same state"
);
assert!(cancelled.promise_stands());
assert!(cancelled.staging_armed());
assert!(cancelled.request((256, 144), (1920, 1080)).is_supported());
assert_eq!(cancelled.requested(), Some((256, 144)));
}
#[test]
fn cancelling_an_idle_stage_changes_nothing() {
let mut stage = ScaledOutput::new();
stage.cancel();
assert_eq!(stage.requested(), None);
assert!(stage.promise_stands());
assert!(stage.staging_armed());
}