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