use std::collections::{BTreeMap, BTreeSet};
use super::{Mode, Rate};
use crate::Size;
#[derive(Clone, Copy, Debug)]
pub(super) struct Request {
pub size: Size,
pub framerate: Option<Rate>,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Candidate {
pub size: Size,
pub framerate: Option<Rate>,
pub cost: u8,
}
pub(super) fn nearest<T>(
candidates: impl IntoIterator<Item = T>,
want: Request,
candidate: impl Fn(&T) -> Candidate,
) -> Option<T> {
candidates
.into_iter()
.filter(|item| candidate(item).size.validate("camera resolution").is_ok())
.min_by(|left, right| {
let (left, right) = (candidate(left), candidate(right));
distance(left.size, want.size)
.cmp(&distance(right.size, want.size))
.then_with(|| rate_distance(left.framerate, right.framerate, want.framerate))
.then_with(|| left.cost.cmp(&right.cost))
})
}
fn rate_distance(left: Option<Rate>, right: Option<Rate>, want: Option<Rate>) -> std::cmp::Ordering {
let Some(want) = want else {
return std::cmp::Ordering::Equal;
};
match (left, right) {
(Some(left), Some(right)) => (left.as_f64() - want.as_f64())
.abs()
.total_cmp(&(right.as_f64() - want.as_f64()).abs()),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
}
}
fn distance(size: Size, want: Size) -> u64 {
u64::from(size.width.abs_diff(want.width)) + u64::from(size.height.abs_diff(want.height))
}
pub(super) fn modes(reported: impl IntoIterator<Item = (Size, Vec<Rate>)>) -> Vec<Mode> {
let mut sizes: BTreeMap<(u32, u32), BTreeSet<Rate>> = BTreeMap::new();
for (size, rates) in reported {
sizes.entry((size.width, size.height)).or_default().extend(rates);
}
let mut modes: Vec<Mode> = sizes
.into_iter()
.map(|((width, height), framerates)| Mode {
width,
height,
framerates: framerates.into_iter().rev().collect(),
})
.collect();
modes.sort_by_key(|mode| std::cmp::Reverse(u64::from(mode.width) * u64::from(mode.height)));
modes
}
#[cfg(test)]
mod tests {
use super::*;
fn rate(fps: u32) -> Rate {
Rate::new(fps, 1).unwrap()
}
#[test]
fn rate_scoring_preserves_fractional_precision_and_handles_unknown_rates() {
let ntsc = Some(Rate::new(60000, 1001).unwrap());
let thirty = Some(rate(30));
assert!(rate_distance(ntsc, thirty, Some(rate(60))).is_lt());
assert!(rate_distance(thirty, ntsc, Some(rate(30))).is_lt());
assert!(rate_distance(ntsc, None, Some(rate(60))).is_lt());
assert!(rate_distance(ntsc, thirty, None).is_eq());
}
#[test]
fn distance_is_zero_only_on_an_exact_match() {
let want = Size::new(1280, 720);
assert_eq!(distance(Size::new(1280, 720), want), 0);
assert_eq!(distance(Size::new(1280, 600), want), 120);
assert_eq!(distance(Size::new(1280, 840), want), 120);
}
#[test]
fn nearest_prefers_geometry_then_rate_then_cost_then_order() {
let candidate = |width, height, fps: Option<u32>, cost| Candidate {
size: Size::new(width, height),
framerate: fps.map(rate),
cost,
};
let want = |framerate| Request {
size: Size::new(1280, 720),
framerate,
};
let candidates = [
candidate(640, 480, Some(30), 0),
candidate(1280, 720, Some(30), 1),
candidate(1280, 720, Some(15), 0),
candidate(1280, 720, Some(30), 0),
candidate(1281, 720, Some(30), 0),
];
let pick = |framerate| nearest(candidates, want(framerate), |c| *c).unwrap();
let chosen = pick(Some(rate(30)));
assert_eq!((chosen.framerate, chosen.cost), (Some(rate(30)), 0));
assert_eq!(pick(None).framerate, Some(rate(15)));
assert!(nearest([candidate(1281, 721, None, 0)], want(None), |c| *c).is_none());
}
#[test]
fn modes_merge_sizes_and_sort_largest_and_fastest_first() {
let modes = modes([
(Size::new(640, 480), vec![rate(30)]),
(Size::new(1280, 720), vec![rate(15)]),
(Size::new(640, 480), vec![rate(60), rate(30)]),
]);
assert_eq!(
modes,
[
Mode {
width: 1280,
height: 720,
framerates: vec![rate(15)],
},
Mode {
width: 640,
height: 480,
framerates: vec![rate(60), rate(30)],
},
]
);
}
}