1 use std::iter::FromIterator;
2 
3 use ordered_float::NotNan;
4 
5 use crate::{
6     algorithms::FitnessDistance, MandatoryMediaTrackConstraints, MediaTrackSettings,
7     MediaTrackSupportedConstraints, SanitizedMandatoryMediaTrackConstraints,
8 };
9 
10 pub trait TieBreakingPolicy {
11     /// Selects a preferred candidate from a non-empty selection of optimal candidates.
12     ///
13     /// As specified in step 6 of the `SelectSettings` algorithm:
14     /// https://www.w3.org/TR/mediacapture-streams/#dfn-selectsettings
15     ///
16     /// > Select one settings dictionary from candidates, and return it as the result
17     /// > of the SelectSettings algorithm. The User Agent MUST use one with the
18     /// > smallest fitness distance, as calculated in step 3.
19     /// > If more than one settings dictionary have the smallest fitness distance,
20     /// > the User Agent chooses one of them based on system default property values
21     /// > and User Agent default property values.
22     fn select_candidate<'a, I>(&self, candidates: I) -> &'a MediaTrackSettings
23     where
24         I: IntoIterator<Item = &'a MediaTrackSettings>;
25 }
26 
27 /// A naïve tie-breaking policy that just picks the first settings item it encounters.
28 pub struct FirstPolicy;
29 
30 impl FirstPolicy {
31     pub fn new() -> Self {
32         Self
33     }
34 }
35 
36 impl Default for FirstPolicy {
37     fn default() -> Self {
38         Self::new()
39     }
40 }
41 
42 impl TieBreakingPolicy for FirstPolicy {
43     fn select_candidate<'a, I>(&self, candidates: I) -> &'a MediaTrackSettings
44     where
45         I: IntoIterator<Item = &'a MediaTrackSettings>,
46     {
47         // Safety: We know that `candidates is non-empty:
48         candidates
49             .into_iter()
50             .next()
51             .expect("The `candidates` iterator should have produced at least one item.")
52     }
53 }
54 
55 /// A tie-breaking policy that picks the settings item that's closest to the specified ideal settings.
56 pub struct ClosestToIdealPolicy {
57     sanitized_constraints: SanitizedMandatoryMediaTrackConstraints,
58 }
59 
60 impl ClosestToIdealPolicy {
61     pub fn new(
62         ideal_settings: MediaTrackSettings,
63         supported_constraints: &MediaTrackSupportedConstraints,
64     ) -> Self {
65         let sanitized_constraints = MandatoryMediaTrackConstraints::from_iter(
66             ideal_settings
67                 .into_iter()
68                 .map(|(property, setting)| (property, setting.into())),
69         )
70         .into_resolved()
71         .into_sanitized(supported_constraints);
72 
73         Self {
74             sanitized_constraints,
75         }
76     }
77 }
78 
79 impl TieBreakingPolicy for ClosestToIdealPolicy {
80     fn select_candidate<'b, I>(&self, candidates: I) -> &'b MediaTrackSettings
81     where
82         I: IntoIterator<Item = &'b MediaTrackSettings>,
83     {
84         candidates
85             .into_iter()
86             .min_by_key(|settings| {
87                 let fitness_distance = self
88                     .sanitized_constraints
89                     .fitness_distance(settings)
90                     .expect("Fitness distance should be positive.");
91                 NotNan::new(fitness_distance).expect("Expected non-NaN fitness distance.")
92             })
93             .expect("The `candidates` iterator should have produced at least one item.")
94     }
95 }
96 
97 #[cfg(test)]
98 mod tests {
99     use super::*;
100 
101     use std::iter::FromIterator;
102 
103     use crate::{
104         property::all::name::*, MediaTrackSettings, MediaTrackSupportedConstraints, ResizeMode,
105     };
106 
107     #[test]
108     fn first() {
109         let settings = vec![
110             MediaTrackSettings::from_iter([(&DEVICE_ID, "device-id-0".into())]),
111             MediaTrackSettings::from_iter([(&DEVICE_ID, "device-id-1".into())]),
112             MediaTrackSettings::from_iter([(&DEVICE_ID, "device-id-2".into())]),
113         ];
114 
115         let policy = FirstPolicy::default();
116 
117         let actual = policy.select_candidate(&settings);
118 
119         let expected = &settings[0];
120 
121         assert_eq!(actual, expected);
122     }
123 
124     #[test]
125     fn closest_to_ideal() {
126         let supported_constraints = MediaTrackSupportedConstraints::from_iter(vec![
127             &DEVICE_ID,
128             &HEIGHT,
129             &WIDTH,
130             &RESIZE_MODE,
131         ]);
132 
133         let settings = vec![
134             MediaTrackSettings::from_iter([
135                 (&DEVICE_ID, "480p".into()),
136                 (&HEIGHT, 480.into()),
137                 (&WIDTH, 720.into()),
138                 (&RESIZE_MODE, ResizeMode::crop_and_scale().into()),
139             ]),
140             MediaTrackSettings::from_iter([
141                 (&DEVICE_ID, "720p".into()),
142                 (&HEIGHT, 720.into()),
143                 (&WIDTH, 1280.into()),
144                 (&RESIZE_MODE, ResizeMode::crop_and_scale().into()),
145             ]),
146             MediaTrackSettings::from_iter([
147                 (&DEVICE_ID, "1080p".into()),
148                 (&HEIGHT, 1080.into()),
149                 (&WIDTH, 1920.into()),
150                 (&RESIZE_MODE, ResizeMode::none().into()),
151             ]),
152             MediaTrackSettings::from_iter([
153                 (&DEVICE_ID, "1440p".into()),
154                 (&HEIGHT, 1440.into()),
155                 (&WIDTH, 2560.into()),
156                 (&RESIZE_MODE, ResizeMode::none().into()),
157             ]),
158             MediaTrackSettings::from_iter([
159                 (&DEVICE_ID, "2160p".into()),
160                 (&HEIGHT, 2160.into()),
161                 (&WIDTH, 3840.into()),
162                 (&RESIZE_MODE, ResizeMode::none().into()),
163             ]),
164         ];
165 
166         let ideal_settings = vec![
167             MediaTrackSettings::from_iter([(&HEIGHT, 450.into()), (&WIDTH, 700.into())]),
168             MediaTrackSettings::from_iter([(&HEIGHT, 700.into()), (&WIDTH, 1250.into())]),
169             MediaTrackSettings::from_iter([(&HEIGHT, 1000.into()), (&WIDTH, 2000.into())]),
170             MediaTrackSettings::from_iter([(&HEIGHT, 1500.into()), (&WIDTH, 2500.into())]),
171             MediaTrackSettings::from_iter([(&HEIGHT, 2000.into()), (&WIDTH, 3750.into())]),
172         ];
173 
174         for (index, ideal) in ideal_settings.iter().enumerate() {
175             let policy = ClosestToIdealPolicy::new(ideal.clone(), &supported_constraints);
176 
177             let actual = policy.select_candidate(&settings);
178 
179             let expected = &settings[index];
180 
181             assert_eq!(actual, expected);
182         }
183     }
184 }
185