atmos/os_lib/web_engine/
media_controls.rs1extern crate alloc;
7
8use alloc::format;
9use alloc::string::String;
10
11#[derive(Debug, Clone, PartialEq)]
13pub struct MediaSourceCandidate {
14 pub src: String,
15 pub type_attr: Option<String>,
16 pub media_attr: Option<String>,
17}
18
19pub fn can_play_type(mime_type: &str) -> &'static str {
24 let raw = mime_type.trim();
25 if raw.is_empty() {
26 return "";
27 }
28
29 let lower = raw.to_ascii_lowercase();
30
31 let mut parts = lower.split(';');
33 let base_mime = parts.next().unwrap_or("").trim();
34 let params_str = parts.next().unwrap_or("").trim();
35
36 let codec_param = if !params_str.is_empty() {
38 let p_trimmed = params_str.trim();
39 if let Some(rest) = p_trimmed.strip_prefix("codecs=") {
40 let val = rest.trim().trim_matches('"').trim_matches('\'').trim();
41 Some(val)
42 } else {
43 None
44 }
45 } else {
46 None
47 };
48
49 match base_mime {
50 "video/mp4" => {
51 if let Some(codec) = codec_param {
52 if codec.starts_with("avc1") || codec.starts_with("mp4v") {
54 "probably"
55 } else if codec.starts_with("mp4a") {
56 "maybe"
58 } else {
59 "" }
61 } else {
62 "maybe"
63 }
64 }
65 "audio/mp4" => {
66 if let Some(codec) = codec_param {
67 if codec.starts_with("mp4a") || codec.starts_with("mp3") {
68 "probably"
69 } else {
70 ""
71 }
72 } else {
73 "maybe"
74 }
75 }
76 "video/webm" => {
77 if let Some(codec) = codec_param {
78 if codec.starts_with("vp8") || codec.starts_with("vorbis") {
79 "probably"
80 } else {
81 ""
82 }
83 } else {
84 "maybe"
85 }
86 }
87 "audio/webm" => {
88 if let Some(codec) = codec_param {
89 if codec.starts_with("vorbis") || codec.starts_with("opus") {
90 "probably"
91 } else {
92 ""
93 }
94 } else {
95 "maybe"
96 }
97 }
98 "audio/wav" | "audio/x-wav" | "audio/wave" => "probably",
99 "audio/mpeg" | "audio/mp3" => "probably",
100 _ => "",
101 }
102}
103
104fn is_supported_media_extension(path: &str, parent_tag: &str) -> bool {
106 let lower = path.to_ascii_lowercase();
107 let ext = lower.rsplit('.').next().unwrap_or("");
108 match ext {
109 "mp4" | "m4v" => true,
110 "webm" => true,
111 "wav" => parent_tag == "audio" || parent_tag == "video",
112 "mp3" => parent_tag == "audio" || parent_tag == "video",
113 _ => false,
114 }
115}
116
117fn evaluate_media_attr(media_query: &str, viewport_width: u32) -> bool {
119 let mq = media_query.trim().to_ascii_lowercase();
120 if mq.is_empty() || mq == "all" || mq == "screen" {
121 return true;
122 }
123
124 if let Some(idx) = mq.find("min-width") {
126 if let Some(colon) = mq.get(idx..).and_then(|s| s.find(':')) {
127 let rest = mq.get(idx + colon + 1..).unwrap_or("");
128 let num_str: String = rest.chars().filter(|c| c.is_ascii_digit()).collect();
129 if let Ok(val) = num_str.parse::<u32>() {
130 if viewport_width < val {
131 return false;
132 }
133 }
134 }
135 }
136
137 if let Some(idx) = mq.find("max-width") {
138 if let Some(colon) = mq.get(idx..).and_then(|s| s.find(':')) {
139 let rest = mq.get(idx + colon + 1..).unwrap_or("");
140 let num_str: String = rest.chars().filter(|c| c.is_ascii_digit()).collect();
141 if let Ok(val) = num_str.parse::<u32>() {
142 if viewport_width > val {
143 return false;
144 }
145 }
146 }
147 }
148
149 true
150}
151
152pub fn select_media_source(
158 parent_tag: &str,
159 parent_src: Option<&str>,
160 sources: &[MediaSourceCandidate],
161 viewport_width: u32,
162 _viewport_height: u32,
163) -> Option<String> {
164 if let Some(src) = parent_src {
165 let trimmed = src.trim();
166 if !trimmed.is_empty() {
167 return Some(String::from(trimmed));
168 }
169 }
170
171 for candidate in sources {
172 let src = candidate.src.trim();
173 if src.is_empty() {
174 continue;
175 }
176
177 if let Some(ref mq) = candidate.media_attr {
179 if !evaluate_media_attr(mq, viewport_width) {
180 continue;
181 }
182 }
183
184 if let Some(ref type_str) = candidate.type_attr {
186 let result = can_play_type(type_str);
187 if result.is_empty() {
188 continue; }
190 } else {
191 if !is_supported_media_extension(src, parent_tag) {
193 continue;
194 }
195 }
196
197 return Some(String::from(src));
198 }
199
200 None
201}
202
203pub fn format_media_time(seconds: f64) -> String {
205 let total_secs = seconds.max(0.0) as u64;
206 let h = total_secs / 3600;
207 let m = (total_secs % 3600) / 60;
208 let s = total_secs % 60;
209 if h > 0 {
210 format!("{:02}:{:02}:{:02}", h, m, s)
211 } else {
212 format!("{:02}:{:02}", m, s)
213 }
214}
215
216#[derive(Debug, Clone, PartialEq)]
218pub struct MediaControlsState {
219 pub current_time: f64,
220 pub duration: f64,
221 pub volume: f32,
222 pub muted: bool,
223 pub paused: bool,
224}
225
226impl MediaControlsState {
227 pub fn new(duration: f64) -> Self {
228 Self {
229 current_time: 0.0,
230 duration: duration.max(0.0),
231 volume: 1.0,
232 muted: false,
233 paused: true,
234 }
235 }
236
237 pub fn progress_ratio(&self) -> f32 {
239 if self.duration <= 0.0 {
240 0.0
241 } else {
242 ((self.current_time / self.duration) as f32).clamp(0.0, 1.0)
243 }
244 }
245
246 pub fn format_display(&self) -> String {
248 format!(
249 "{} / {}",
250 format_media_time(self.current_time),
251 format_media_time(self.duration)
252 )
253 }
254
255 pub fn seek_by_ratio(&mut self, ratio: f32) {
257 let clamped = ratio.clamp(0.0, 1.0) as f64;
258 self.current_time = (self.duration * clamped).clamp(0.0, self.duration);
259 }
260
261 pub fn calculate_seek_time(click_x: i32, bar_x: i32, bar_width: i32, duration: f64) -> f64 {
263 if bar_width <= 0 || duration <= 0.0 {
264 return 0.0;
265 }
266 let offset = (click_x - bar_x).max(0);
267 let ratio = (offset as f64 / bar_width as f64).clamp(0.0, 1.0);
268 (duration * ratio).clamp(0.0, duration)
269 }
270
271 pub fn effective_volume(&self) -> f32 {
273 if self.muted {
274 0.0
275 } else {
276 self.volume
277 }
278 }
279
280 pub fn set_volume(&mut self, vol: f32) {
282 self.volume = vol.clamp(0.0, 1.0);
283 if self.volume == 0.0 {
284 self.muted = true;
285 } else if self.muted {
286 self.muted = false;
287 }
288 }
289
290 pub fn toggle_mute(&mut self) -> bool {
292 self.muted = !self.muted;
293 self.muted
294 }
295
296 pub fn toggle_play(&mut self) -> bool {
298 self.paused = !self.paused;
299 !self.paused
300 }
301}