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