Skip to main content

script/dom/html/
htmlmediaelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::collections::VecDeque;
7use std::rc::Rc;
8use std::sync::{Arc, Mutex, OnceLock, RwLock, Weak};
9use std::time::{Duration, Instant};
10use std::{f64, mem};
11
12use content_security_policy::sandboxing_directive::SandboxingFlagSet;
13use dom_struct::dom_struct;
14use embedder_traits::{MediaPositionState, MediaSessionEvent, MediaSessionPlaybackState};
15use euclid::default::Size2D;
16use headers::{ContentLength, ContentRange, HeaderMapExt};
17use html5ever::{LocalName, Prefix, QualName, local_name, ns};
18use http::StatusCode;
19use http::header::{self, HeaderMap, HeaderValue};
20use js::context::JSContext;
21use js::realm::CurrentRealm;
22use layout_api::MediaFrame;
23use media::{GLPlayerMsg, GLPlayerMsgForward, WindowGLContext};
24use net_traits::request::{Destination, RequestId};
25use net_traits::{
26    CoreResourceThread, FetchMetadata, FilteredMetadata, NetworkError, ResourceFetchTiming,
27};
28use paint_api::{CrossProcessPaintApi, ImageUpdate, SerializableImageData};
29use pixels::RasterImage;
30use script_bindings::assert::assert_in_script;
31use script_bindings::cell::DomRefCell;
32use script_bindings::codegen::InheritTypes::{
33    ElementTypeId, HTMLElementTypeId, HTMLMediaElementTypeId, NodeTypeId,
34};
35use script_bindings::weakref::WeakRef;
36use servo_base::generic_channel::{self, GenericCallback, GenericSharedMemory};
37use servo_base::id::WebViewId;
38use servo_config::pref;
39use servo_media::player::audio::AudioRenderer;
40use servo_media::player::video::{VideoFrame, VideoFrameRenderer};
41use servo_media::player::{PlaybackState, Player, PlayerError, PlayerEvent, SeekLock, StreamType};
42use servo_media::{ClientContextId, ServoMedia, SupportsMediaType};
43use servo_url::ServoUrl;
44use stylo_atoms::Atom;
45use uuid::Uuid;
46use webrender_api::{
47    ExternalImageData, ExternalImageId, ExternalImageType, ImageBufferKind, ImageDescriptor,
48    ImageDescriptorFlags, ImageFormat, ImageKey,
49};
50
51use crate::document_loader::{LoadBlocker, LoadType};
52use crate::dom::audio::audiotrack::AudioTrack;
53use crate::dom::audio::audiotracklist::AudioTrackList;
54use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::{
55    CanPlayTypeResult, HTMLMediaElementConstants, HTMLMediaElementMethods,
56};
57use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*;
58use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods;
59use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
60use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
61use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{
62    TextTrackKind, TextTrackMethods, TextTrackMode,
63};
64use crate::dom::bindings::codegen::Bindings::URLBinding::URLMethods;
65use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
66use crate::dom::bindings::codegen::UnionTypes::{
67    MediaStreamOrBlob, VideoTrackOrAudioTrackOrTextTrack,
68};
69use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
70use crate::dom::bindings::inheritance::Castable;
71use crate::dom::bindings::num::Finite;
72use crate::dom::bindings::refcounted::Trusted;
73use crate::dom::bindings::reflector::DomGlobal;
74use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom, UnrootedDom};
75use crate::dom::bindings::str::{DOMString, USVString};
76use crate::dom::blob::Blob;
77use crate::dom::csp::{GlobalCspReporting, Violation};
78use crate::dom::document::Document;
79use crate::dom::element::attributes::storage::AttrRef;
80use crate::dom::element::{
81    AttributeMutation, AttributeMutationReason, CustomElementCreationMode, Element, ElementCreator,
82    cors_setting_for_element, reflect_cross_origin_attribute, set_cross_origin_attribute,
83};
84use crate::dom::event::Event;
85use crate::dom::eventtarget::EventTarget;
86use crate::dom::globalscope::GlobalScope;
87use crate::dom::html::htmlelement::HTMLElement;
88use crate::dom::html::htmlsourceelement::HTMLSourceElement;
89use crate::dom::html::htmlvideoelement::HTMLVideoElement;
90use crate::dom::mediaerror::MediaError;
91use crate::dom::mediafragmentparser::MediaFragmentParser;
92use crate::dom::medialist::MediaList;
93use crate::dom::mediastream::MediaStream;
94use crate::dom::node::virtualmethods::VirtualMethods;
95use crate::dom::node::{Node, NodeDamage, NodeTraits, UnbindContext};
96use crate::dom::performance::performanceresourcetiming::InitiatorType;
97use crate::dom::promise::Promise;
98use crate::dom::referrer_policy_for_element;
99use crate::dom::texttrack::TextTrack;
100use crate::dom::texttracklist::TextTrackList;
101use crate::dom::timeranges::{TimeRanges, TimeRangesContainer};
102use crate::dom::trackevent::TrackEvent;
103use crate::dom::url::URL;
104use crate::dom::videotrack::VideoTrack;
105use crate::dom::videotracklist::VideoTrackList;
106use crate::fetch::{FetchCanceller, RequestWithGlobalScope, create_a_potential_cors_request};
107use crate::microtask::MicrotaskRunnable;
108use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
109use crate::realms::enter_auto_realm;
110use crate::script_thread::ScriptThread;
111use crate::task_source::SendableTaskSource;
112
113/// A CSS file to style the media controls.
114static MEDIA_CONTROL_CSS: &str = include_str!("../../resources/media-controls.css");
115
116/// A JS file to control the media controls.
117static MEDIA_CONTROL_JS: &str = include_str!("../../resources/media-controls.js");
118
119/// The media engine may report a seek-done position that differs slightly from the
120/// requested position (e.g. snapping to the nearest keyframe), so we use a threshold
121/// instead of strict equality. (Unit is second)
122const SEEK_POSITION_THRESHOLD: f64 = 0.5;
123
124#[derive(MallocSizeOf, PartialEq)]
125enum FrameStatus {
126    Locked,
127    Unlocked,
128}
129
130#[derive(MallocSizeOf)]
131struct FrameHolder(
132    FrameStatus,
133    #[ignore_malloc_size_of = "defined in servo-media"] VideoFrame,
134);
135
136impl FrameHolder {
137    fn new(frame: VideoFrame) -> FrameHolder {
138        FrameHolder(FrameStatus::Unlocked, frame)
139    }
140
141    fn lock(&mut self) {
142        if self.0 == FrameStatus::Unlocked {
143            self.0 = FrameStatus::Locked;
144        };
145    }
146
147    fn unlock(&mut self) {
148        if self.0 == FrameStatus::Locked {
149            self.0 = FrameStatus::Unlocked;
150        };
151    }
152
153    fn set(&mut self, new_frame: VideoFrame) {
154        if self.0 == FrameStatus::Unlocked {
155            self.1 = new_frame
156        };
157    }
158
159    fn get(&self) -> (u32, Size2D<i32>, usize) {
160        if self.0 == FrameStatus::Locked {
161            (
162                self.1.get_texture_id(),
163                Size2D::new(self.1.get_width(), self.1.get_height()),
164                0,
165            )
166        } else {
167            unreachable!();
168        }
169    }
170
171    fn get_frame(&self) -> VideoFrame {
172        self.1.clone()
173    }
174}
175
176#[derive(MallocSizeOf)]
177pub(crate) struct MediaFrameRenderer {
178    webview_id: WebViewId,
179    player_id: Option<usize>,
180    #[conditional_malloc_size_of]
181    glplayer_id: Arc<RwLock<Option<u64>>>,
182    paint_api: CrossProcessPaintApi,
183    #[ignore_malloc_size_of = "Defined in other crates"]
184    player_context: WindowGLContext,
185    current_frame: Option<MediaFrame>,
186    old_frame: Option<ImageKey>,
187    very_old_frame: Option<ImageKey>,
188    current_frame_holder: Option<FrameHolder>,
189    /// <https://html.spec.whatwg.org/multipage/#poster-frame>
190    poster_frame: Option<MediaFrame>,
191}
192
193impl MediaFrameRenderer {
194    fn new(
195        webview_id: WebViewId,
196        paint_api: CrossProcessPaintApi,
197        player_context: WindowGLContext,
198    ) -> Self {
199        Self {
200            webview_id,
201            player_id: None,
202            glplayer_id: Arc::new(RwLock::new(None)),
203            paint_api,
204            player_context,
205            current_frame: None,
206            old_frame: None,
207            very_old_frame: None,
208            current_frame_holder: None,
209            poster_frame: None,
210        }
211    }
212
213    fn setup(
214        &mut self,
215        player_id: usize,
216        task_source: SendableTaskSource,
217        weak_video_renderer: Weak<Mutex<MediaFrameRenderer>>,
218    ) {
219        self.player_id = Some(player_id);
220        let glplayer_id = self.glplayer_id.clone();
221        let callback = GenericCallback::new(move |message| {
222            let message = message.unwrap();
223            let weak_video_renderer = weak_video_renderer.clone();
224
225            let glplayer_id = glplayer_id.clone();
226            task_source.queue(task!(handle_glplayer_message: move || {
227                trace!("GLPlayer message {:?}", message);
228
229                let Some(video_renderer) = weak_video_renderer.upgrade() else {
230                    return;
231                };
232
233                match message {
234                    GLPlayerMsgForward::Lock(sender) => {
235                        if let Some(holder) = video_renderer
236                            .lock()
237                            .unwrap()
238                            .current_frame_holder
239                            .as_mut() {
240                                holder.lock();
241                                sender.send(holder.get()).unwrap();
242                            };
243                    },
244                    GLPlayerMsgForward::Unlock() => {
245                        if let Some(holder) = video_renderer
246                            .lock()
247                            .unwrap()
248                            .current_frame_holder
249                            .as_mut() { holder.unlock() }
250                    },
251                    GLPlayerMsgForward::PlayerId(id) => {
252                        let mut glplayer_id = glplayer_id.write().unwrap();
253                        if let Some(already_set_id) = *glplayer_id {
254                            error!("Player id already set to {already_set_id} will be replaced with {id}");
255                        }
256                        *glplayer_id = Some(id);
257                    },
258                }
259            }));
260        })
261        .expect("Could not create callback");
262
263        if let Some(glplayer_thread_sender) = &self.player_context.glplayer_thread_sender {
264            glplayer_thread_sender
265                .send(GLPlayerMsg::RegisterPlayer(callback))
266                .unwrap();
267        }
268    }
269
270    fn reset(&mut self) {
271        self.player_id = None;
272
273        if let Some(glplayer_id) = self.glplayer_id.write().unwrap().take() {
274            self.player_context
275                .send(GLPlayerMsg::UnregisterPlayer(glplayer_id));
276        }
277
278        self.current_frame_holder = None;
279
280        let mut updates = smallvec::smallvec![];
281
282        if let Some(current_frame) = self.current_frame.take() {
283            updates.push(ImageUpdate::DeleteImage(current_frame.image_key));
284        }
285
286        if let Some(old_image_key) = self.old_frame.take() {
287            updates.push(ImageUpdate::DeleteImage(old_image_key));
288        }
289
290        if let Some(very_old_image_key) = self.very_old_frame.take() {
291            updates.push(ImageUpdate::DeleteImage(very_old_image_key));
292        }
293
294        if !updates.is_empty() {
295            self.paint_api
296                .update_images(self.webview_id.into(), updates);
297        }
298    }
299
300    fn set_poster_frame(&mut self, image: Option<Arc<RasterImage>>) {
301        self.poster_frame = image.and_then(|image| {
302            image.id.map(|image_key| MediaFrame {
303                image_key,
304                width: image.metadata.width as i32,
305                height: image.metadata.height as i32,
306            })
307        });
308    }
309}
310
311impl Drop for MediaFrameRenderer {
312    fn drop(&mut self) {
313        self.reset();
314    }
315}
316
317impl VideoFrameRenderer for MediaFrameRenderer {
318    fn render(&mut self, frame: VideoFrame) {
319        if self.player_id.is_none() ||
320            (frame.is_gl_texture() && self.glplayer_id.read().unwrap().is_none())
321        {
322            return;
323        }
324
325        let mut updates = smallvec::smallvec![];
326
327        if let Some(old_image_key) = mem::replace(&mut self.very_old_frame, self.old_frame.take()) {
328            updates.push(ImageUpdate::DeleteImage(old_image_key));
329        }
330
331        let descriptor = ImageDescriptor::new(
332            frame.get_width(),
333            frame.get_height(),
334            ImageFormat::BGRA8,
335            ImageDescriptorFlags::empty(),
336        );
337
338        match &mut self.current_frame {
339            Some(current_frame)
340                if current_frame.width == frame.get_width() &&
341                    current_frame.height == frame.get_height() =>
342            {
343                if !frame.is_gl_texture() {
344                    updates.push(ImageUpdate::UpdateImage(
345                        current_frame.image_key,
346                        descriptor,
347                        SerializableImageData::Raw(GenericSharedMemory::from_arc_vec(
348                            frame.get_data(),
349                        )),
350                        None,
351                    ));
352                }
353
354                self.current_frame_holder
355                    .get_or_insert_with(|| FrameHolder::new(frame.clone()))
356                    .set(frame);
357
358                if let Some(old_image_key) = self.old_frame.take() {
359                    updates.push(ImageUpdate::DeleteImage(old_image_key));
360                }
361            },
362            Some(current_frame) => {
363                self.old_frame = Some(current_frame.image_key);
364
365                let Some(new_image_key) =
366                    self.paint_api.generate_image_key_blocking(self.webview_id)
367                else {
368                    return;
369                };
370
371                /* update current_frame */
372                current_frame.image_key = new_image_key;
373                current_frame.width = frame.get_width();
374                current_frame.height = frame.get_height();
375
376                // FIXME: This code is duplicated below this branch
377                let image_data = self
378                    .glplayer_id
379                    .read()
380                    .unwrap()
381                    .filter(|_| frame.is_gl_texture())
382                    .map(|glplayer_id| {
383                        let texture_target = if frame.is_external_oes() {
384                            ImageBufferKind::TextureExternal
385                        } else {
386                            ImageBufferKind::Texture2D
387                        };
388
389                        SerializableImageData::External(ExternalImageData {
390                            id: ExternalImageId(glplayer_id),
391                            channel_index: 0,
392                            image_type: ExternalImageType::TextureHandle(texture_target),
393                            normalized_uvs: false,
394                        })
395                    })
396                    .unwrap_or_else(|| {
397                        SerializableImageData::Raw(GenericSharedMemory::from_arc_vec(
398                            frame.get_data(),
399                        ))
400                    });
401
402                self.current_frame_holder
403                    .get_or_insert_with(|| FrameHolder::new(frame.clone()))
404                    .set(frame);
405
406                updates.push(ImageUpdate::AddImage(
407                    new_image_key,
408                    descriptor,
409                    image_data,
410                    false,
411                ));
412            },
413            None => {
414                let Some(image_key) = self.paint_api.generate_image_key_blocking(self.webview_id)
415                else {
416                    return;
417                };
418
419                self.current_frame = Some(MediaFrame {
420                    image_key,
421                    width: frame.get_width(),
422                    height: frame.get_height(),
423                });
424
425                let image_data = self
426                    .glplayer_id
427                    .read()
428                    .unwrap()
429                    .filter(|_| frame.is_gl_texture())
430                    .map(|glplayer_id| {
431                        let texture_target = if frame.is_external_oes() {
432                            ImageBufferKind::TextureExternal
433                        } else {
434                            ImageBufferKind::Texture2D
435                        };
436
437                        SerializableImageData::External(ExternalImageData {
438                            id: ExternalImageId(glplayer_id),
439                            channel_index: 0,
440                            image_type: ExternalImageType::TextureHandle(texture_target),
441                            normalized_uvs: false,
442                        })
443                    })
444                    .unwrap_or_else(|| {
445                        SerializableImageData::Raw(GenericSharedMemory::from_arc_vec(
446                            frame.get_data(),
447                        ))
448                    });
449
450                self.current_frame_holder = Some(FrameHolder::new(frame));
451
452                updates.push(ImageUpdate::AddImage(
453                    image_key, descriptor, image_data, false,
454                ));
455            },
456        }
457        self.paint_api
458            .update_images(self.webview_id.into(), updates);
459    }
460}
461
462#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
463#[derive(JSTraceable, MallocSizeOf)]
464enum SrcObject {
465    MediaStream(Dom<MediaStream>),
466    Blob(Dom<Blob>),
467}
468
469impl From<MediaStreamOrBlob> for SrcObject {
470    fn from(src_object: MediaStreamOrBlob) -> SrcObject {
471        match src_object {
472            MediaStreamOrBlob::Blob(blob) => SrcObject::Blob(Dom::from_ref(&*blob)),
473            MediaStreamOrBlob::MediaStream(stream) => {
474                SrcObject::MediaStream(Dom::from_ref(&*stream))
475            },
476        }
477    }
478}
479
480#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
481enum LoadState {
482    NotLoaded,
483    LoadingFromSrcObject,
484    LoadingFromSrcAttribute,
485    LoadingFromSourceChild,
486    WaitingForSource,
487}
488
489/// <https://html.spec.whatwg.org/multipage/#loading-the-media-resource:media-element-29>
490#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
491#[derive(JSTraceable, MallocSizeOf)]
492struct SourceChildrenPointer {
493    source_before_pointer: Dom<HTMLSourceElement>,
494    inclusive: bool,
495}
496
497impl SourceChildrenPointer {
498    fn new(source_before_pointer: &HTMLSourceElement, inclusive: bool) -> Self {
499        Self {
500            source_before_pointer: Dom::from_ref(source_before_pointer),
501            inclusive,
502        }
503    }
504}
505
506/// Generally the presence of the loop attribute should be considered to mean playback has not
507/// "ended", as "ended" and "looping" are mutually exclusive.
508/// <https://html.spec.whatwg.org/multipage/#ended-playback>
509#[derive(Clone, Copy, Debug, PartialEq)]
510enum LoopCondition {
511    Included,
512    Ignored,
513}
514
515#[dom_struct]
516pub(crate) struct HTMLMediaElement {
517    htmlelement: HTMLElement,
518    /// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate>
519    network_state: Cell<NetworkState>,
520    /// <https://html.spec.whatwg.org/multipage/#dom-media-readystate>
521    ready_state: Cell<ReadyState>,
522    /// <https://html.spec.whatwg.org/multipage/#dom-media-srcobject>
523    src_object: DomRefCell<Option<SrcObject>>,
524    /// <https://html.spec.whatwg.org/multipage/#dom-media-currentsrc>
525    current_src: DomRefCell<String>,
526    /// Incremented whenever tasks associated with this element are cancelled.
527    generation_id: Cell<u32>,
528    /// <https://html.spec.whatwg.org/multipage/#fire-loadeddata>
529    ///
530    /// Reset to false every time the load algorithm is invoked.
531    fired_loadeddata_event: Cell<bool>,
532    /// <https://html.spec.whatwg.org/multipage/#dom-media-error>
533    error: MutNullableDom<MediaError>,
534    /// <https://html.spec.whatwg.org/multipage/#dom-media-paused>
535    paused: Cell<bool>,
536    /// <https://html.spec.whatwg.org/multipage/#dom-media-defaultplaybackrate>
537    default_playback_rate: Cell<f64>,
538    /// <https://html.spec.whatwg.org/multipage/#dom-media-playbackrate>
539    playback_rate: Cell<f64>,
540    /// <https://html.spec.whatwg.org/multipage/#attr-media-autoplay>
541    autoplaying: Cell<bool>,
542    /// <https://html.spec.whatwg.org/multipage/#delaying-the-load-event-flag>
543    delaying_the_load_event_flag: DomRefCell<Option<LoadBlocker>>,
544    /// <https://html.spec.whatwg.org/multipage/#list-of-pending-play-promises>
545    #[conditional_malloc_size_of]
546    pending_play_promises: DomRefCell<Vec<Rc<Promise>>>,
547    /// Play promises which are soon to be fulfilled by a queued task.
548    #[expect(clippy::type_complexity)]
549    #[conditional_malloc_size_of]
550    in_flight_play_promises_queue: DomRefCell<VecDeque<(Box<[Rc<Promise>]>, ErrorResult)>>,
551    #[ignore_malloc_size_of = "servo_media"]
552    #[no_trace]
553    player: DomRefCell<Option<Arc<Mutex<dyn Player>>>>,
554    #[conditional_malloc_size_of]
555    #[no_trace]
556    video_renderer: Arc<Mutex<MediaFrameRenderer>>,
557    #[ignore_malloc_size_of = "servo_media"]
558    #[no_trace]
559    audio_renderer: DomRefCell<Option<Arc<Mutex<dyn AudioRenderer>>>>,
560    #[conditional_malloc_size_of]
561    #[no_trace]
562    event_handler: RefCell<Option<Arc<Mutex<HTMLMediaElementEventHandler>>>>,
563    /// <https://html.spec.whatwg.org/multipage/#show-poster-flag>
564    show_poster: Cell<bool>,
565    /// <https://html.spec.whatwg.org/multipage/#dom-media-duration>
566    duration: Cell<f64>,
567    /// <https://html.spec.whatwg.org/multipage/#current-playback-position>
568    current_playback_position: Cell<f64>,
569    /// <https://html.spec.whatwg.org/multipage/#official-playback-position>
570    official_playback_position: Cell<f64>,
571    /// <https://html.spec.whatwg.org/multipage/#default-playback-start-position>
572    default_playback_start_position: Cell<f64>,
573    /// <https://html.spec.whatwg.org/multipage/#dom-media-volume>
574    volume: Cell<f64>,
575    /// <https://html.spec.whatwg.org/multipage/#dom-media-seeking>
576    seeking: Cell<bool>,
577    /// The latest seek position (in seconds) is used to distinguish whether the seek request was
578    /// initiated by a script or by the user agent itself, rather than by the media engine and to
579    /// abort other running instance of the `seek` algorithm.
580    current_seek_position: Cell<f64>,
581    /// <https://html.spec.whatwg.org/multipage/#dom-media-muted>
582    muted: Cell<bool>,
583    /// Loading state from source, if any.
584    load_state: Cell<LoadState>,
585    source_children_pointer: DomRefCell<Option<SourceChildrenPointer>>,
586    current_source_child: MutNullableDom<HTMLSourceElement>,
587    /// URL of the media resource, if any.
588    #[no_trace]
589    resource_url: DomRefCell<Option<ServoUrl>>,
590    /// URL of the media resource, if the resource is set through the src_object attribute and it
591    /// is a blob.
592    #[no_trace]
593    blob_url: DomRefCell<Option<ServoUrl>>,
594    /// <https://html.spec.whatwg.org/multipage/#dom-media-played>
595    played: DomRefCell<TimeRangesContainer>,
596    // https://html.spec.whatwg.org/multipage/#dom-media-audiotracks
597    audio_tracks_list: MutNullableDom<AudioTrackList>,
598    // https://html.spec.whatwg.org/multipage/#dom-media-videotracks
599    video_tracks_list: MutNullableDom<VideoTrackList>,
600    /// <https://html.spec.whatwg.org/multipage/#list-of-text-tracks>
601    text_tracks_list: MutNullableDom<TextTrackList>,
602    /// Time of last timeupdate notification.
603    #[ignore_malloc_size_of = "Defined in std::time"]
604    next_timeupdate_event: Cell<Instant>,
605    /// Latest fetch request context.
606    current_fetch_context: RefCell<Option<HTMLMediaElementFetchContext>>,
607    /// Media controls id.
608    /// In order to workaround the lack of privileged JS context, we secure the
609    /// the access to the "privileged" document.servoGetMediaControls(id) API by
610    /// keeping a whitelist of media controls identifiers.
611    media_controls_id: DomRefCell<Option<String>>,
612    /// <https://html.spec.whatwg.org/multipage/#did-perform-automatic-track-selection>
613    did_perform_automatic_track_selection: Cell<bool>,
614}
615
616/// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate>
617#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
618#[repr(u8)]
619pub(crate) enum NetworkState {
620    Empty = HTMLMediaElementConstants::NETWORK_EMPTY as u8,
621    Idle = HTMLMediaElementConstants::NETWORK_IDLE as u8,
622    Loading = HTMLMediaElementConstants::NETWORK_LOADING as u8,
623    NoSource = HTMLMediaElementConstants::NETWORK_NO_SOURCE as u8,
624}
625
626/// <https://html.spec.whatwg.org/multipage/#dom-media-readystate>
627#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
628#[repr(u8)]
629#[expect(clippy::enum_variant_names)] // Clippy warning silenced here because these names are from the specification.
630pub(crate) enum ReadyState {
631    HaveNothing = HTMLMediaElementConstants::HAVE_NOTHING as u8,
632    HaveMetadata = HTMLMediaElementConstants::HAVE_METADATA as u8,
633    HaveCurrentData = HTMLMediaElementConstants::HAVE_CURRENT_DATA as u8,
634    HaveFutureData = HTMLMediaElementConstants::HAVE_FUTURE_DATA as u8,
635    HaveEnoughData = HTMLMediaElementConstants::HAVE_ENOUGH_DATA as u8,
636}
637
638/// <https://html.spec.whatwg.org/multipage/#direction-of-playback>
639#[derive(Clone, Copy, PartialEq)]
640enum PlaybackDirection {
641    Forwards,
642    Backwards,
643}
644
645impl HTMLMediaElement {
646    pub(crate) fn new_inherited(
647        tag_name: LocalName,
648        prefix: Option<Prefix>,
649        document: &Document,
650    ) -> Self {
651        Self {
652            htmlelement: HTMLElement::new_inherited(tag_name, prefix, document),
653            network_state: Cell::new(NetworkState::Empty),
654            ready_state: Cell::new(ReadyState::HaveNothing),
655            src_object: Default::default(),
656            current_src: DomRefCell::new("".to_owned()),
657            generation_id: Cell::new(0),
658            fired_loadeddata_event: Cell::new(false),
659            error: Default::default(),
660            paused: Cell::new(true),
661            default_playback_rate: Cell::new(1.0),
662            playback_rate: Cell::new(1.0),
663            muted: Cell::new(false),
664            load_state: Cell::new(LoadState::NotLoaded),
665            source_children_pointer: DomRefCell::new(None),
666            current_source_child: Default::default(),
667            // FIXME(nox): Why is this initialised to true?
668            autoplaying: Cell::new(true),
669            delaying_the_load_event_flag: Default::default(),
670            pending_play_promises: Default::default(),
671            in_flight_play_promises_queue: Default::default(),
672            player: Default::default(),
673            video_renderer: Arc::new(Mutex::new(MediaFrameRenderer::new(
674                document.webview_id(),
675                document.window().paint_api().clone(),
676                document.window().get_player_context(),
677            ))),
678            audio_renderer: Default::default(),
679            event_handler: Default::default(),
680            show_poster: Cell::new(true),
681            duration: Cell::new(f64::NAN),
682            current_playback_position: Cell::new(0.),
683            official_playback_position: Cell::new(0.),
684            default_playback_start_position: Cell::new(0.),
685            volume: Cell::new(1.0),
686            seeking: Cell::new(false),
687            current_seek_position: Cell::new(f64::NAN),
688            resource_url: DomRefCell::new(None),
689            blob_url: DomRefCell::new(None),
690            played: DomRefCell::new(TimeRangesContainer::default()),
691            audio_tracks_list: Default::default(),
692            video_tracks_list: Default::default(),
693            text_tracks_list: Default::default(),
694            next_timeupdate_event: Cell::new(Instant::now() + Duration::from_millis(250)),
695            current_fetch_context: RefCell::new(None),
696            media_controls_id: DomRefCell::new(None),
697            did_perform_automatic_track_selection: Default::default(),
698        }
699    }
700
701    pub(crate) fn network_state(&self) -> NetworkState {
702        self.network_state.get()
703    }
704
705    pub(crate) fn get_ready_state(&self) -> ReadyState {
706        self.ready_state.get()
707    }
708
709    fn media_type_id(&self) -> HTMLMediaElementTypeId {
710        match self.upcast::<Node>().type_id() {
711            NodeTypeId::Element(ElementTypeId::HTMLElement(
712                HTMLElementTypeId::HTMLMediaElement(media_type_id),
713            )) => media_type_id,
714            _ => unreachable!(),
715        }
716    }
717
718    fn update_media_state(&self) {
719        let is_playing = self
720            .player
721            .borrow()
722            .as_ref()
723            .is_some_and(|player| !player.lock().unwrap().paused());
724
725        if self.is_potentially_playing() && !is_playing {
726            if let Some(ref player) = *self.player.borrow() {
727                let player = player.lock().unwrap();
728
729                if let Err(error) = player.set_playback_rate(self.playback_rate.get()) {
730                    warn!("Could not set the playback rate: {error:?}");
731                }
732                if let Err(error) = player.set_volume(self.volume.get()) {
733                    warn!("Could not set the volume: {error:?}");
734                }
735                if let Err(error) = player.play() {
736                    error!("Could not play media: {error:?}");
737                }
738            }
739        } else if is_playing &&
740            let Some(ref player) = *self.player.borrow() &&
741            let Err(error) = player.lock().unwrap().pause()
742        {
743            error!("Could not pause player: {error:?}");
744        }
745    }
746
747    /// Marks that element as delaying the load event or not.
748    ///
749    /// Nothing happens if the element was already delaying the load event and
750    /// we pass true to that method again.
751    ///
752    /// <https://html.spec.whatwg.org/multipage/#delaying-the-load-event-flag>
753    pub(crate) fn delay_load_event(&self, delay: bool, cx: &mut JSContext) {
754        let blocker = &self.delaying_the_load_event_flag;
755
756        if delay {
757            if blocker.borrow().is_none() {
758                *blocker.borrow_mut() =
759                    Some(LoadBlocker::new(&self.owner_document(), LoadType::Media));
760            }
761        } else {
762            LoadBlocker::terminate(blocker, cx);
763        }
764    }
765
766    /// <https://html.spec.whatwg.org/multipage/#time-marches-on>
767    fn time_marches_on(&self) {
768        // Step 6. If the time was reached through the usual monotonic increase of the current
769        // playback position during normal playback, and if the user agent has not fired a
770        // timeupdate event at the element in the past 15 to 250ms and is not still running event
771        // handlers for such an event, then the user agent must queue a media element task given the
772        // media element to fire an event named timeupdate at the element.
773        if Instant::now() > self.next_timeupdate_event.get() {
774            self.queue_media_element_task_to_fire_event(atom!("timeupdate"));
775            self.next_timeupdate_event
776                .set(Instant::now() + Duration::from_millis(250));
777        }
778    }
779
780    /// <https://html.spec.whatwg.org/multipage/#internal-play-steps>
781    fn internal_play_steps(&self, cx: &mut JSContext) {
782        // Step 1. If the media element's networkState attribute has the value NETWORK_EMPTY, invoke
783        // the media element's resource selection algorithm.
784        if self.network_state.get() == NetworkState::Empty {
785            self.invoke_resource_selection_algorithm(cx);
786        }
787
788        // Step 2. If the playback has ended and the direction of playback is forwards, seek to the
789        // earliest possible position of the media resource.
790        // Generally "ended" and "looping" are exclusive. Here, the loop attribute is ignored to
791        // seek back to start in case loop was set after playback ended.
792        // <https://github.com/whatwg/html/issues/4487>
793        if self.ended_playback(LoopCondition::Ignored) &&
794            self.direction_of_playback() == PlaybackDirection::Forwards
795        {
796            self.seek(
797                self.earliest_possible_position(),
798                /* approximate_for_speed */ false,
799            );
800        }
801
802        let state = self.ready_state.get();
803
804        // Step 3. If the media element's paused attribute is true, then:
805        if self.Paused() {
806            // Step 3.1. Change the value of paused to false.
807            self.paused.set(false);
808
809            // Step 3.2. If the show poster flag is true, set the element's show poster flag to
810            // false and run the time marches on steps.
811            if self.show_poster.get() {
812                self.show_poster.set(false);
813                self.time_marches_on();
814            }
815
816            // Step 3.3. Queue a media element task given the media element to fire an event named
817            // play at the element.
818            self.queue_media_element_task_to_fire_event(atom!("play"));
819
820            // Step 3.4. If the media element's readyState attribute has the value HAVE_NOTHING,
821            // HAVE_METADATA, or HAVE_CURRENT_DATA, queue a media element task given the media
822            // element to fire an event named waiting at the element. Otherwise, the media element's
823            // readyState attribute has the value HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: notify about
824            // playing for the element.
825            match state {
826                ReadyState::HaveNothing |
827                ReadyState::HaveMetadata |
828                ReadyState::HaveCurrentData => {
829                    self.queue_media_element_task_to_fire_event(atom!("waiting"));
830                },
831                ReadyState::HaveFutureData | ReadyState::HaveEnoughData => {
832                    self.notify_about_playing();
833                },
834            }
835        }
836        // Step 4. Otherwise, if the media element's readyState attribute has the value
837        // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and queue a media
838        // element task given the media element to resolve pending play promises with the
839        // result.
840        else if state == ReadyState::HaveFutureData || state == ReadyState::HaveEnoughData {
841            self.take_pending_play_promises(Ok(()));
842
843            let this = Trusted::new(self);
844            let generation_id = self.generation_id.get();
845
846            self.owner_global()
847                .task_manager()
848                .media_element_task_source()
849                .queue(task!(resolve_pending_play_promises: move |cx| {
850                    let this = this.root();
851                    if generation_id != this.generation_id.get() {
852                        return;
853                    }
854
855                    this.fulfill_in_flight_play_promises(cx, |_| {});
856                }));
857        }
858
859        // Step 5. Set the media element's can autoplay flag to false.
860        self.autoplaying.set(false);
861
862        self.update_media_state();
863    }
864
865    /// <https://html.spec.whatwg.org/multipage/#internal-pause-steps>
866    fn internal_pause_steps(&self) {
867        // Step 1. Set the media element's can autoplay flag to false.
868        self.autoplaying.set(false);
869
870        // Step 2. If the media element's paused attribute is false, run the following steps:
871        if !self.Paused() {
872            // Step 2.1. Change the value of paused to true.
873            self.paused.set(true);
874
875            // Step 2.2. Take pending play promises and let promises be the result.
876            self.take_pending_play_promises(Err(Error::Abort(None)));
877
878            // Step 2.3. Queue a media element task given the media element and the following steps:
879            let this = Trusted::new(self);
880            let generation_id = self.generation_id.get();
881
882            self.owner_global()
883                .task_manager()
884                .media_element_task_source()
885                .queue(task!(internal_pause_steps: move |cx| {
886                    let this = this.root();
887                    if generation_id != this.generation_id.get() {
888                        return;
889                    }
890
891                    this.fulfill_in_flight_play_promises(cx, |cx| {
892                        // Step 2.3.1. Fire an event named timeupdate at the element.
893                        this.upcast::<EventTarget>().fire_event(cx, atom!("timeupdate"));
894
895                        // Step 2.3.2. Fire an event named pause at the element.
896                        this.upcast::<EventTarget>().fire_event(cx, atom!("pause"));
897
898                        // Step 2.3.3. Reject pending play promises with promises and an
899                        // "AbortError" DOMException.
900                        // Done after running this closure in `fulfill_in_flight_play_promises`.
901                    });
902                }));
903
904            // Step 2.4. Set the official playback position to the current playback position.
905            self.official_playback_position
906                .set(self.current_playback_position.get());
907        }
908
909        self.update_media_state();
910    }
911
912    /// <https://html.spec.whatwg.org/multipage/#allowed-to-play>
913    fn is_allowed_to_play(&self) -> bool {
914        true
915    }
916
917    /// <https://html.spec.whatwg.org/multipage/#notify-about-playing>
918    fn notify_about_playing(&self) {
919        // Step 1. Take pending play promises and let promises be the result.
920        self.take_pending_play_promises(Ok(()));
921
922        // Step 2. Queue a media element task given the element and the following steps:
923        let this = Trusted::new(self);
924        let generation_id = self.generation_id.get();
925
926        self.owner_global()
927            .task_manager()
928            .media_element_task_source()
929            .queue(task!(notify_about_playing: move |cx| {
930                let this = this.root();
931                if generation_id != this.generation_id.get() {
932                    return;
933                }
934
935                this.fulfill_in_flight_play_promises(cx, |cx| {
936                    // Step 2.1. Fire an event named playing at the element.
937                    this.upcast::<EventTarget>().fire_event(cx, atom!("playing"));
938
939                    // Step 2.2. Resolve pending play promises with promises.
940                    // Done after running this closure in `fulfill_in_flight_play_promises`.
941                });
942            }));
943    }
944
945    /// <https://html.spec.whatwg.org/multipage/#ready-states>
946    #[expect(
947        clippy::collapsible_match,
948        reason = "This way follows the spec more closely"
949    )]
950    fn change_ready_state(&self, ready_state: ReadyState) {
951        let old_ready_state = self.ready_state.get();
952        self.ready_state.set(ready_state);
953
954        if self.network_state.get() == NetworkState::Empty {
955            return;
956        }
957
958        if old_ready_state == ready_state {
959            return;
960        }
961
962        // Step 1. Apply the first applicable set of substeps from the following list:
963        match (old_ready_state, ready_state) {
964            // => "If the previous ready state was HAVE_NOTHING, and the new ready state is
965            // HAVE_METADATA"
966            (ReadyState::HaveNothing, ReadyState::HaveMetadata) => {
967                // Queue a media element task given the media element to fire an event named
968                // loadedmetadata at the element.
969                self.queue_media_element_task_to_fire_event(atom!("loadedmetadata"));
970                // No other steps are applicable in this case.
971                return;
972            },
973            // => "If the previous ready state was HAVE_METADATA and the new ready state is
974            // HAVE_CURRENT_DATA or greater"
975            (ReadyState::HaveMetadata, new) if new >= ReadyState::HaveCurrentData => {
976                // If this is the first time this occurs for this media element since the load()
977                // algorithm was last invoked, the user agent must queue a media element task given
978                // the media element to fire an event named loadeddata at the element.
979                if !self.fired_loadeddata_event.get() {
980                    self.fired_loadeddata_event.set(true);
981
982                    let this = Trusted::new(self);
983                    let generation_id = self.generation_id.get();
984
985                    self.owner_global()
986                        .task_manager()
987                        .media_element_task_source()
988                        .queue(task!(media_reached_current_data: move |cx| {
989                            let this = this.root();
990                            if generation_id != this.generation_id.get() {
991                                return;
992                            }
993
994                            this.upcast::<EventTarget>().fire_event(cx, atom!("loadeddata"));
995                            // Once the readyState attribute reaches HAVE_CURRENT_DATA, after the
996                            // loadeddata event has been fired, set the element's
997                            // delaying-the-load-event flag to false.
998                            this.delay_load_event(false, cx);
999                        }));
1000                }
1001
1002                // Steps for the transition from HaveMetadata to HaveCurrentData
1003                // or HaveFutureData also apply here, as per the next match
1004                // expression.
1005            },
1006            (ReadyState::HaveFutureData, new) if new <= ReadyState::HaveCurrentData => {
1007                // FIXME(nox): Queue a task to fire timeupdate and waiting
1008                // events if the conditions call from the spec are met.
1009
1010                // No other steps are applicable in this case.
1011                return;
1012            },
1013
1014            _ => (),
1015        }
1016
1017        // => "If the previous ready state was HAVE_CURRENT_DATA or less, and the new ready state is
1018        // HAVE_FUTURE_DATA or more"
1019        if old_ready_state <= ReadyState::HaveCurrentData &&
1020            ready_state >= ReadyState::HaveFutureData
1021        {
1022            // The user agent must queue a media element task given the media element to fire an
1023            // event named canplay at the element.
1024            self.queue_media_element_task_to_fire_event(atom!("canplay"));
1025
1026            // If the element's paused attribute is false, the user agent must notify about playing
1027            // for the element.
1028            if !self.Paused() {
1029                self.notify_about_playing();
1030            }
1031        }
1032
1033        // => "If the new ready state is HAVE_ENOUGH_DATA"
1034        if ready_state == ReadyState::HaveEnoughData {
1035            // The user agent must queue a media element task given the media element to fire an
1036            // event named canplaythrough at the element.
1037            self.queue_media_element_task_to_fire_event(atom!("canplaythrough"));
1038
1039            // If the element is eligible for autoplay, then the user agent may run the following
1040            // substeps:
1041            if self.eligible_for_autoplay() {
1042                // Step 1. Set the paused attribute to false.
1043                self.paused.set(false);
1044
1045                // Step 2. If the element's show poster flag is true, set it to false and run the
1046                // time marches on steps.
1047                if self.show_poster.get() {
1048                    self.show_poster.set(false);
1049                    self.time_marches_on();
1050                }
1051
1052                // Step 3. Queue a media element task given the element to fire an event named play
1053                // at the element.
1054                self.queue_media_element_task_to_fire_event(atom!("play"));
1055
1056                // Step 4. Notify about playing for the element.
1057                self.notify_about_playing();
1058            }
1059        }
1060
1061        self.update_media_state();
1062    }
1063
1064    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1065    fn invoke_resource_selection_algorithm(&self, cx: &mut JSContext) {
1066        // Step 1. Set the element's networkState attribute to the NETWORK_NO_SOURCE value.
1067        self.network_state.set(NetworkState::NoSource);
1068
1069        // Step 2. Set the element's show poster flag to true.
1070        self.show_poster.set(true);
1071
1072        // Step 3. Set the media element's delaying-the-load-event flag to true (this delays the
1073        // load event).
1074        self.delay_load_event(true, cx);
1075
1076        // Step 4. Await a stable state, allowing the task that invoked this algorithm to continue.
1077        // If the resource selection mode in the synchronous section is
1078        // "attribute", the URL of the resource to fetch is relative to the
1079        // media element's node document when the src attribute was last
1080        // changed, which is why we need to pass the base URL in the task
1081        // right here.
1082        let task = MediaElementMicrotask::ResourceSelection {
1083            elem: Dom::from_ref(self),
1084            generation_id: self.generation_id.get(),
1085            base_url: self.owner_document().base_url(),
1086        };
1087
1088        // FIXME(nox): This will later call the resource_selection_algorithm_sync
1089        // method from below, if microtasks were trait objects, we would be able
1090        // to put the code directly in this method, without the boilerplate
1091        // indirections.
1092        ScriptThread::await_stable_state(cx, Box::new(task));
1093    }
1094
1095    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1096    fn resource_selection_algorithm_sync(&self, base_url: ServoUrl, cx: &mut JSContext) {
1097        // TODO Step 5. If the media element's blocked-on-parser flag is false, then populate the
1098        // list of pending text tracks.
1099        // FIXME(ferjm): Implement blocked_on_parser logic
1100        // https://html.spec.whatwg.org/multipage/#blocked-on-parser
1101        // FIXME(nox): Maybe populate the list of pending text tracks.
1102
1103        enum Mode {
1104            Object,
1105            Attribute(String),
1106            Children(DomRoot<HTMLSourceElement>),
1107        }
1108
1109        // Step 6.
1110        let mode = if self.src_object.borrow().is_some() {
1111            // If the media element has an assigned media provider object, then let mode be object.
1112            Mode::Object
1113        } else if let Some(src) = self
1114            .upcast::<Element>()
1115            .get_attribute_string_value(&local_name!("src"))
1116        {
1117            // Otherwise, if the media element has no assigned media provider object but has a src
1118            // attribute, then let mode be attribute.
1119            Mode::Attribute(src)
1120        } else if let Some(source) = self
1121            .upcast::<Node>()
1122            .children_unrooted(cx.no_gc())
1123            .find_map(UnrootedDom::downcast::<HTMLSourceElement>)
1124        {
1125            // Otherwise, if the media element does not have an assigned media provider object and
1126            // does not have a src attribute, but does have a source element child, then let mode be
1127            // children and let candidate be the first such source element child in tree order.
1128            Mode::Children(source.as_rooted())
1129        } else {
1130            // Otherwise, the media element has no assigned media provider object and has neither a
1131            // src attribute nor a source element child:
1132            self.load_state.set(LoadState::NotLoaded);
1133
1134            // Step 6.none.1. Set the networkState to NETWORK_EMPTY.
1135            self.network_state.set(NetworkState::Empty);
1136
1137            // Step 6.none.2. Set the element's delaying-the-load-event flag to false. This stops
1138            // delaying the load event.
1139            self.delay_load_event(false, cx);
1140
1141            // Step 6.none.3. End the synchronous section and return.
1142            return;
1143        };
1144
1145        // Step 7. Set the media element's networkState to NETWORK_LOADING.
1146        self.network_state.set(NetworkState::Loading);
1147
1148        // Step 8. Queue a media element task given the media element to fire an event named
1149        // loadstart at the media element.
1150        self.queue_media_element_task_to_fire_event(atom!("loadstart"));
1151
1152        // Step 9. Run the appropriate steps from the following list:
1153        match mode {
1154            Mode::Object => {
1155                // => "If mode is object"
1156                self.load_from_src_object(cx);
1157            },
1158            Mode::Attribute(src) => {
1159                // => "If mode is attribute"
1160                self.load_from_src_attribute(cx, base_url, &src);
1161            },
1162            Mode::Children(source) => {
1163                // => "Otherwise (mode is children)""
1164                self.load_from_source_child(cx, &source);
1165            },
1166        }
1167    }
1168
1169    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1170    fn load_from_src_object(&self, cx: &JSContext) {
1171        self.load_state.set(LoadState::LoadingFromSrcObject);
1172
1173        // Step 9.object.1. Set the currentSrc attribute to the empty string.
1174        "".clone_into(&mut self.current_src.borrow_mut());
1175
1176        // Step 9.object.3. Run the resource fetch algorithm with the assigned media
1177        // provider object. If that algorithm returns without aborting this one, then the
1178        // load failed.
1179        // Note that the resource fetch algorithm itself takes care of the cleanup in case
1180        // of failure itself.
1181        self.resource_fetch_algorithm(cx, Resource::Object);
1182    }
1183
1184    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1185    fn load_from_src_attribute(&self, cx: &JSContext, base_url: ServoUrl, src: &str) {
1186        self.load_state.set(LoadState::LoadingFromSrcAttribute);
1187
1188        // Step 9.attribute.1. If the src attribute's value is the empty string, then end
1189        // the synchronous section, and jump down to the failed with attribute step below.
1190        if src.is_empty() {
1191            self.queue_dedicated_media_source_failure_steps();
1192            return;
1193        }
1194
1195        // Step 9.attribute.2. Let urlRecord be the result of encoding-parsing a URL given
1196        // the src attribute's value, relative to the media element's node document when the
1197        // src attribute was last changed.
1198        let Ok(url_record) = base_url.join(src) else {
1199            self.queue_dedicated_media_source_failure_steps();
1200            return;
1201        };
1202
1203        // Step 9.attribute.3. If urlRecord is not failure, then set the currentSrc
1204        // attribute to the result of applying the URL serializer to urlRecord.
1205        *self.current_src.borrow_mut() = url_record.as_str().into();
1206
1207        // Step 9.attribute.5. If urlRecord is not failure, then run the resource fetch
1208        // algorithm with urlRecord. If that algorithm returns without aborting this one,
1209        // then the load failed.
1210        // Note that the resource fetch algorithm itself takes care
1211        // of the cleanup in case of failure itself.
1212        self.resource_fetch_algorithm(cx, Resource::Url(url_record));
1213    }
1214
1215    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1216    fn load_from_source_child(&self, cx: &JSContext, source: &HTMLSourceElement) {
1217        self.load_state.set(LoadState::LoadingFromSourceChild);
1218
1219        // Step 9.children.1. Let pointer be a position defined by two adjacent nodes in the media
1220        // element's child list, treating the start of the list (before the first child in the list,
1221        // if any) and end of the list (after the last child in the list, if any) as nodes in their
1222        // own right. One node is the node before pointer, and the other node is the node after
1223        // pointer. Initially, let pointer be the position between the candidate node and the next
1224        // node, if there are any, or the end of the list, if it is the last node.
1225        *self.source_children_pointer.borrow_mut() =
1226            Some(SourceChildrenPointer::new(source, false));
1227
1228        let element = source.upcast::<Element>();
1229
1230        // Step 9.children.2. Process candidate: If candidate does not have a src attribute, or if
1231        // its src attribute's value is the empty string, then end the synchronous section, and jump
1232        // down to the failed with elements step below.
1233        let Some(src) = element
1234            .get_attribute_string_value(&local_name!("src"))
1235            .filter(|value| !value.is_empty())
1236        else {
1237            self.load_from_source_child_failure_steps(cx, source);
1238            return;
1239        };
1240
1241        // Step 9.children.3. If candidate has a media attribute whose value does not match the
1242        // environment, then end the synchronous section, and jump down to the failed with elements
1243        // step below.
1244        if let Some(media) = element.get_attribute_string_value(&local_name!("media")) &&
1245            !MediaList::matches_environment(&element.owner_document(), &media)
1246        {
1247            self.load_from_source_child_failure_steps(cx, source);
1248            return;
1249        }
1250
1251        // Step 9.children.4. Let urlRecord be the result of encoding-parsing a URL given
1252        // candidate's src attribute's value, relative to candidate's node document when the src
1253        // attribute was last changed.
1254        let Ok(url_record) = source.owner_document().base_url().join(&src) else {
1255            // Step 9.children.5. If urlRecord is failure, then end the synchronous section,
1256            // and jump down to the failed with elements step below.
1257            self.load_from_source_child_failure_steps(cx, source);
1258            return;
1259        };
1260
1261        // Step 9.children.6. If candidate has a type attribute whose value, when parsed as a MIME
1262        // type (including any codecs described by the codecs parameter, for types that define that
1263        // parameter), represents a type that the user agent knows it cannot render, then end the
1264        // synchronous section, and jump down to the failed with elements step below.
1265        if let Some(type_) = element.get_attribute_string_value(&local_name!("type")) &&
1266            ServoMedia::get().can_play_type(&type_) == SupportsMediaType::No
1267        {
1268            self.load_from_source_child_failure_steps(cx, source);
1269            return;
1270        }
1271
1272        // Reset the media player before loading the next source child.
1273        self.reset_media_player();
1274
1275        self.current_source_child.set(Some(source));
1276
1277        // Step 9.children.7. Set the currentSrc attribute to the result of applying the URL
1278        // serializer to urlRecord.
1279        *self.current_src.borrow_mut() = url_record.as_str().into();
1280
1281        // Step 9.children.9. Run the resource fetch algorithm with urlRecord. If that
1282        // algorithm returns without aborting this one, then the load failed.
1283        // Note that the resource fetch algorithm itself takes care
1284        // of the cleanup in case of failure itself.
1285        self.resource_fetch_algorithm(cx, Resource::Url(url_record));
1286    }
1287
1288    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1289    fn load_from_source_child_failure_steps(&self, cx: &JSContext, source: &HTMLSourceElement) {
1290        // Step 9.children.10. Failed with elements: Queue a media element task given the media
1291        // element to fire an event named error at candidate.
1292        let trusted_this = Trusted::new(self);
1293        let trusted_source = Trusted::new(source);
1294        let generation_id = self.generation_id.get();
1295
1296        self.owner_global()
1297            .task_manager()
1298            .media_element_task_source()
1299            .queue(task!(queue_error_event: move |cx| {
1300                let this = trusted_this.root();
1301                if generation_id != this.generation_id.get() {
1302                    return;
1303                }
1304
1305                let source = trusted_source.root();
1306                source.upcast::<EventTarget>().fire_event(cx, atom!("error"));
1307            }));
1308
1309        // Step 9.children.11. Await a stable state.
1310        let task = MediaElementMicrotask::SelectNextSourceChild {
1311            elem: Dom::from_ref(self),
1312            generation_id: self.generation_id.get(),
1313        };
1314
1315        ScriptThread::await_stable_state(cx, Box::new(task));
1316    }
1317
1318    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1319    fn select_next_source_child(&self, cx: &mut JSContext) {
1320        // Step 9.children.12. Forget the media element's media-resource-specific tracks.
1321        self.AudioTracks(cx).clear();
1322        self.VideoTracks(cx).clear();
1323
1324        // Step 9.children.13. Find next candidate: Let candidate be null.
1325        let mut source_candidate = None;
1326
1327        // Step 9.children.14. Search loop: If the node after pointer is the end of the list, then
1328        // jump to the waiting step below.
1329        // Step 9.children.15. If the node after pointer is a source element, let candidate be that
1330        // element.
1331        // Step 9.children.16. Advance pointer so that the node before pointer is now the node that
1332        // was after pointer, and the node after pointer is the node after the node that used to be
1333        // after pointer, if any.
1334        if let Some(ref source_children_pointer) = *self.source_children_pointer.borrow() {
1335            // Note that shared implementation between opaque types from
1336            // `inclusively_following_siblings` and `following_siblings` if not possible due to
1337            // precise capturing.
1338            if source_children_pointer.inclusive {
1339                for next_sibling in source_children_pointer
1340                    .source_before_pointer
1341                    .upcast::<Node>()
1342                    .inclusively_following_siblings()
1343                {
1344                    if let Some(next_source) = DomRoot::downcast::<HTMLSourceElement>(next_sibling)
1345                    {
1346                        source_candidate = Some(next_source);
1347                        break;
1348                    }
1349                }
1350            } else {
1351                for next_sibling in source_children_pointer
1352                    .source_before_pointer
1353                    .upcast::<Node>()
1354                    .following_siblings()
1355                {
1356                    if let Some(next_source) = DomRoot::downcast::<HTMLSourceElement>(next_sibling)
1357                    {
1358                        source_candidate = Some(next_source);
1359                        break;
1360                    }
1361                }
1362            };
1363        }
1364
1365        // Step 9.children.17. If candidate is null, jump back to the search loop step. Otherwise,
1366        // jump back to the process candidate step.
1367        if let Some(source_candidate) = source_candidate {
1368            self.load_from_source_child(cx, &source_candidate);
1369            return;
1370        }
1371
1372        self.load_state.set(LoadState::WaitingForSource);
1373
1374        *self.source_children_pointer.borrow_mut() = None;
1375
1376        // Step 9.children.18. Waiting: Set the element's networkState attribute to the
1377        // NETWORK_NO_SOURCE value.
1378        self.network_state.set(NetworkState::NoSource);
1379
1380        // Step 9.children.19. Set the element's show poster flag to true.
1381        self.show_poster.set(true);
1382
1383        // Step 9.children.20. Queue a media element task given the media element to set the
1384        // element's delaying-the-load-event flag to false. This stops delaying the load event.
1385        let this = Trusted::new(self);
1386        let generation_id = self.generation_id.get();
1387
1388        self.owner_global()
1389            .task_manager()
1390            .media_element_task_source()
1391            .queue(task!(queue_delay_load_event: move |cx| {
1392                let this = this.root();
1393                if generation_id != this.generation_id.get() {
1394                    return;
1395                }
1396
1397                this.delay_load_event(false, cx);
1398            }));
1399
1400        // Step 9.children.22. Wait until the node after pointer is a node other than the end of the
1401        // list. (This step might wait forever.)
1402    }
1403
1404    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1405    fn resource_selection_algorithm_failure_steps(&self, cx: &JSContext) {
1406        match self.load_state.get() {
1407            LoadState::LoadingFromSrcObject => {
1408                // Step 9.object.4. Failed with media provider: Reaching this step indicates that
1409                // the media resource failed to load. Take pending play promises and queue a media
1410                // element task given the media element to run the dedicated media source failure
1411                // steps with the result.
1412                self.queue_dedicated_media_source_failure_steps();
1413            },
1414            LoadState::LoadingFromSrcAttribute => {
1415                // Step 9.attribute.6. Failed with attribute: Reaching this step indicates that the
1416                // media resource failed to load or that urlRecord is failure. Take pending play
1417                // promises and queue a media element task given the media element to run the
1418                // dedicated media source failure steps with the result.
1419                self.queue_dedicated_media_source_failure_steps();
1420            },
1421            LoadState::LoadingFromSourceChild => {
1422                // Step 9.children.10. Failed with elements: Queue a media element task given the
1423                // media element to fire an event named error at candidate.
1424                if let Some(source) = self.current_source_child.take() {
1425                    self.load_from_source_child_failure_steps(cx, &source);
1426                }
1427            },
1428            _ => {},
1429        }
1430    }
1431
1432    fn fetch_request(&self, cx: &JSContext, offset: Option<u64>, seek_lock: Option<SeekLock>) {
1433        if self.resource_url.borrow().is_none() && self.blob_url.borrow().is_none() {
1434            error!("Missing request url");
1435            if let Some(seek_lock) = seek_lock {
1436                seek_lock.unlock(/* successful seek */ false);
1437            }
1438            self.resource_selection_algorithm_failure_steps(cx);
1439            return;
1440        }
1441
1442        let document = self.owner_document();
1443        let destination = match self.media_type_id() {
1444            HTMLMediaElementTypeId::HTMLAudioElement => Destination::Audio,
1445            HTMLMediaElementTypeId::HTMLVideoElement => Destination::Video,
1446        };
1447        let mut headers = HeaderMap::new();
1448        // FIXME(eijebong): Use typed headers once we have a constructor for the range header
1449        headers.insert(
1450            header::RANGE,
1451            HeaderValue::from_str(&format!("bytes={}-", offset.unwrap_or(0))).unwrap(),
1452        );
1453        let url = match self.resource_url.borrow().as_ref() {
1454            Some(url) => url.clone(),
1455            None => self.blob_url.borrow().as_ref().unwrap().clone(),
1456        };
1457
1458        let cors_setting = cors_setting_for_element(self.upcast());
1459        let global = self.global();
1460        let request = create_a_potential_cors_request(
1461            Some(document.webview_id()),
1462            url.clone(),
1463            destination,
1464            cors_setting,
1465            None,
1466            global.get_referrer(),
1467        )
1468        .with_global_scope(&global)
1469        .headers(headers)
1470        .referrer_policy(referrer_policy_for_element(self.upcast()));
1471
1472        let mut current_fetch_context = self.current_fetch_context.borrow_mut();
1473        if let Some(ref mut current_fetch_context) = *current_fetch_context {
1474            current_fetch_context.cancel(CancelReason::Abort);
1475        }
1476
1477        *current_fetch_context = Some(HTMLMediaElementFetchContext::new(
1478            request.id,
1479            global.core_resource_thread(),
1480        ));
1481        let listener =
1482            HTMLMediaElementFetchListener::new(self, request.id, url, offset.unwrap_or(0));
1483
1484        self.owner_document().fetch_background(request, listener);
1485
1486        // Since we cancelled the previous fetch, from now on the media element
1487        // will only receive response data from the new fetch that's been
1488        // initiated. This means the player can resume operation, since all subsequent data
1489        // pushes will originate from the new seek offset.
1490        if let Some(seek_lock) = seek_lock {
1491            seek_lock.unlock(/* successful seek */ true);
1492        }
1493    }
1494
1495    /// <https://html.spec.whatwg.org/multipage/#eligible-for-autoplay>
1496    fn eligible_for_autoplay(&self) -> bool {
1497        // its can autoplay flag is true;
1498        self.autoplaying.get() &&
1499
1500        // its paused attribute is true;
1501        self.Paused() &&
1502
1503        // it has an autoplay attribute specified;
1504        self.Autoplay() &&
1505
1506        // its node document's active sandboxing flag set does not have the sandboxed automatic
1507        // features browsing context flag set; and
1508        {
1509            let document = self.owner_document();
1510
1511            !document.has_active_sandboxing_flag(
1512                SandboxingFlagSet::SANDBOXED_AUTOMATIC_FEATURES_BROWSING_CONTEXT_FLAG,
1513            )
1514        }
1515
1516        // its node document is allowed to use the "autoplay" feature.
1517        // TODO: Feature policy: https://html.spec.whatwg.org/iframe-embed-object.html#allowed-to-use
1518    }
1519
1520    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-resource>
1521    fn resource_fetch_algorithm(&self, cx: &JSContext, resource: Resource) {
1522        if let Err(e) = self.create_media_player(&resource) {
1523            error!("Create media player error {:?}", e);
1524            self.resource_selection_algorithm_failure_steps(cx);
1525            return;
1526        }
1527
1528        // Steps 1-2.
1529        // Unapplicable, the `resource` variable already conveys which mode
1530        // is in use.
1531
1532        // Step 3.
1533        // FIXME(nox): Remove all media-resource-specific text tracks.
1534
1535        // Step 5. Run the appropriate steps from the following list:
1536        match resource {
1537            Resource::Url(url) => {
1538                // Step 5.remote.1. Optionally, run the following substeps. This is the expected
1539                // behavior if the user agent intends to not attempt to fetch the resource until the
1540                // user requests it explicitly (e.g. as a way to implement the preload attribute's
1541                // none keyword).
1542                if self.Preload() == "none" && !self.autoplaying.get() {
1543                    // Step 5.remote.1.1. Set the networkState to NETWORK_IDLE.
1544                    self.network_state.set(NetworkState::Idle);
1545
1546                    // Step 5.remote.1.2. Queue a media element task given the media element to fire
1547                    // an event named suspend at the element.
1548                    self.queue_media_element_task_to_fire_event(atom!("suspend"));
1549
1550                    // Step 5.remote.1.3. Queue a media element task given the media element to set
1551                    // the element's delaying-the-load-event flag to false. This stops delaying the
1552                    // load event.
1553                    let this = Trusted::new(self);
1554                    let generation_id = self.generation_id.get();
1555
1556                    self.owner_global()
1557                        .task_manager()
1558                        .media_element_task_source()
1559                        .queue(task!(queue_delay_load_event: move |cx| {
1560                            let this = this.root();
1561                            if generation_id != this.generation_id.get() {
1562                                return;
1563                            }
1564
1565                            this.delay_load_event(false, cx);
1566                        }));
1567
1568                    // TODO Steps 5.remote.1.4. Wait for the task to be run.
1569                    // FIXME(nox): Somehow we should wait for the task from previous
1570                    // step to be ran before continuing.
1571
1572                    // TODO Steps 5.remote.1.5-5.remote.1.7.
1573                    // FIXME(nox): Wait for an implementation-defined event and
1574                    // then continue with the normal set of steps instead of just
1575                    // returning.
1576                    return;
1577                }
1578
1579                *self.resource_url.borrow_mut() = Some(url);
1580
1581                // Steps 5.remote.2-5.remote.8
1582                self.fetch_request(cx, None, None);
1583            },
1584            Resource::Object => {
1585                if let Some(ref src_object) = *self.src_object.borrow() {
1586                    match src_object {
1587                        SrcObject::Blob(blob) => {
1588                            let blob_url = URL::CreateObjectURL(&self.global(), blob);
1589                            *self.blob_url.borrow_mut() =
1590                                Some(ServoUrl::parse(&blob_url.str()).expect("infallible"));
1591                            self.fetch_request(cx, None, None);
1592                        },
1593                        SrcObject::MediaStream(stream) => {
1594                            let tracks = &*stream.get_tracks();
1595                            for (pos, track) in tracks.iter().enumerate() {
1596                                if self
1597                                    .player
1598                                    .borrow()
1599                                    .as_ref()
1600                                    .unwrap()
1601                                    .lock()
1602                                    .unwrap()
1603                                    .set_stream(&track.id(), pos == tracks.len() - 1)
1604                                    .is_err()
1605                                {
1606                                    self.resource_selection_algorithm_failure_steps(cx);
1607                                }
1608                            }
1609                        },
1610                    }
1611                }
1612            },
1613        }
1614    }
1615
1616    /// Queues a task to run the [dedicated media source failure steps][steps].
1617    ///
1618    /// [steps]: https://html.spec.whatwg.org/multipage/#dedicated-media-source-failure-steps
1619    fn queue_dedicated_media_source_failure_steps(&self) {
1620        let this = Trusted::new(self);
1621        let generation_id = self.generation_id.get();
1622        self.take_pending_play_promises(Err(Error::NotSupported(None)));
1623        self.owner_global()
1624            .task_manager()
1625            .media_element_task_source()
1626            .queue(task!(dedicated_media_source_failure_steps: move |cx| {
1627                let this = this.root();
1628                if generation_id != this.generation_id.get() {
1629                    return;
1630                }
1631
1632                this.fulfill_in_flight_play_promises(cx, |cx| {
1633                    // Step 1. Set the error attribute to the result of creating a MediaError with
1634                    // MEDIA_ERR_SRC_NOT_SUPPORTED.
1635                    this.error.set(Some(&*MediaError::new(
1636                        cx,
1637                        &this.owner_window(),
1638                        MEDIA_ERR_SRC_NOT_SUPPORTED)));
1639
1640                    // Step 2. Forget the media element's media-resource-specific tracks.
1641                    this.AudioTracks(cx).clear();
1642                    this.VideoTracks(cx).clear();
1643
1644                    // Step 3. Set the element's networkState attribute to the NETWORK_NO_SOURCE
1645                    // value.
1646                    this.network_state.set(NetworkState::NoSource);
1647
1648                    // Step 4. Set the element's show poster flag to true.
1649                    this.show_poster.set(true);
1650
1651                    // Step 5. Fire an event named error at the media element.
1652                    this.upcast::<EventTarget>().fire_event(cx, atom!("error"));
1653
1654                    if let Some(ref player) = *this.player.borrow()
1655                        && let Err(error) = player.lock().unwrap().stop() {
1656                            error!("Could not stop player: {error:?}");
1657                        }
1658
1659                    // Step 6. Reject pending play promises with promises and a "NotSupportedError"
1660                    // DOMException.
1661                    // Done after running this closure in `fulfill_in_flight_play_promises`.
1662                });
1663
1664                // Step 7. Set the element's delaying-the-load-event flag to false. This stops
1665                // delaying the load event.
1666                this.delay_load_event(false, cx);
1667            }));
1668    }
1669
1670    fn in_error_state(&self) -> bool {
1671        self.error.get().is_some()
1672    }
1673
1674    /// <https://html.spec.whatwg.org/multipage/#potentially-playing>
1675    fn is_potentially_playing(&self) -> bool {
1676        !self.paused.get() &&
1677            !self.ended_playback(LoopCondition::Included) &&
1678            self.error.get().is_none() &&
1679            !self.is_blocked_media_element()
1680    }
1681
1682    /// <https://html.spec.whatwg.org/multipage/#blocked-media-element>
1683    fn is_blocked_media_element(&self) -> bool {
1684        self.ready_state.get() <= ReadyState::HaveCurrentData ||
1685            self.is_paused_for_user_interaction() ||
1686            self.is_paused_for_in_band_content()
1687    }
1688
1689    /// <https://html.spec.whatwg.org/multipage/#paused-for-user-interaction>
1690    fn is_paused_for_user_interaction(&self) -> bool {
1691        // FIXME: we will likely be able to fill this placeholder once (if) we
1692        //        implement the MediaSession API.
1693        false
1694    }
1695
1696    /// <https://html.spec.whatwg.org/multipage/#paused-for-in-band-content>
1697    fn is_paused_for_in_band_content(&self) -> bool {
1698        // FIXME: we will likely be able to fill this placeholder once (if) we
1699        //        implement https://github.com/servo/servo/issues/22314
1700        false
1701    }
1702
1703    /// <https://html.spec.whatwg.org/multipage/#media-element-load-algorithm>
1704    fn media_element_load_algorithm(&self, cx: &mut JSContext) {
1705        // Reset the flag that signals whether loadeddata was ever fired for
1706        // this invokation of the load algorithm.
1707        self.fired_loadeddata_event.set(false);
1708
1709        // TODO Step 1. Set this element's is currently stalled to false.
1710
1711        // Step 2. Abort any already-running instance of the resource selection algorithm for this
1712        // element.
1713        self.generation_id.set(self.generation_id.get() + 1);
1714
1715        self.load_state.set(LoadState::NotLoaded);
1716        *self.source_children_pointer.borrow_mut() = None;
1717        self.current_source_child.set(None);
1718
1719        // Step 3. Let pending tasks be a list of all tasks from the media element's media element
1720        // event task source in one of the task queues.
1721
1722        // Step 4. For each task in pending tasks that would resolve pending play promises or reject
1723        // pending play promises, immediately resolve or reject those promises in the order the
1724        // corresponding tasks were queued.
1725        while !self.in_flight_play_promises_queue.borrow().is_empty() {
1726            self.fulfill_in_flight_play_promises(cx, |_| ());
1727        }
1728
1729        // Step 5. Remove each task in pending tasks from its task queue.
1730        // Note that each media element's pending event and callback is scheduled with associated
1731        // generation id and will be aborted eventually (from Step 2).
1732
1733        let network_state = self.network_state.get();
1734
1735        // Step 6. If the media element's networkState is set to NETWORK_LOADING or NETWORK_IDLE,
1736        // queue a media element task given the media element to fire an event named abort at the
1737        // media element.
1738        if network_state == NetworkState::Loading || network_state == NetworkState::Idle {
1739            self.queue_media_element_task_to_fire_event(atom!("abort"));
1740        }
1741
1742        // Reset the media player for any previously playing media resource (see Step 11).
1743        self.reset_media_player();
1744
1745        // Step 7. If the media element's networkState is not set to NETWORK_EMPTY, then:
1746        if network_state != NetworkState::Empty {
1747            // Step 7.1. Queue a media element task given the media element to fire an event named
1748            // emptied at the media element.
1749            self.queue_media_element_task_to_fire_event(atom!("emptied"));
1750
1751            // Step 7.2. If a fetching process is in progress for the media element, the user agent
1752            // should stop it.
1753            if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
1754                current_fetch_context.cancel(CancelReason::Abort);
1755            }
1756
1757            // TODO Step 7.3. If the media element's assigned media provider object is a MediaSource
1758            // object, then detach it.
1759
1760            // Step 7.4. Forget the media element's media-resource-specific tracks.
1761            self.AudioTracks(cx).clear();
1762            self.VideoTracks(cx).clear();
1763
1764            // Step 7.5. If readyState is not set to HAVE_NOTHING, then set it to that state.
1765            if self.ready_state.get() != ReadyState::HaveNothing {
1766                self.change_ready_state(ReadyState::HaveNothing);
1767            }
1768
1769            // Step 7.6. If the paused attribute is false, then:
1770            if !self.Paused() {
1771                // Step 7.6.1. Set the paused attribute to true.
1772                self.paused.set(true);
1773
1774                // Step 7.6.2. Take pending play promises and reject pending play promises with the
1775                // result and an "AbortError" DOMException.
1776                self.take_pending_play_promises(Err(Error::Abort(None)));
1777                self.fulfill_in_flight_play_promises(cx, |_| ());
1778            }
1779
1780            // Step 7.7. If seeking is true, set it to false.
1781            self.seeking.set(false);
1782
1783            self.current_seek_position.set(f64::NAN);
1784
1785            // Step 7.8. Set the current playback position to 0.
1786            // Set the official playback position to 0.
1787            // If this changed the official playback position, then queue a media element task given
1788            // the media element to fire an event named timeupdate at the media element.
1789            self.current_playback_position.set(0.);
1790            if self.official_playback_position.get() != 0. {
1791                self.queue_media_element_task_to_fire_event(atom!("timeupdate"));
1792            }
1793            self.official_playback_position.set(0.);
1794
1795            // TODO Step 7.9. Set the timeline offset to Not-a-Number (NaN).
1796
1797            // Step 7.10. Update the duration attribute to Not-a-Number (NaN).
1798            self.duration.set(f64::NAN);
1799        }
1800
1801        // Step 8. Set the playbackRate attribute to the value of the defaultPlaybackRate attribute.
1802        self.playback_rate.set(self.default_playback_rate.get());
1803
1804        // Step 9. Set the error attribute to null and the can autoplay flag to true.
1805        self.error.set(None);
1806        self.autoplaying.set(true);
1807
1808        // Step 10. Invoke the media element's resource selection algorithm.
1809        self.invoke_resource_selection_algorithm(cx);
1810
1811        // Step 11. Note: Playback of any previously playing media resource for this element stops.
1812    }
1813
1814    /// Queue a media element task given the media element to fire an event at the media element.
1815    /// <https://html.spec.whatwg.org/multipage/#queue-a-media-element-task>
1816    fn queue_media_element_task_to_fire_event(&self, name: Atom) {
1817        let this = Trusted::new(self);
1818        let generation_id = self.generation_id.get();
1819
1820        self.owner_global()
1821            .task_manager()
1822            .media_element_task_source()
1823            .queue(task!(queue_event: move |cx| {
1824                let this = this.root();
1825                if generation_id != this.generation_id.get() {
1826                    return;
1827                }
1828
1829                this.upcast::<EventTarget>().fire_event(cx, name);
1830            }));
1831    }
1832
1833    /// Appends a promise to the list of pending play promises.
1834    fn push_pending_play_promise(&self, promise: &Rc<Promise>) {
1835        self.pending_play_promises
1836            .borrow_mut()
1837            .push(promise.clone());
1838    }
1839
1840    /// Takes the pending play promises.
1841    ///
1842    /// The result with which these promises will be fulfilled is passed here
1843    /// and this method returns nothing because we actually just move the
1844    /// current list of pending play promises to the
1845    /// `in_flight_play_promises_queue` field.
1846    ///
1847    /// Each call to this method must be followed by a call to
1848    /// `fulfill_in_flight_play_promises`, to actually fulfill the promises
1849    /// which were taken and moved to the in-flight queue.
1850    fn take_pending_play_promises(&self, result: ErrorResult) {
1851        let pending_play_promises = std::mem::take(&mut *self.pending_play_promises.borrow_mut());
1852        self.in_flight_play_promises_queue
1853            .borrow_mut()
1854            .push_back((pending_play_promises.into(), result));
1855    }
1856
1857    /// Fulfills the next in-flight play promises queue after running a closure.
1858    ///
1859    /// See the comment on `take_pending_play_promises` for why this method
1860    /// does not take a list of promises to fulfill. Callers cannot just pop
1861    /// the front list off of `in_flight_play_promises_queue` and later fulfill
1862    /// the promises because that would mean putting
1863    /// `#[cfg_attr(crown, expect(crown::unrooted_must_root))]` on even more functions, potentially
1864    /// hiding actual safety bugs.
1865    fn fulfill_in_flight_play_promises<F>(&self, cx: &mut JSContext, f: F)
1866    where
1867        F: FnOnce(&mut JSContext),
1868    {
1869        let (promises, result) = self
1870            .in_flight_play_promises_queue
1871            .borrow_mut()
1872            .pop_front()
1873            .expect("there should be at least one list of in flight play promises");
1874        f(cx);
1875        for promise in &*promises {
1876            match result {
1877                Ok(ref value) => promise.resolve_native(cx, value),
1878                Err(ref error) => promise.reject_error(cx, error.clone()),
1879            }
1880        }
1881    }
1882
1883    pub(crate) fn handle_source_child_insertion(
1884        &self,
1885        source: &HTMLSourceElement,
1886        cx: &mut JSContext,
1887    ) {
1888        // <https://html.spec.whatwg.org/multipage/#the-source-element:html-element-insertion-steps>
1889        // Step 2. If parent is a media element that has no src attribute and whose networkState has
1890        // the value NETWORK_EMPTY, then invoke that media element's resource selection algorithm.
1891        if self.upcast::<Element>().has_attribute(&local_name!("src")) {
1892            return;
1893        }
1894
1895        if self.network_state.get() == NetworkState::Empty {
1896            self.invoke_resource_selection_algorithm(cx);
1897            return;
1898        }
1899
1900        // <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1901        // Step 9.children.22. Wait until the node after pointer is a node other than the end of the
1902        // list. (This step might wait forever.)
1903        if self.load_state.get() != LoadState::WaitingForSource {
1904            return;
1905        }
1906
1907        self.load_state.set(LoadState::LoadingFromSourceChild);
1908
1909        *self.source_children_pointer.borrow_mut() = Some(SourceChildrenPointer::new(source, true));
1910
1911        // Step 9.children.23. Await a stable state.
1912        let task = MediaElementMicrotask::SelectNextSourceChildAfterWait {
1913            elem: Dom::from_ref(self),
1914            generation_id: self.generation_id.get(),
1915        };
1916
1917        ScriptThread::await_stable_state(cx, Box::new(task));
1918    }
1919
1920    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm>
1921    fn select_next_source_child_after_wait(&self, cx: &mut JSContext) {
1922        // Step 9.children.24. Set the element's delaying-the-load-event flag back to true (this
1923        // delays the load event again, in case it hasn't been fired yet).
1924        self.delay_load_event(true, cx);
1925
1926        // Step 9.children.25. Set the networkState back to NETWORK_LOADING.
1927        self.network_state.set(NetworkState::Loading);
1928
1929        // Step 9.children.26. Jump back to the find next candidate step above.
1930        self.select_next_source_child(cx);
1931    }
1932
1933    /// <https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list>
1934    /// => "If the media data cannot be fetched at all, due to network errors..."
1935    /// => "If the media data can be fetched but is found by inspection to be in an unsupported
1936    /// format, or can otherwise not be rendered at all"
1937    fn media_data_processing_failure_steps(&self, cx: &JSContext) {
1938        // Step 1. The user agent should cancel the fetching process.
1939        if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
1940            current_fetch_context.cancel(CancelReason::Error);
1941        }
1942
1943        // Step 2. Abort this subalgorithm, returning to the resource selection algorithm.
1944        self.resource_selection_algorithm_failure_steps(cx);
1945    }
1946
1947    /// <https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list>
1948    /// => "If the connection is interrupted after some media data has been received..."
1949    /// => "If the media data is corrupted"
1950    fn media_data_processing_fatal_steps(&self, error: u16, cx: &mut JSContext) {
1951        *self.source_children_pointer.borrow_mut() = None;
1952        self.current_source_child.set(None);
1953
1954        // Step 1. The user agent should cancel the fetching process.
1955        if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() {
1956            current_fetch_context.cancel(CancelReason::Error);
1957        }
1958
1959        // Step 2. Set the error attribute to the result of creating a MediaError with
1960        // MEDIA_ERR_NETWORK/MEDIA_ERR_DECODE.
1961        self.error
1962            .set(Some(&*MediaError::new(cx, &self.owner_window(), error)));
1963
1964        // Step 3. Set the element's networkState attribute to the NETWORK_IDLE value.
1965        self.network_state.set(NetworkState::Idle);
1966
1967        // Step 4. Set the element's delaying-the-load-event flag to false. This stops delaying
1968        // the load event.
1969        self.delay_load_event(false, cx);
1970
1971        // Step 5. Fire an event named error at the media element.
1972        self.upcast::<EventTarget>().fire_event(cx, atom!("error"));
1973
1974        // Step 6. Abort the overall resource selection algorithm.
1975    }
1976
1977    /// <https://html.spec.whatwg.org/multipage/#dom-media-seek>
1978    fn seek(&self, time: f64, _approximate_for_speed: bool) {
1979        // Step 1. Set the media element's show poster flag to false.
1980        self.show_poster.set(false);
1981
1982        // Step 2. If the media element's readyState is HAVE_NOTHING, return.
1983        if self.ready_state.get() == ReadyState::HaveNothing {
1984            return;
1985        }
1986
1987        // Step 3. If the element's seeking IDL attribute is true, then another instance of this
1988        // algorithm is already running. Abort that other instance of the algorithm without waiting
1989        // for the step that it is running to complete.
1990        self.current_seek_position.set(f64::NAN);
1991
1992        // Step 4. Set the seeking IDL attribute to true.
1993        self.seeking.set(true);
1994
1995        // Step 5. If the seek was in response to a DOM method call or setting of an IDL attribute,
1996        // then continue the script. The remainder of these steps must be run in parallel.
1997
1998        // Step 6. If the new playback position is later than the end of the media resource, then
1999        // let it be the end of the media resource instead.
2000        let time = f64::min(time, self.Duration());
2001
2002        // Step 7. If the new playback position is less than the earliest possible position, let it
2003        // be that position instead.
2004        let time = f64::max(time, self.earliest_possible_position());
2005
2006        // Step 8. If the (possibly now changed) new playback position is not in one of the ranges
2007        // given in the seekable attribute, then let it be the position in one of the ranges given
2008        // in the seekable attribute that is the nearest to the new playback position. If there are
2009        // no ranges given in the seekable attribute, then set the seeking IDL attribute to false
2010        // and return.
2011        let seekable = self.seekable();
2012
2013        if seekable.is_empty() {
2014            self.seeking.set(false);
2015            return;
2016        }
2017
2018        let mut nearest_seekable_position = 0.0;
2019        let mut in_seekable_range = false;
2020        let mut nearest_seekable_distance = f64::MAX;
2021        for i in 0..seekable.len() {
2022            let start = seekable.start(i).unwrap().abs();
2023            let end = seekable.end(i).unwrap().abs();
2024            if time >= start && time <= end {
2025                nearest_seekable_position = time;
2026                in_seekable_range = true;
2027                break;
2028            } else if time < start {
2029                let distance = start - time;
2030                if distance < nearest_seekable_distance {
2031                    nearest_seekable_distance = distance;
2032                    nearest_seekable_position = start;
2033                }
2034            } else {
2035                let distance = time - end;
2036                if distance < nearest_seekable_distance {
2037                    nearest_seekable_distance = distance;
2038                    nearest_seekable_position = end;
2039                }
2040            }
2041        }
2042        let time = if in_seekable_range {
2043            time
2044        } else {
2045            nearest_seekable_position
2046        };
2047
2048        // Step 9. If the approximate-for-speed flag is set, adjust the new playback position to a
2049        // value that will allow for playback to resume promptly. If new playback position before
2050        // this step is before current playback position, then the adjusted new playback position
2051        // must also be before the current playback position. Similarly, if the new playback
2052        // position before this step is after current playback position, then the adjusted new
2053        // playback position must also be after the current playback position.
2054        // TODO: Note that servo-media with gstreamer does not support inaccurate seeking for now.
2055
2056        // Step 10. Queue a media element task given the media element to fire an event named
2057        // seeking at the element.
2058        self.queue_media_element_task_to_fire_event(atom!("seeking"));
2059
2060        // Step 11. Set the current playback position to the new playback position.
2061        self.current_playback_position.set(time);
2062
2063        if let Some(ref player) = *self.player.borrow() &&
2064            let Err(error) = player.lock().unwrap().seek(time)
2065        {
2066            error!("Could not seek player: {error:?}");
2067        }
2068
2069        self.current_seek_position.set(time);
2070
2071        // Step 12. Wait until the user agent has established whether or not the media data for the
2072        // new playback position is available, and, if it is, until it has decoded enough data to
2073        // play back that position.
2074        // The rest of the steps are handled when the media engine signals a ready state change or
2075        // otherwise satisfies seek completion and signals a position change.
2076    }
2077
2078    /// <https://html.spec.whatwg.org/multipage/#dom-media-seek>
2079    fn seek_end(&self) {
2080        // Any time the user agent provides a stable state, the official playback position must be
2081        // set to the current playback position.
2082        self.official_playback_position
2083            .set(self.current_playback_position.get());
2084
2085        // Step 14. Set the seeking IDL attribute to false.
2086        self.seeking.set(false);
2087
2088        self.current_seek_position.set(f64::NAN);
2089
2090        // Step 15. Run the time marches on steps.
2091        self.time_marches_on();
2092
2093        // Step 16. Queue a media element task given the media element to fire an event named
2094        // timeupdate at the element.
2095        self.queue_media_element_task_to_fire_event(atom!("timeupdate"));
2096
2097        // Step 17. Queue a media element task given the media element to fire an event named seeked
2098        // at the element.
2099        self.queue_media_element_task_to_fire_event(atom!("seeked"));
2100    }
2101
2102    /// <https://html.spec.whatwg.org/multipage/#poster-frame>
2103    pub(crate) fn set_poster_frame(&self, image: Option<Arc<RasterImage>>) {
2104        if pref!(media_testing_enabled) && image.is_some() {
2105            self.queue_media_element_task_to_fire_event(atom!("postershown"));
2106        }
2107
2108        self.video_renderer.lock().unwrap().set_poster_frame(image);
2109
2110        self.upcast::<Node>().dirty(NodeDamage::Other);
2111    }
2112
2113    fn player_id(&self) -> Option<usize> {
2114        self.player
2115            .borrow()
2116            .as_ref()
2117            .map(|player| player.lock().unwrap().get_id())
2118    }
2119
2120    fn create_media_player(&self, resource: &Resource) -> Result<(), ()> {
2121        let stream_type = match *resource {
2122            Resource::Object => {
2123                if let Some(ref src_object) = *self.src_object.borrow() {
2124                    match src_object {
2125                        SrcObject::MediaStream(_) => StreamType::Stream,
2126                        _ => StreamType::Seekable,
2127                    }
2128                } else {
2129                    return Err(());
2130                }
2131            },
2132            _ => StreamType::Seekable,
2133        };
2134
2135        let window = self.owner_window();
2136
2137        let video_renderer: Option<Arc<Mutex<dyn VideoFrameRenderer>>> = match self.media_type_id()
2138        {
2139            HTMLMediaElementTypeId::HTMLAudioElement => None,
2140            HTMLMediaElementTypeId::HTMLVideoElement => Some(self.video_renderer.clone()),
2141        };
2142
2143        let audio_renderer = self.audio_renderer.borrow().as_ref().cloned();
2144
2145        let pipeline_id = window.pipeline_id();
2146        let client_context_id =
2147            ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get());
2148
2149        // This player id will be set later when we have a player.
2150        // The callback will be the only valid user after this function.
2151        let shared_player_id = Arc::new(OnceLock::new());
2152
2153        let event_handler = Arc::new(Mutex::new(HTMLMediaElementEventHandler::new(self)));
2154        let weak_event_handler = Arc::downgrade(&event_handler);
2155        *self.event_handler.borrow_mut() = Some(event_handler);
2156
2157        let task_source = self
2158            .owner_global()
2159            .task_manager()
2160            .media_element_task_source()
2161            .to_sendable();
2162
2163        let shared_player_id_clone = shared_player_id.clone();
2164        let action_callback = generic_channel::GenericCallback::new(move |message| {
2165            let event = message.unwrap();
2166            let weak_event_handler = weak_event_handler.clone();
2167
2168            let shared_player_id_clone = shared_player_id_clone.clone();
2169            task_source.queue(task!(handle_player_event: move |cx| {
2170                trace!("HTMLMediaElement event: {event:?}");
2171
2172                let Some(event_handler) = weak_event_handler.upgrade() else {
2173                    return;
2174                };
2175
2176                if let Some(shared_player_id) = shared_player_id_clone.get() {
2177                    event_handler.lock().unwrap().handle_player_event(*shared_player_id, event, cx);
2178                } else {
2179                    error!("Player Action without ID being assigned yet.");
2180                }
2181            }));
2182        })
2183        .unwrap();
2184        let player = ServoMedia::get().create_player(
2185            &client_context_id,
2186            stream_type,
2187            action_callback,
2188            video_renderer,
2189            audio_renderer,
2190            Box::new(window.get_player_context()),
2191        );
2192
2193        let player_id = {
2194            let player_guard = player.lock().unwrap();
2195
2196            if let Err(error) = player_guard.set_mute(self.muted.get()) {
2197                warn!("Could not set mute state: {error:?}");
2198            }
2199
2200            let id = player_guard.get_id();
2201            if shared_player_id.set(id).is_err() {
2202                error!("Error setting player id. Already set?");
2203            }
2204            id
2205        };
2206
2207        *self.player.borrow_mut() = Some(player);
2208
2209        let task_source = self
2210            .owner_global()
2211            .task_manager()
2212            .media_element_task_source()
2213            .to_sendable();
2214        let weak_video_renderer = Arc::downgrade(&self.video_renderer);
2215
2216        self.video_renderer
2217            .lock()
2218            .unwrap()
2219            .setup(player_id, task_source, weak_video_renderer);
2220
2221        Ok(())
2222    }
2223
2224    fn reset_media_player(&self) {
2225        if self.player.borrow().is_none() {
2226            return;
2227        }
2228
2229        if let Some(ref player) = *self.player.borrow() &&
2230            let Err(error) = player.lock().unwrap().stop()
2231        {
2232            error!("Could not stop player: {error:?}");
2233        }
2234
2235        *self.player.borrow_mut() = None;
2236        self.video_renderer.lock().unwrap().reset();
2237        *self.event_handler.borrow_mut() = None;
2238
2239        if let Some(video_element) = self.downcast::<HTMLVideoElement>() {
2240            video_element.set_natural_dimensions(None, None);
2241        }
2242    }
2243
2244    pub(crate) fn set_audio_track(&self, idx: usize, enabled: bool) {
2245        if let Some(ref player) = *self.player.borrow() &&
2246            let Err(error) = player.lock().unwrap().set_audio_track(idx as i32, enabled)
2247        {
2248            warn!("Could not set audio track {error:?}");
2249        }
2250    }
2251
2252    pub(crate) fn set_video_track(&self, idx: usize, enabled: bool) {
2253        if let Some(ref player) = *self.player.borrow() &&
2254            let Err(error) = player.lock().unwrap().set_video_track(idx as i32, enabled)
2255        {
2256            warn!("Could not set video track: {error:?}");
2257        }
2258    }
2259
2260    /// <https://html.spec.whatwg.org/multipage/#direction-of-playback>
2261    fn direction_of_playback(&self) -> PlaybackDirection {
2262        // If the element's playbackRate is positive or zero, then the direction of playback is
2263        // forwards. Otherwise, it is backwards.
2264        if self.playback_rate.get() >= 0. {
2265            PlaybackDirection::Forwards
2266        } else {
2267            PlaybackDirection::Backwards
2268        }
2269    }
2270
2271    /// <https://html.spec.whatwg.org/multipage/#ended-playback>
2272    fn ended_playback(&self, loop_condition: LoopCondition) -> bool {
2273        // A media element is said to have ended playback when:
2274
2275        // The element's readyState attribute is HAVE_METADATA or greater, and
2276        if self.ready_state.get() < ReadyState::HaveMetadata {
2277            return false;
2278        }
2279
2280        let playback_position = self.current_playback_position.get();
2281
2282        match self.direction_of_playback() {
2283            // Either: The current playback position is the end of the media resource, and the
2284            // direction of playback is forwards, and the media element does not have a loop
2285            // attribute specified.
2286            PlaybackDirection::Forwards => {
2287                playback_position >= self.Duration() &&
2288                    (loop_condition == LoopCondition::Ignored || !self.Loop())
2289            },
2290            // Or: The current playback position is the earliest possible position, and the
2291            // direction of playback is backwards.
2292            PlaybackDirection::Backwards => playback_position <= self.earliest_possible_position(),
2293        }
2294    }
2295
2296    /// <https://html.spec.whatwg.org/multipage/#reaches-the-end>
2297    fn end_of_playback_in_forwards_direction(&self) {
2298        // When the current playback position reaches the end of the media resource when the
2299        // direction of playback is forwards, then the user agent must follow these steps:
2300
2301        // Step 1. If the media element has a loop attribute specified, then seek to the earliest
2302        // posible position of the media resource and return.
2303        if self.Loop() {
2304            self.seek(
2305                self.earliest_possible_position(),
2306                /* approximate_for_speed */ false,
2307            );
2308            return;
2309        }
2310
2311        // Step 2. As defined above, the ended IDL attribute starts returning true once the event
2312        // loop returns to step 1.
2313
2314        // Step 3. Queue a media element task given the media element and the following steps:
2315        let this = Trusted::new(self);
2316        let generation_id = self.generation_id.get();
2317
2318        self.owner_global()
2319            .task_manager()
2320            .media_element_task_source()
2321            .queue(task!(reaches_the_end_steps: move |cx| {
2322                let this = this.root();
2323                if generation_id != this.generation_id.get() {
2324                    return;
2325                }
2326
2327                // Step 3.1. Fire an event named timeupdate at the media element.
2328                this.upcast::<EventTarget>().fire_event(cx, atom!("timeupdate"));
2329
2330                // Step 3.2. If the media element has ended playback, the direction of playback is
2331                // forwards, and paused is false, then:
2332                if this.ended_playback(LoopCondition::Included) &&
2333                    this.direction_of_playback() == PlaybackDirection::Forwards &&
2334                    !this.Paused() {
2335                    // Step 3.2.1. Set the paused attribute to true.
2336                    this.paused.set(true);
2337
2338                    // Step 3.2.2. Fire an event named pause at the media element.
2339                    this.upcast::<EventTarget>().fire_event(cx, atom!("pause"));
2340
2341                    // Step 3.2.3. Take pending play promises and reject pending play promises with
2342                    // the result and an "AbortError" DOMException.
2343                    this.take_pending_play_promises(Err(Error::Abort(None)));
2344                    this.fulfill_in_flight_play_promises(cx, |_| ());
2345                }
2346
2347                // Step 3.3. Fire an event named ended at the media element.
2348                this.upcast::<EventTarget>().fire_event(cx, atom!("ended"));
2349            }));
2350
2351        // <https://html.spec.whatwg.org/multipage/#dom-media-have_current_data>
2352        self.change_ready_state(ReadyState::HaveCurrentData);
2353    }
2354
2355    /// <https://html.spec.whatwg.org/multipage/#reaches-the-end>
2356    fn end_of_playback_in_backwards_direction(&self) {
2357        // When the current playback position reaches the earliest possible position of the media
2358        // resource when the direction of playback is backwards, then the user agent must only queue
2359        // a media element task given the media element to fire an event named timeupdate at the
2360        // element.
2361        if self.current_playback_position.get() <= self.earliest_possible_position() {
2362            self.queue_media_element_task_to_fire_event(atom!("timeupdate"));
2363        }
2364    }
2365
2366    fn playback_end(&self) {
2367        // Abort the following steps of the end of playback if seeking is in progress.
2368        if self.seeking.get() {
2369            return;
2370        }
2371
2372        match self.direction_of_playback() {
2373            PlaybackDirection::Forwards => self.end_of_playback_in_forwards_direction(),
2374            PlaybackDirection::Backwards => self.end_of_playback_in_backwards_direction(),
2375        }
2376    }
2377
2378    fn playback_error(&self, error: &str, cx: &mut JSContext) {
2379        error!("Player error: {:?}", error);
2380
2381        // If we have already flagged an error condition while processing
2382        // the network response, we should silently skip any observable
2383        // errors originating while decoding the erroneous response.
2384        if self.in_error_state() {
2385            return;
2386        }
2387
2388        // <https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list>
2389        if self.ready_state.get() == ReadyState::HaveNothing {
2390            // => "If the media data can be fetched but is found by inspection to be in an
2391            // unsupported format, or can otherwise not be rendered at all"
2392            self.media_data_processing_failure_steps(cx);
2393        } else {
2394            // => "If the media data is corrupted"
2395            self.media_data_processing_fatal_steps(MEDIA_ERR_DECODE, cx);
2396        }
2397    }
2398
2399    fn playback_metadata_updated(
2400        &self,
2401        cx: &mut JSContext,
2402        metadata: &servo_media::player::metadata::Metadata,
2403    ) {
2404        // The following steps should be run once on the initial `metadata` signal from the media
2405        // engine.
2406        if self.ready_state.get() != ReadyState::HaveNothing {
2407            return;
2408        }
2409
2410        // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list
2411        // => "If the media resource is found to have an audio track"
2412        for (i, _track) in metadata.audio_tracks.iter().enumerate() {
2413            let audio_track_list = self.AudioTracks(cx);
2414
2415            // Step 1. Create an AudioTrack object to represent the audio track.
2416            let kind = match i {
2417                0 => DOMString::from("main"),
2418                _ => DOMString::new(),
2419            };
2420
2421            let audio_track = AudioTrack::new(
2422                cx,
2423                self.global().as_window(),
2424                DOMString::new(),
2425                kind,
2426                DOMString::new(),
2427                DOMString::new(),
2428                Some(&*audio_track_list),
2429            );
2430
2431            // Steps 2. Update the media element's audioTracks attribute's AudioTrackList object
2432            // with the new AudioTrack object.
2433            audio_track_list.add(&audio_track);
2434
2435            // Step 3. Let enable be unknown.
2436            // Step 4. If either the media resource or the URL of the current media resource
2437            // indicate a particular set of audio tracks to enable, or if the user agent has
2438            // information that would facilitate the selection of specific audio tracks to
2439            // improve the user's experience, then: if this audio track is one of the ones to
2440            // enable, then set enable to true, otherwise, set enable to false.
2441            if let Some(servo_url) = self.resource_url.borrow().as_ref() {
2442                let fragment = MediaFragmentParser::from(servo_url);
2443                if let Some(id) = fragment.id() &&
2444                    audio_track.id() == id
2445                {
2446                    audio_track_list.set_enabled(audio_track_list.len() - 1, true);
2447                }
2448
2449                if fragment.tracks().contains(&audio_track.kind().into()) {
2450                    audio_track_list.set_enabled(audio_track_list.len() - 1, true);
2451                }
2452            }
2453
2454            // Step 5. If enable is still unknown, then, if the media element does not yet have an
2455            // enabled audio track, then set enable to true, otherwise, set enable to false.
2456            // Step 6. If enable is true, then enable this audio track, otherwise, do not enable
2457            // this audio track.
2458            if audio_track_list.enabled_index().is_none() {
2459                audio_track_list.set_enabled(audio_track_list.len() - 1, true);
2460            }
2461
2462            // Step 7. Fire an event named addtrack at this AudioTrackList object, using TrackEvent,
2463            // with the track attribute initialized to the new AudioTrack object.
2464            let event = TrackEvent::new(
2465                cx,
2466                self.global().as_window(),
2467                atom!("addtrack"),
2468                false,
2469                false,
2470                &Some(VideoTrackOrAudioTrackOrTextTrack::AudioTrack(audio_track)),
2471            );
2472
2473            event
2474                .upcast::<Event>()
2475                .fire(cx, audio_track_list.upcast::<EventTarget>());
2476        }
2477
2478        // => "If the media resource is found to have a video track"
2479        for (i, _track) in metadata.video_tracks.iter().enumerate() {
2480            let video_track_list = self.VideoTracks(cx);
2481
2482            // Step 1. Create a VideoTrack object to represent the video track.
2483            let kind = match i {
2484                0 => DOMString::from("main"),
2485                _ => DOMString::new(),
2486            };
2487
2488            let video_track = VideoTrack::new(
2489                cx,
2490                self.global().as_window(),
2491                DOMString::new(),
2492                kind,
2493                DOMString::new(),
2494                DOMString::new(),
2495                Some(&*video_track_list),
2496            );
2497
2498            // Steps 2. Update the media element's videoTracks attribute's VideoTrackList object
2499            // with the new VideoTrack object.
2500            video_track_list.add(&video_track);
2501
2502            // Step 3. Let enable be unknown.
2503            // Step 4. If either the media resource or the URL of the current media resource
2504            // indicate a particular set of video tracks to enable, or if the user agent has
2505            // information that would facilitate the selection of specific video tracks to
2506            // improve the user's experience, then: if this video track is the first such video
2507            // track, then set enable to true, otherwise, set enable to false.
2508            if let Some(track) = video_track_list.item(0) &&
2509                let Some(servo_url) = self.resource_url.borrow().as_ref()
2510            {
2511                let fragment = MediaFragmentParser::from(servo_url);
2512                if let Some(id) = fragment.id() {
2513                    if track.id() == id {
2514                        video_track_list.set_selected(0, true);
2515                    }
2516                } else if fragment.tracks().contains(&track.kind().into()) {
2517                    video_track_list.set_selected(0, true);
2518                }
2519            }
2520
2521            // Step 5. If enable is still unknown, then, if the media element does not yet have a
2522            // selected video track, then set enable to true, otherwise, set enable to false.
2523            // Step 6. If enable is true, then select this track and unselect any previously
2524            // selected video tracks, otherwise, do not select this video track. If other tracks are
2525            // unselected, then a change event will be fired.
2526            if video_track_list.selected_index().is_none() {
2527                video_track_list.set_selected(video_track_list.len() - 1, true);
2528            }
2529
2530            // Step 7. Fire an event named addtrack at this VideoTrackList object, using TrackEvent,
2531            // with the track attribute initialized to the new VideoTrack object.
2532            let event = TrackEvent::new(
2533                cx,
2534                self.global().as_window(),
2535                atom!("addtrack"),
2536                false,
2537                false,
2538                &Some(VideoTrackOrAudioTrackOrTextTrack::VideoTrack(video_track)),
2539            );
2540
2541            event
2542                .upcast::<Event>()
2543                .fire(cx, video_track_list.upcast::<EventTarget>());
2544        }
2545
2546        // => "Once enough of the media data has been fetched to determine the duration..."
2547
2548        // TODO Step 1. Establish the media timeline for the purposes of the current playback
2549        // position and the earliest possible position, based on the media data.
2550
2551        // TODO Step 2. Update the timeline offset to the date and time that corresponds to the zero
2552        // time in the media timeline established in the previous step, if any. If no explicit time
2553        // and date is given by the media resource, the timeline offset must be set to Not-a-Number
2554        // (NaN).
2555
2556        // Step 3. Set the current playback position and the official playback position to the
2557        // earliest possible position.
2558        let earliest_possible_position = self.earliest_possible_position();
2559        self.current_playback_position
2560            .set(earliest_possible_position);
2561        self.official_playback_position
2562            .set(earliest_possible_position);
2563
2564        // Step 4. Update the duration attribute with the time of the last frame of the resource, if
2565        // known, on the media timeline established above. If it is not known (e.g. a stream that is
2566        // in principle infinite), update the duration attribute to the value positive Infinity.
2567        // Note: The user agent will queue a media element task given the media element to fire an
2568        // event named durationchange at the element at this point.
2569        self.duration.set(
2570            metadata
2571                .duration
2572                .map_or(f64::INFINITY, |duration| duration.as_secs_f64()),
2573        );
2574        self.queue_media_element_task_to_fire_event(atom!("durationchange"));
2575
2576        // Step 5. For video elements, set the videoWidth and videoHeight attributes, and queue a
2577        // media element task given the media element to fire an event named resize at the media
2578        // element.
2579        if let Some(video_element) = self.downcast::<HTMLVideoElement>() {
2580            video_element.set_natural_dimensions(Some(metadata.width), Some(metadata.height));
2581            self.queue_media_element_task_to_fire_event(atom!("resize"));
2582        }
2583
2584        // Step 6. Set the readyState attribute to HAVE_METADATA.
2585        self.change_ready_state(ReadyState::HaveMetadata);
2586
2587        // Step 7. Let jumped be false.
2588        let mut jumped = false;
2589
2590        // Step 8. If the media element's default playback start position is greater than zero, then
2591        // seek to that time, and let jumped be true.
2592        if self.default_playback_start_position.get() > 0. {
2593            self.seek(
2594                self.default_playback_start_position.get(),
2595                /* approximate_for_speed */ false,
2596            );
2597            jumped = true;
2598        }
2599
2600        // Step 9. Set the media element's default playback start position to zero.
2601        self.default_playback_start_position.set(0.);
2602
2603        // Step 10. Let the initial playback position be 0.
2604        // Step 11. If either the media resource or the URL of the current media resource indicate a
2605        // particular start time, then set the initial playback position to that time and, if jumped
2606        // is still false, seek to that time.
2607        if let Some(servo_url) = self.resource_url.borrow().as_ref() {
2608            let fragment = MediaFragmentParser::from(servo_url);
2609            if let Some(initial_playback_position) = fragment.start() &&
2610                initial_playback_position > 0. &&
2611                initial_playback_position < self.duration.get() &&
2612                !jumped
2613            {
2614                self.seek(
2615                    initial_playback_position,
2616                    /* approximate_for_speed */ false,
2617                )
2618            }
2619        }
2620
2621        // Step 12. If there is no enabled audio track, then enable an audio track. This will cause
2622        // a change event to be fired.
2623        // Step 13. If there is no selected video track, then select a video track. This will cause
2624        // a change event to be fired.
2625        // Note that these steps are already handled by the earlier media track processing.
2626
2627        let global = self.global();
2628        let window = global.as_window();
2629
2630        // Update the media session metadata title with the obtained metadata.
2631        window.Navigator(cx).MediaSession(cx).update_title(
2632            metadata
2633                .title
2634                .clone()
2635                .unwrap_or(window.get_url().into_string()),
2636        );
2637    }
2638
2639    fn playback_duration_changed(&self, duration: Option<Duration>) {
2640        let duration = duration.map_or(f64::INFINITY, |duration| duration.as_secs_f64());
2641
2642        if self.duration.get() == duration {
2643            return;
2644        }
2645
2646        self.duration.set(duration);
2647
2648        // When the length of the media resource changes to a known value (e.g. from being unknown
2649        // to known, or from a previously established length to a new length), the user agent must
2650        // queue a media element task given the media element to fire an event named durationchange
2651        // at the media element.
2652        // <https://html.spec.whatwg.org/multipage/#offsets-into-the-media-resource:media-resource-22>
2653        self.queue_media_element_task_to_fire_event(atom!("durationchange"));
2654
2655        // If the duration is changed such that the current playback position ends up being greater
2656        // than the time of the end of the media resource, then the user agent must also seek to the
2657        // time of the end of the media resource.
2658        if self.current_playback_position.get() > duration {
2659            self.seek(duration, /* approximate_for_speed */ false);
2660        }
2661    }
2662
2663    fn playback_video_frame_updated(&self) {
2664        let Some(video_element) = self.downcast::<HTMLVideoElement>() else {
2665            return;
2666        };
2667
2668        // Whenever the natural width or natural height of the video changes (including, for
2669        // example, because the selected video track was changed), if the element's readyState
2670        // attribute is not HAVE_NOTHING, the user agent must queue a media element task given
2671        // the media element to fire an event named resize at the media element.
2672        // <https://html.spec.whatwg.org/multipage/#concept-video-intrinsic-width>
2673
2674        // The event for the prerolled frame from media engine could reached us before the media
2675        // element HAVE_METADATA ready state so subsequent steps will be cancelled.
2676        if self.ready_state.get() == ReadyState::HaveNothing {
2677            return;
2678        }
2679
2680        if let Some(frame) = self.video_renderer.lock().unwrap().current_frame {
2681            if video_element
2682                .set_natural_dimensions(Some(frame.width as u32), Some(frame.height as u32))
2683            {
2684                self.queue_media_element_task_to_fire_event(atom!("resize"));
2685            } else {
2686                // If the natural dimensions have not been changed, the node should be marked as
2687                // damaged to force a repaint with the new frame contents.
2688                self.upcast::<Node>().dirty(NodeDamage::Other);
2689            }
2690        }
2691    }
2692
2693    fn playback_need_data(&self) {
2694        // The media engine signals that the source needs more data. If we already have a valid
2695        // fetch request, we do nothing. Otherwise, if we have no request and the previous request
2696        // was cancelled because we got an EnoughData event, we restart fetching where we left.
2697        if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() &&
2698            let Some(reason) = current_fetch_context.cancel_reason()
2699        {
2700            // XXX(ferjm) Ideally we should just create a fetch request from
2701            // where we left. But keeping track of the exact next byte that the
2702            // media backend expects is not the easiest task, so I'm simply
2703            // seeking to the current playback position for now which will create
2704            // a new fetch request for the last rendered frame.
2705            if *reason == CancelReason::Backoff {
2706                self.seek(
2707                    self.current_playback_position.get(),
2708                    /* approximate_for_speed */ false,
2709                );
2710            }
2711            return;
2712        }
2713
2714        if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() &&
2715            let Err(e) = {
2716                let mut data_source = current_fetch_context.data_source().borrow_mut();
2717                data_source.set_locked(false);
2718                data_source.process_into_player_from_queue(self.player.borrow().as_ref().unwrap())
2719            }
2720        {
2721            // If we are pushing too much data and we know that we can
2722            // restart the download later from where we left, we cancel
2723            // the current request. Otherwise, we continue the request
2724            // assuming that we may drop some frames.
2725            if e == PlayerError::EnoughData {
2726                current_fetch_context.cancel(CancelReason::Backoff);
2727            }
2728        }
2729    }
2730
2731    fn playback_enough_data(&self) {
2732        // The media engine signals that the source has enough data and asks us to stop pushing bytes
2733        // to avoid excessive buffer queueing, so we cancel the ongoing fetch request if we are able
2734        // to restart it from where we left. Otherwise, we continue the current fetch request,
2735        // assuming that some frames will be dropped.
2736        if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() &&
2737            current_fetch_context.is_seekable()
2738        {
2739            current_fetch_context.cancel(CancelReason::Backoff);
2740        }
2741    }
2742
2743    fn playback_position_changed(&self, cx: &mut JSContext, position: f64) {
2744        // Abort the following steps of the current time update if seeking is in progress.
2745        if self.seeking.get() {
2746            return;
2747        }
2748
2749        let _ = self
2750            .played
2751            .borrow_mut()
2752            .add(self.current_playback_position.get(), position);
2753        self.current_playback_position.set(position);
2754        self.official_playback_position.set(position);
2755        self.time_marches_on();
2756
2757        let media_position_state =
2758            MediaPositionState::new(self.duration.get(), self.playback_rate.get(), position);
2759        debug!(
2760            "Sending media session event set position state {:?}",
2761            media_position_state
2762        );
2763        self.send_media_session_event(
2764            cx,
2765            MediaSessionEvent::SetPositionState(media_position_state),
2766        );
2767    }
2768
2769    fn playback_seek_done(&self, cx: &JSContext, position: f64) {
2770        // If the seek was initiated by script or by the user agent itself continue with the
2771        // following steps, otherwise abort.
2772        let delta = (position - self.current_seek_position.get()).abs();
2773        if !self.seeking.get() || delta > SEEK_POSITION_THRESHOLD {
2774            return;
2775        }
2776
2777        // <https://html.spec.whatwg.org/multipage/#dom-media-seek>
2778        // Step 13. Await a stable state.
2779        let task = MediaElementMicrotask::Seeked {
2780            elem: Dom::from_ref(self),
2781            generation_id: self.generation_id.get(),
2782        };
2783
2784        ScriptThread::await_stable_state(cx, Box::new(task));
2785    }
2786
2787    fn playback_state_changed(&self, cx: &mut JSContext, state: &PlaybackState) {
2788        let mut media_session_playback_state = MediaSessionPlaybackState::None_;
2789        match *state {
2790            PlaybackState::Paused => {
2791                media_session_playback_state = MediaSessionPlaybackState::Paused;
2792                if self.ready_state.get() == ReadyState::HaveMetadata {
2793                    self.change_ready_state(ReadyState::HaveEnoughData);
2794                }
2795            },
2796            PlaybackState::Playing => {
2797                media_session_playback_state = MediaSessionPlaybackState::Playing;
2798                if self.ready_state.get() == ReadyState::HaveMetadata {
2799                    self.change_ready_state(ReadyState::HaveEnoughData);
2800                }
2801            },
2802            PlaybackState::Buffering => {
2803                // Do not send the media session playback state change event
2804                // in this case as a None_ state is expected to clean up the
2805                // session.
2806                return;
2807            },
2808            _ => {},
2809        };
2810        debug!(
2811            "Sending media session event playback state changed to {:?}",
2812            media_session_playback_state
2813        );
2814        self.send_media_session_event(
2815            cx,
2816            MediaSessionEvent::PlaybackStateChange(media_session_playback_state),
2817        );
2818    }
2819
2820    fn seekable(&self) -> TimeRangesContainer {
2821        let mut seekable = TimeRangesContainer::default();
2822        if let Some(ref player) = *self.player.borrow() {
2823            let ranges = player.lock().unwrap().seekable();
2824            for range in ranges {
2825                let _ = seekable.add(range.start, range.end);
2826            }
2827        }
2828        seekable
2829    }
2830
2831    /// <https://html.spec.whatwg.org/multipage/#earliest-possible-position>
2832    fn earliest_possible_position(&self) -> f64 {
2833        self.seekable()
2834            .start(0)
2835            .unwrap_or_else(|_| self.current_playback_position.get())
2836    }
2837
2838    fn render_controls(&self, cx: &mut JSContext) {
2839        if self.upcast::<Element>().is_shadow_host() {
2840            // Bail out if we are already showing the controls.
2841            return;
2842        }
2843
2844        // FIXME(stevennovaryo): Recheck styling of media element to avoid
2845        //                       reparsing styles.
2846        let shadow_root = self.upcast::<Element>().attach_ua_shadow_root(cx, false);
2847        let document = self.owner_document();
2848        let script = Element::create(
2849            cx,
2850            QualName::new(None, ns!(html), local_name!("script")),
2851            None,
2852            &document,
2853            ElementCreator::ScriptCreated,
2854            CustomElementCreationMode::Asynchronous,
2855            None,
2856        );
2857        // This is our hacky way to temporarily workaround the lack of a privileged
2858        // JS context.
2859        // The media controls UI accesses the document.servoGetMediaControls(id) API
2860        // to get an instance to the media controls ShadowRoot.
2861        // `id` needs to match the internally generated UUID assigned to a media element.
2862        let id = Uuid::new_v4().to_string();
2863        document.register_media_controls(&id, &shadow_root);
2864        let media_controls_script = MEDIA_CONTROL_JS.replace("@@@id@@@", &id);
2865        *self.media_controls_id.borrow_mut() = Some(id);
2866        script
2867            .upcast::<Node>()
2868            .set_text_content_for_element(cx, Some(DOMString::from(media_controls_script)));
2869        if let Err(e) = shadow_root
2870            .upcast::<Node>()
2871            .AppendChild(cx, script.upcast::<Node>())
2872        {
2873            warn!("Could not render media controls {:?}", e);
2874            return;
2875        }
2876
2877        let style = Element::create(
2878            cx,
2879            QualName::new(None, ns!(html), local_name!("style")),
2880            None,
2881            &document,
2882            ElementCreator::ScriptCreated,
2883            CustomElementCreationMode::Asynchronous,
2884            None,
2885        );
2886
2887        style
2888            .upcast::<Node>()
2889            .set_text_content_for_element(cx, Some(DOMString::from(MEDIA_CONTROL_CSS)));
2890
2891        if let Err(e) = shadow_root
2892            .upcast::<Node>()
2893            .AppendChild(cx, style.upcast::<Node>())
2894        {
2895            warn!("Could not render media controls {:?}", e);
2896        }
2897
2898        self.upcast::<Node>().dirty(NodeDamage::Other);
2899    }
2900
2901    fn remove_controls(&self) {
2902        if let Some(id) = self.media_controls_id.borrow_mut().take() {
2903            self.owner_document().unregister_media_controls(&id);
2904        }
2905    }
2906
2907    /// Gets the video frame at the current playback position.
2908    pub(crate) fn get_current_frame(&self) -> Option<VideoFrame> {
2909        self.video_renderer
2910            .lock()
2911            .unwrap()
2912            .current_frame_holder
2913            .as_ref()
2914            .map(|holder| holder.get_frame())
2915    }
2916
2917    /// Gets the current frame of the video element to present, if any.
2918    /// <https://html.spec.whatwg.org/multipage/#the-video-element:the-video-element-7>
2919    pub(crate) fn get_current_frame_to_present(&self) -> Option<MediaFrame> {
2920        let (current_frame, poster_frame) = {
2921            let renderer = self.video_renderer.lock().unwrap();
2922            (renderer.current_frame, renderer.poster_frame)
2923        };
2924
2925        // If the show poster flag is set (or there is no current video frame to
2926        // present) AND there is a poster frame, present that.
2927        if (self.show_poster.get() || current_frame.is_none()) && poster_frame.is_some() {
2928            return poster_frame;
2929        }
2930
2931        current_frame
2932    }
2933
2934    /// By default the audio is rendered through the audio sink automatically
2935    /// selected by the servo-media Player instance. However, in some cases, like
2936    /// the WebAudio MediaElementAudioSourceNode, we need to set a custom audio
2937    /// renderer.
2938    pub(crate) fn set_audio_renderer(
2939        &self,
2940        audio_renderer: Option<Arc<Mutex<dyn AudioRenderer>>>,
2941        cx: &mut JSContext,
2942    ) {
2943        *self.audio_renderer.borrow_mut() = audio_renderer;
2944
2945        let had_player = {
2946            if let Some(ref player) = *self.player.borrow() {
2947                if let Err(error) = player.lock().unwrap().stop() {
2948                    error!("Could not stop player: {error:?}");
2949                }
2950                true
2951            } else {
2952                false
2953            }
2954        };
2955
2956        if had_player {
2957            self.media_element_load_algorithm(cx);
2958        }
2959    }
2960
2961    fn send_media_session_event(&self, cx: &mut JSContext, event: MediaSessionEvent) {
2962        let global = self.global();
2963        let media_session = global.as_window().Navigator(cx).MediaSession(cx);
2964
2965        media_session.register_media_instance(self);
2966
2967        media_session.send_event(event);
2968    }
2969
2970    /// <https://html.spec.whatwg.org/multipage/#concept-media-load-resource>
2971    pub(crate) fn origin_is_clean(&self) -> bool {
2972        // Step 5.local (media provider object).
2973        if self.src_object.borrow().is_some() {
2974            // The resource described by the current media resource, if any,
2975            // contains the media data. It is CORS-same-origin.
2976            return true;
2977        }
2978
2979        // Step 5.remote (URL record).
2980        if self.resource_url.borrow().is_some() {
2981            // Update the media data with the contents
2982            // of response's unsafe response obtained in this fashion.
2983            // Response can be CORS-same-origin or CORS-cross-origin;
2984            if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() {
2985                return current_fetch_context.origin_is_clean();
2986            }
2987        }
2988
2989        true
2990    }
2991
2992    pub(crate) fn was_added_to_list_of_text_tracks(&self) {
2993        // https://html.spec.whatwg.org/multipage/#sourcing-out-of-band-text-tracks
2994        // > When a text track corresponding to a track element is added to a media element's list of text tracks,
2995        // > the user agent must queue a media element task given the media element to
2996        // > run the following steps for the media element:
2997        let this = Trusted::new(self);
2998        self.global()
2999            .task_manager()
3000            .media_element_task_source()
3001            .queue(task!(track_event_queue: move |cx| {
3002                let element = this.root();
3003                // Step 1. If the element's blocked-on-parser flag is true, then return.
3004                // TODO
3005                // Step 2. If the element's did-perform-automatic-track-selection flag is true, then return.
3006                if element.did_perform_automatic_track_selection.get() {
3007                    return;
3008                }
3009                // Step 3. Honor user preferences for automatic text track selection for this element.
3010                element.honor_user_preferences_for_automatic_text_track_selection(cx);
3011            }));
3012    }
3013
3014    /// <https://html.spec.whatwg.org/multipage/#honor-user-preferences-for-automatic-text-track-selection>
3015    fn honor_user_preferences_for_automatic_text_track_selection(&self, cx: &mut JSContext) {
3016        // Step 1. Perform automatic text track selection for subtitles and captions.
3017        self.perform_automatic_text_track_selection(
3018            cx,
3019            vec![TextTrackKind::Subtitles, TextTrackKind::Captions],
3020        );
3021        // Step 2. Perform automatic text track selection for descriptions.
3022        self.perform_automatic_text_track_selection(cx, vec![TextTrackKind::Descriptions]);
3023        // Step 3. If there are any text tracks in the media element's list of
3024        // text tracks whose text track kind is chapters or metadata that correspond to
3025        // track elements with a default attribute set whose text track mode is set to disabled,
3026        // then set the text track mode of all such tracks to hidden.
3027        // TODO
3028        // Step 4. Set the element's did-perform-automatic-track-selection flag to true.
3029        self.did_perform_automatic_track_selection.set(true);
3030    }
3031
3032    /// <https://html.spec.whatwg.org/multipage/#perform-automatic-text-track-selection>
3033    fn perform_automatic_text_track_selection(
3034        &self,
3035        cx: &mut JSContext,
3036        text_track_kinds: Vec<TextTrackKind>,
3037    ) {
3038        // Step 1. Let candidates be a list consisting of the text tracks in the media element's
3039        // list of text tracks whose text track kind is one of the kinds that were passed to the algorithm,
3040        // if any, in the order given in the list of text tracks.
3041        let Some(text_tracks_list) = self.text_tracks_list.get() else {
3042            return;
3043        };
3044        let candidates = text_tracks_list.tracks_for_kinds(text_track_kinds);
3045        // Step 2. If candidates is empty, then return.
3046        if candidates.is_empty() {
3047            return;
3048        }
3049        // Step 3. If any of the text tracks in candidates have a text track mode set to showing, return.
3050        if candidates
3051            .iter()
3052            .any(|candidate| candidate.Mode() == TextTrackMode::Showing)
3053        {
3054            return;
3055        }
3056        // Step 4. If the user has expressed an interest in having a track from candidates enabled
3057        // based on its text track kind, text track language, and text track label,
3058        // then set its text track mode to showing.
3059        // Otherwise, if there are any text tracks in candidates that correspond to track elements with
3060        // a default attribute set whose text track mode is set to disabled,
3061        // then set the text track mode of the first such track to showing.
3062        if let Some(default_candidate) = candidates.iter().find(|candidate| {
3063            candidate.associated_track().is_some_and(|track| {
3064                track
3065                    .upcast::<Element>()
3066                    .has_attribute(&local_name!("default"))
3067            }) && candidate.Mode() == TextTrackMode::Disabled
3068        }) {
3069            default_candidate.set_text_track_mode(cx, TextTrackMode::Showing);
3070        }
3071    }
3072}
3073
3074impl HTMLMediaElementMethods<crate::DomTypeHolder> for HTMLMediaElement {
3075    /// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate>
3076    fn NetworkState(&self) -> u16 {
3077        self.network_state.get() as u16
3078    }
3079
3080    /// <https://html.spec.whatwg.org/multipage/#dom-media-readystate>
3081    fn ReadyState(&self) -> u16 {
3082        self.ready_state.get() as u16
3083    }
3084
3085    // https://html.spec.whatwg.org/multipage/#dom-media-autoplay
3086    make_bool_getter!(Autoplay, "autoplay");
3087    // https://html.spec.whatwg.org/multipage/#dom-media-autoplay
3088    make_bool_setter!(SetAutoplay, "autoplay");
3089
3090    // https://html.spec.whatwg.org/multipage/#attr-media-loop
3091    make_bool_getter!(Loop, "loop");
3092    // https://html.spec.whatwg.org/multipage/#attr-media-loop
3093    make_bool_setter!(SetLoop, "loop");
3094
3095    // https://html.spec.whatwg.org/multipage/#dom-media-defaultmuted
3096    make_bool_getter!(DefaultMuted, "muted");
3097    // https://html.spec.whatwg.org/multipage/#dom-media-defaultmuted
3098    make_bool_setter!(SetDefaultMuted, "muted");
3099
3100    // https://html.spec.whatwg.org/multipage/#dom-media-controls
3101    make_bool_getter!(Controls, "controls");
3102    // https://html.spec.whatwg.org/multipage/#dom-media-controls
3103    make_bool_setter!(SetControls, "controls");
3104
3105    // https://html.spec.whatwg.org/multipage/#dom-media-src
3106    make_url_getter!(Src, "src");
3107
3108    // https://html.spec.whatwg.org/multipage/#dom-media-src
3109    make_url_setter!(SetSrc, "src");
3110
3111    /// <https://html.spec.whatwg.org/multipage/#dom-media-crossOrigin>
3112    fn GetCrossOrigin(&self) -> Option<DOMString> {
3113        reflect_cross_origin_attribute(self.upcast::<Element>())
3114    }
3115    /// <https://html.spec.whatwg.org/multipage/#dom-media-crossOrigin>
3116    fn SetCrossOrigin(&self, cx: &mut JSContext, value: Option<DOMString>) {
3117        set_cross_origin_attribute(cx, self.upcast::<Element>(), value);
3118    }
3119
3120    /// <https://html.spec.whatwg.org/multipage/#dom-media-muted>
3121    fn Muted(&self) -> bool {
3122        self.muted.get()
3123    }
3124
3125    /// <https://html.spec.whatwg.org/multipage/#dom-media-muted>
3126    fn SetMuted(&self, _cx: &mut JSContext, value: bool) {
3127        if self.muted.get() == value {
3128            return;
3129        }
3130
3131        self.muted.set(value);
3132
3133        if let Some(ref player) = *self.player.borrow() &&
3134            let Err(error) = player.lock().unwrap().set_mute(value)
3135        {
3136            warn!("Could not set mute state: {error:?}");
3137        }
3138
3139        // The user agent must queue a media element task given the media element to fire an event
3140        // named volumechange at the media element.
3141        self.queue_media_element_task_to_fire_event(atom!("volumechange"));
3142
3143        // Then, if the media element is not allowed to play, the user agent must run the internal
3144        // pause steps for the media element.
3145        if !self.is_allowed_to_play() {
3146            self.internal_pause_steps();
3147        }
3148    }
3149
3150    /// <https://html.spec.whatwg.org/multipage/#dom-media-srcobject>
3151    fn GetSrcObject(&self) -> Option<MediaStreamOrBlob> {
3152        (*self.src_object.borrow())
3153            .as_ref()
3154            .map(|src_object| match src_object {
3155                SrcObject::Blob(blob) => MediaStreamOrBlob::Blob(DomRoot::from_ref(blob)),
3156                SrcObject::MediaStream(stream) => {
3157                    MediaStreamOrBlob::MediaStream(DomRoot::from_ref(stream))
3158                },
3159            })
3160    }
3161
3162    /// <https://html.spec.whatwg.org/multipage/#dom-media-srcobject>
3163    fn SetSrcObject(&self, cx: &mut JSContext, value: Option<MediaStreamOrBlob>) {
3164        *self.src_object.borrow_mut() = value.map(|value| value.into());
3165        self.media_element_load_algorithm(cx);
3166    }
3167
3168    // https://html.spec.whatwg.org/multipage/#attr-media-preload
3169    // Missing/Invalid values are user-agent defined.
3170    make_enumerated_getter!(
3171        Preload,
3172        "preload",
3173        "none" | "metadata" | "auto",
3174        missing => "auto",
3175        invalid => "auto"
3176    );
3177
3178    // https://html.spec.whatwg.org/multipage/#attr-media-preload
3179    make_setter!(SetPreload, "preload");
3180
3181    /// <https://html.spec.whatwg.org/multipage/#dom-media-currentsrc>
3182    fn CurrentSrc(&self) -> USVString {
3183        USVString(self.current_src.borrow().clone())
3184    }
3185
3186    /// <https://html.spec.whatwg.org/multipage/#dom-media-load>
3187    fn Load(&self, cx: &mut JSContext) {
3188        self.media_element_load_algorithm(cx);
3189    }
3190
3191    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-canplaytype>
3192    fn CanPlayType(&self, type_: DOMString) -> CanPlayTypeResult {
3193        match ServoMedia::get().can_play_type(&type_.str()) {
3194            SupportsMediaType::No => CanPlayTypeResult::_empty,
3195            SupportsMediaType::Maybe => CanPlayTypeResult::Maybe,
3196            SupportsMediaType::Probably => CanPlayTypeResult::Probably,
3197        }
3198    }
3199
3200    /// <https://html.spec.whatwg.org/multipage/#dom-media-error>
3201    fn GetError(&self) -> Option<DomRoot<MediaError>> {
3202        self.error.get()
3203    }
3204
3205    /// <https://html.spec.whatwg.org/multipage/#dom-media-play>
3206    fn Play(&self, cx: &mut CurrentRealm) -> Rc<Promise> {
3207        let promise = Promise::new_in_realm(cx);
3208
3209        // TODO Step 1. If the media element is not allowed to play, then return a promise rejected
3210        // with a "NotAllowedError" DOMException.
3211
3212        // Step 2. If the media element's error attribute is not null and its code is
3213        // MEDIA_ERR_SRC_NOT_SUPPORTED, then return a promise rejected with a "NotSupportedError"
3214        // DOMException.
3215        if self
3216            .error
3217            .get()
3218            .is_some_and(|e| e.Code() == MEDIA_ERR_SRC_NOT_SUPPORTED)
3219        {
3220            promise.reject_error(cx, Error::NotSupported(None));
3221            return promise;
3222        }
3223
3224        // Step 3. Let promise be a new promise and append promise to the list of pending play
3225        // promises.
3226        self.push_pending_play_promise(&promise);
3227
3228        // Step 4. Run the internal play steps for the media element.
3229        self.internal_play_steps(cx);
3230
3231        // Step 5. Return promise.
3232        promise
3233    }
3234
3235    /// <https://html.spec.whatwg.org/multipage/#dom-media-pause>
3236    fn Pause(&self, cx: &mut JSContext) {
3237        // Step 1. If the media element's networkState attribute has the value NETWORK_EMPTY, invoke
3238        // the media element's resource selection algorithm.
3239        if self.network_state.get() == NetworkState::Empty {
3240            self.invoke_resource_selection_algorithm(cx);
3241        }
3242
3243        // Step 2. Run the internal pause steps for the media element.
3244        self.internal_pause_steps();
3245    }
3246
3247    /// <https://html.spec.whatwg.org/multipage/#dom-media-paused>
3248    fn Paused(&self) -> bool {
3249        self.paused.get()
3250    }
3251
3252    /// <https://html.spec.whatwg.org/multipage/#dom-media-defaultplaybackrate>
3253    fn GetDefaultPlaybackRate(&self) -> Fallible<Finite<f64>> {
3254        Ok(Finite::wrap(self.default_playback_rate.get()))
3255    }
3256
3257    /// <https://html.spec.whatwg.org/multipage/#dom-media-defaultplaybackrate>
3258    fn SetDefaultPlaybackRate(&self, _cx: &mut JSContext, value: Finite<f64>) -> ErrorResult {
3259        // If the given value is not supported by the user agent, then throw a "NotSupportedError"
3260        // DOMException.
3261        let min_allowed = -64.0;
3262        let max_allowed = 64.0;
3263        if *value < min_allowed || *value > max_allowed {
3264            return Err(Error::NotSupported(None));
3265        }
3266
3267        if self.default_playback_rate.get() == *value {
3268            return Ok(());
3269        }
3270
3271        self.default_playback_rate.set(*value);
3272
3273        // The user agent must queue a media element task given the media element to fire an event
3274        // named ratechange at the media element.
3275        self.queue_media_element_task_to_fire_event(atom!("ratechange"));
3276
3277        Ok(())
3278    }
3279
3280    /// <https://html.spec.whatwg.org/multipage/#dom-media-playbackrate>
3281    fn GetPlaybackRate(&self) -> Fallible<Finite<f64>> {
3282        Ok(Finite::wrap(self.playback_rate.get()))
3283    }
3284
3285    /// <https://html.spec.whatwg.org/multipage/#dom-media-playbackrate>
3286    fn SetPlaybackRate(&self, _cx: &mut JSContext, value: Finite<f64>) -> ErrorResult {
3287        // The attribute is mutable: on setting, the user agent must follow these steps:
3288
3289        // Step 1. If the given value is not supported by the user agent, then throw a
3290        // "NotSupportedError" DOMException.
3291        let min_allowed = -64.0;
3292        let max_allowed = 64.0;
3293        if *value < min_allowed || *value > max_allowed {
3294            return Err(Error::NotSupported(None));
3295        }
3296
3297        if self.playback_rate.get() == *value {
3298            return Ok(());
3299        }
3300
3301        // Step 2. Set playbackRate to the new value, and if the element is potentially playing,
3302        // change the playback speed.
3303        self.playback_rate.set(*value);
3304
3305        if self.is_potentially_playing() &&
3306            let Some(ref player) = *self.player.borrow() &&
3307            let Err(error) = player.lock().unwrap().set_playback_rate(*value)
3308        {
3309            warn!("Could not set the playback rate: {error:?}");
3310        }
3311
3312        // The user agent must queue a media element task given the media element to fire an event
3313        // named ratechange at the media element.
3314        self.queue_media_element_task_to_fire_event(atom!("ratechange"));
3315
3316        Ok(())
3317    }
3318
3319    /// <https://html.spec.whatwg.org/multipage/#dom-media-duration>
3320    fn Duration(&self) -> f64 {
3321        self.duration.get()
3322    }
3323
3324    /// <https://html.spec.whatwg.org/multipage/#dom-media-currenttime>
3325    fn CurrentTime(&self) -> Finite<f64> {
3326        Finite::wrap(if self.default_playback_start_position.get() != 0. {
3327            self.default_playback_start_position.get()
3328        } else if self.seeking.get() {
3329            // Note that the other browsers do the similar (by checking `seeking` value or clamp the
3330            // `official` position to the earliest possible position, the duration, and the seekable
3331            // ranges.
3332            // <https://github.com/whatwg/html/issues/11773>
3333            self.current_seek_position.get()
3334        } else {
3335            self.official_playback_position.get()
3336        })
3337    }
3338
3339    /// <https://html.spec.whatwg.org/multipage/#dom-media-currenttime>
3340    fn SetCurrentTime(&self, _cx: &mut JSContext, time: Finite<f64>) {
3341        if self.ready_state.get() == ReadyState::HaveNothing {
3342            self.default_playback_start_position.set(*time);
3343        } else {
3344            self.official_playback_position.set(*time);
3345            self.seek(*time, /* approximate_for_speed */ false);
3346        }
3347    }
3348
3349    /// <https://html.spec.whatwg.org/multipage/#dom-media-seeking>
3350    fn Seeking(&self) -> bool {
3351        self.seeking.get()
3352    }
3353
3354    /// <https://html.spec.whatwg.org/multipage/#dom-media-ended>
3355    fn Ended(&self) -> bool {
3356        self.ended_playback(LoopCondition::Included) &&
3357            self.direction_of_playback() == PlaybackDirection::Forwards
3358    }
3359
3360    /// <https://html.spec.whatwg.org/multipage/#dom-media-fastseek>
3361    fn FastSeek(&self, time: Finite<f64>) {
3362        self.seek(*time, /* approximate_for_speed */ true);
3363    }
3364
3365    /// <https://html.spec.whatwg.org/multipage/#dom-media-played>
3366    fn Played(&self, cx: &mut JSContext) -> DomRoot<TimeRanges> {
3367        TimeRanges::new(cx, self.global().as_window(), self.played.borrow().clone())
3368    }
3369
3370    /// <https://html.spec.whatwg.org/multipage/#dom-media-seekable>
3371    fn Seekable(&self, cx: &mut JSContext) -> DomRoot<TimeRanges> {
3372        TimeRanges::new(cx, self.global().as_window(), self.seekable())
3373    }
3374
3375    /// <https://html.spec.whatwg.org/multipage/#dom-media-buffered>
3376    fn Buffered(&self, cx: &mut JSContext) -> DomRoot<TimeRanges> {
3377        let mut buffered = TimeRangesContainer::default();
3378        if let Some(ref player) = *self.player.borrow() {
3379            let ranges = player.lock().unwrap().buffered();
3380            for range in ranges {
3381                let _ = buffered.add(range.start, range.end);
3382            }
3383        }
3384        TimeRanges::new(cx, self.global().as_window(), buffered)
3385    }
3386
3387    /// <https://html.spec.whatwg.org/multipage/#dom-media-audiotracks>
3388    fn AudioTracks(&self, cx: &mut JSContext) -> DomRoot<AudioTrackList> {
3389        let window = self.owner_window();
3390        self.audio_tracks_list
3391            .or_init(|| AudioTrackList::new(cx, &window, &[], Some(self)))
3392    }
3393
3394    /// <https://html.spec.whatwg.org/multipage/#dom-media-videotracks>
3395    fn VideoTracks(&self, cx: &mut JSContext) -> DomRoot<VideoTrackList> {
3396        let window = self.owner_window();
3397        self.video_tracks_list
3398            .or_init(|| VideoTrackList::new(cx, &window, &[], Some(self)))
3399    }
3400
3401    /// <https://html.spec.whatwg.org/multipage/#dom-media-texttracks>
3402    fn TextTracks(&self, cx: &mut JSContext) -> DomRoot<TextTrackList> {
3403        // > The textTracks attribute of media elements must return a
3404        // > TextTrackList object representing the TextTrack objects of
3405        // > the text tracks in the media element's list of text tracks,
3406        // > in the same order as in the list of text tracks.
3407        let window = self.owner_window();
3408        self.text_tracks_list
3409            .or_init(|| TextTrackList::new(cx, &window, &[]))
3410    }
3411
3412    /// <https://html.spec.whatwg.org/multipage/#dom-media-addtexttrack>
3413    fn AddTextTrack(
3414        &self,
3415        cx: &mut JSContext,
3416        kind: TextTrackKind,
3417        label: DOMString,
3418        language: DOMString,
3419    ) -> DomRoot<TextTrack> {
3420        let window = self.owner_window();
3421        // Step 1. Create a new TextTrack object.
3422        // Step 2. Create a new text track corresponding to the new object,
3423        // and set its text track kind to kind, its text track label to label,
3424        // its text track language to language, its text track readiness state
3425        // to the text track loaded state, its text track mode to the text
3426        // track hidden mode, and its text track list of cues to an empty list.
3427        let track = TextTrack::new(
3428            cx,
3429            &window,
3430            "".into(),
3431            kind,
3432            label,
3433            language,
3434            TextTrackMode::Hidden,
3435            None,
3436        );
3437        // Step 3. Add the new text track to the media element's list of text tracks.
3438        // Step 4. Queue a media element task given the media element to fire an event
3439        // named addtrack at the media element's textTracks attribute's TextTrackList object,
3440        // using TrackEvent, with the track attribute initialized to
3441        // the new text track's TextTrack object.
3442        self.TextTracks(cx).add(self, &track);
3443        // Step 5. Return the new TextTrack object.
3444        DomRoot::from_ref(&track)
3445    }
3446
3447    /// <https://html.spec.whatwg.org/multipage/#dom-media-volume>
3448    fn GetVolume(&self) -> Fallible<Finite<f64>> {
3449        Ok(Finite::wrap(self.volume.get()))
3450    }
3451
3452    /// <https://html.spec.whatwg.org/multipage/#dom-media-volume>
3453    fn SetVolume(&self, _cx: &mut JSContext, value: Finite<f64>) -> ErrorResult {
3454        // If the new value is outside the range 0.0 to 1.0 inclusive, then, on setting, an
3455        // "IndexSizeError" DOMException must be thrown instead.
3456        let minimum_volume = 0.0;
3457        let maximum_volume = 1.0;
3458        if *value < minimum_volume || *value > maximum_volume {
3459            return Err(Error::IndexSize(None));
3460        }
3461
3462        if self.volume.get() == *value {
3463            return Ok(());
3464        }
3465
3466        self.volume.set(*value);
3467
3468        if let Some(ref player) = *self.player.borrow() &&
3469            let Err(error) = player.lock().unwrap().set_volume(*value)
3470        {
3471            warn!("Could not set the volume: {error:?}");
3472        }
3473
3474        // The user agent must queue a media element task given the media element to fire an event
3475        // named volumechange at the media element.
3476        self.queue_media_element_task_to_fire_event(atom!("volumechange"));
3477
3478        // Then, if the media element is not allowed to play, the user agent must run the internal
3479        // pause steps for the media element.
3480        if !self.is_allowed_to_play() {
3481            self.internal_pause_steps();
3482        }
3483
3484        Ok(())
3485    }
3486}
3487
3488impl VirtualMethods for HTMLMediaElement {
3489    fn super_type(&self) -> Option<&dyn VirtualMethods> {
3490        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
3491    }
3492
3493    fn attribute_mutated(
3494        &self,
3495        cx: &mut JSContext,
3496        attr: AttrRef<'_>,
3497        mutation: AttributeMutation,
3498    ) {
3499        self.super_type()
3500            .unwrap()
3501            .attribute_mutated(cx, attr, mutation);
3502
3503        match *attr.local_name() {
3504            local_name!("muted") => {
3505                // <https://html.spec.whatwg.org/multipage/#dom-media-muted>
3506                // When a media element is created, if the element has a muted content attribute
3507                // specified, then the muted IDL attribute should be set to true.
3508                if let AttributeMutation::Set(
3509                    _,
3510                    AttributeMutationReason::ByCloning | AttributeMutationReason::ByParser,
3511                ) = mutation
3512                {
3513                    self.SetMuted(cx, true);
3514                }
3515            },
3516            local_name!("src") => {
3517                // <https://html.spec.whatwg.org/multipage/#location-of-the-media-resource>
3518                // If a src attribute of a media element is set or changed, the user agent must invoke
3519                // the media element's media element load algorithm (Removing the src attribute does
3520                // not do this, even if there are source elements present).
3521                if !mutation.is_removal() {
3522                    self.media_element_load_algorithm(cx);
3523                }
3524            },
3525            local_name!("controls") => {
3526                if mutation.new_value(attr).is_some() {
3527                    self.render_controls(cx);
3528                } else {
3529                    self.remove_controls();
3530                }
3531            },
3532            _ => (),
3533        };
3534    }
3535
3536    /// <https://html.spec.whatwg.org/multipage/#playing-the-media-resource:remove-an-element-from-a-document>
3537    fn unbind_from_tree(&self, cx: &mut JSContext, context: &UnbindContext) {
3538        self.super_type().unwrap().unbind_from_tree(cx, context);
3539
3540        self.remove_controls();
3541
3542        // Step 1. Await a stable state, allowing the task that removed the media element from the Document to continue.
3543        // The synchronous section consists of all the remaining steps of this algorithm.
3544        // (Steps in the synchronous section are marked with ⌛.)
3545        if context.tree_connected {
3546            let task = MediaElementMicrotask::PauseIfNotInDocument {
3547                elem: Dom::from_ref(self),
3548            };
3549            ScriptThread::await_stable_state(cx, Box::new(task));
3550        }
3551    }
3552
3553    fn adopting_steps(&self, cx: &mut JSContext, old_doc: &Document) {
3554        self.super_type().unwrap().adopting_steps(cx, old_doc);
3555
3556        // Note that media control id should be adopting between documents so "privileged"
3557        // document.servoGetMediaControls(id) API is keeping access to the whitelist of media
3558        // controls identifiers.
3559        if let Some(id) = &*self.media_controls_id.borrow() {
3560            let Some(shadow_root) = self.upcast::<Element>().shadow_root() else {
3561                error!("Missing media controls shadow root");
3562                return;
3563            };
3564
3565            old_doc.unregister_media_controls(id);
3566            self.owner_document()
3567                .register_media_controls(id, &shadow_root);
3568        }
3569    }
3570}
3571
3572#[derive(JSTraceable, MallocSizeOf)]
3573pub(crate) enum MediaElementMicrotask {
3574    ResourceSelection {
3575        elem: Dom<HTMLMediaElement>,
3576        generation_id: u32,
3577        #[no_trace]
3578        base_url: ServoUrl,
3579    },
3580    PauseIfNotInDocument {
3581        elem: Dom<HTMLMediaElement>,
3582    },
3583    Seeked {
3584        elem: Dom<HTMLMediaElement>,
3585        generation_id: u32,
3586    },
3587    SelectNextSourceChild {
3588        elem: Dom<HTMLMediaElement>,
3589        generation_id: u32,
3590    },
3591    SelectNextSourceChildAfterWait {
3592        elem: Dom<HTMLMediaElement>,
3593        generation_id: u32,
3594    },
3595}
3596
3597impl MicrotaskRunnable for MediaElementMicrotask {
3598    fn handler(&self, cx: &mut JSContext) {
3599        let mut realm = match self {
3600            &MediaElementMicrotask::ResourceSelection { ref elem, .. } |
3601            &MediaElementMicrotask::PauseIfNotInDocument { ref elem } |
3602            &MediaElementMicrotask::Seeked { ref elem, .. } |
3603            &MediaElementMicrotask::SelectNextSourceChild { ref elem, .. } |
3604            &MediaElementMicrotask::SelectNextSourceChildAfterWait { ref elem, .. } => {
3605                enter_auto_realm(cx, &**elem)
3606            },
3607        };
3608        let cx = &mut realm;
3609        match self {
3610            &MediaElementMicrotask::ResourceSelection {
3611                ref elem,
3612                generation_id,
3613                ref base_url,
3614            } => {
3615                if generation_id == elem.generation_id.get() {
3616                    elem.resource_selection_algorithm_sync(base_url.clone(), cx);
3617                }
3618            },
3619            // https://html.spec.whatwg.org/multipage/#playing-the-media-resource:remove-an-element-from-a-document
3620            MediaElementMicrotask::PauseIfNotInDocument { elem } => {
3621                // Step 2. ⌛ If the media element is in a document, return.
3622                if elem.upcast::<Node>().is_connected() {
3623                    return;
3624                }
3625                // Step 3. ⌛ Run the internal pause steps for the media element.
3626                elem.internal_pause_steps();
3627            },
3628            &MediaElementMicrotask::Seeked {
3629                ref elem,
3630                generation_id,
3631            } => {
3632                if generation_id == elem.generation_id.get() {
3633                    elem.seek_end();
3634                }
3635            },
3636            &MediaElementMicrotask::SelectNextSourceChild {
3637                ref elem,
3638                generation_id,
3639            } => {
3640                if generation_id == elem.generation_id.get() {
3641                    elem.select_next_source_child(cx);
3642                }
3643            },
3644            &MediaElementMicrotask::SelectNextSourceChildAfterWait {
3645                ref elem,
3646                generation_id,
3647            } => {
3648                if generation_id == elem.generation_id.get() {
3649                    elem.select_next_source_child_after_wait(cx);
3650                }
3651            },
3652        }
3653    }
3654}
3655
3656enum Resource {
3657    Object,
3658    Url(ServoUrl),
3659}
3660
3661#[derive(Debug, MallocSizeOf, PartialEq)]
3662enum DataBuffer {
3663    Payload(Vec<u8>),
3664    EndOfStream,
3665}
3666
3667#[derive(MallocSizeOf)]
3668struct BufferedDataSource {
3669    /// During initial setup and seeking (including clearing the buffer queue
3670    /// and resetting the end-of-stream state), the data source should be locked and
3671    /// any request for processing should be ignored until the media player informs us
3672    /// via the NeedData event that it is ready to accept incoming data.
3673    locked: Cell<bool>,
3674    /// Temporary storage for incoming data.
3675    buffers: VecDeque<DataBuffer>,
3676}
3677
3678impl BufferedDataSource {
3679    fn new() -> BufferedDataSource {
3680        BufferedDataSource {
3681            locked: Cell::new(true),
3682            buffers: VecDeque::default(),
3683        }
3684    }
3685
3686    fn set_locked(&self, locked: bool) {
3687        self.locked.set(locked)
3688    }
3689
3690    fn add_buffer_to_queue(&mut self, buffer: DataBuffer) {
3691        debug_assert_ne!(
3692            self.buffers.back(),
3693            Some(&DataBuffer::EndOfStream),
3694            "The media backend not expects any further data after end of stream"
3695        );
3696
3697        self.buffers.push_back(buffer);
3698    }
3699
3700    fn process_into_player_from_queue(
3701        &mut self,
3702        player: &Arc<Mutex<dyn Player>>,
3703    ) -> Result<(), PlayerError> {
3704        // Early out if any request for processing should be ignored.
3705        if self.locked.get() {
3706            return Ok(());
3707        }
3708
3709        while let Some(buffer) = self.buffers.pop_front() {
3710            match buffer {
3711                DataBuffer::Payload(payload) => {
3712                    if let Err(error) = player.lock().unwrap().push_data(payload) {
3713                        warn!("Could not push input data to player: {error:?}");
3714                        return Err(error);
3715                    }
3716                },
3717                DataBuffer::EndOfStream => {
3718                    if let Err(error) = player.lock().unwrap().end_of_stream() {
3719                        warn!("Could not signal EOS to player: {error:?}");
3720                        return Err(error);
3721                    }
3722                },
3723            }
3724        }
3725
3726        Ok(())
3727    }
3728
3729    fn reset(&mut self) {
3730        self.locked.set(true);
3731        self.buffers.clear();
3732    }
3733}
3734
3735/// Indicates the reason why a fetch request was cancelled.
3736#[derive(Debug, MallocSizeOf, PartialEq)]
3737enum CancelReason {
3738    /// We were asked to stop pushing data to the player.
3739    Backoff,
3740    /// An error ocurred while fetching the media data.
3741    Error,
3742    /// The fetching process is aborted by the user.
3743    Abort,
3744}
3745
3746#[derive(MallocSizeOf)]
3747pub(crate) struct HTMLMediaElementFetchContext {
3748    /// The fetch request id.
3749    request_id: RequestId,
3750    /// Some if the request has been cancelled.
3751    cancel_reason: Option<CancelReason>,
3752    /// Indicates whether the fetched stream is seekable.
3753    is_seekable: bool,
3754    /// Indicates whether the fetched stream is origin clean.
3755    origin_clean: bool,
3756    /// The buffered data source which to be processed by media backend.
3757    data_source: RefCell<BufferedDataSource>,
3758    /// Fetch canceller. Allows cancelling the current fetch request by
3759    /// manually calling its .cancel() method or automatically on Drop.
3760    fetch_canceller: FetchCanceller,
3761}
3762
3763impl HTMLMediaElementFetchContext {
3764    fn new(
3765        request_id: RequestId,
3766        core_resource_thread: CoreResourceThread,
3767    ) -> HTMLMediaElementFetchContext {
3768        HTMLMediaElementFetchContext {
3769            request_id,
3770            cancel_reason: None,
3771            is_seekable: false,
3772            origin_clean: true,
3773            data_source: RefCell::new(BufferedDataSource::new()),
3774            fetch_canceller: FetchCanceller::new(request_id, false, core_resource_thread),
3775        }
3776    }
3777
3778    fn request_id(&self) -> RequestId {
3779        self.request_id
3780    }
3781
3782    fn is_seekable(&self) -> bool {
3783        self.is_seekable
3784    }
3785
3786    fn set_seekable(&mut self, seekable: bool) {
3787        self.is_seekable = seekable;
3788    }
3789
3790    fn origin_is_clean(&self) -> bool {
3791        self.origin_clean
3792    }
3793
3794    fn set_origin_clean(&mut self, origin_clean: bool) {
3795        self.origin_clean = origin_clean;
3796    }
3797
3798    fn data_source(&self) -> &RefCell<BufferedDataSource> {
3799        &self.data_source
3800    }
3801
3802    fn cancel(&mut self, reason: CancelReason) {
3803        if self.cancel_reason.is_some() {
3804            return;
3805        }
3806        self.cancel_reason = Some(reason);
3807        self.data_source.borrow_mut().reset();
3808        self.fetch_canceller.abort();
3809    }
3810
3811    fn cancel_reason(&self) -> &Option<CancelReason> {
3812        &self.cancel_reason
3813    }
3814}
3815
3816struct HTMLMediaElementFetchListener {
3817    /// The element that initiated the request.
3818    element: Trusted<HTMLMediaElement>,
3819    /// The generation of the media element when this fetch started.
3820    generation_id: u32,
3821    /// The fetch request id.
3822    request_id: RequestId,
3823    /// Time of last progress notification.
3824    next_progress_event: Instant,
3825    /// Url for the resource.
3826    url: ServoUrl,
3827    /// Expected content length of the media asset being fetched or played.
3828    expected_content_length: Option<u64>,
3829    /// Actual content length of the media asset was fetched.
3830    fetched_content_length: u64,
3831    /// Discarded content length from the network for the ongoing
3832    /// request if range requests are not supported. Seek requests set it
3833    /// to the required position (in bytes).
3834    content_length_to_discard: u64,
3835}
3836
3837impl FetchResponseListener for HTMLMediaElementFetchListener {
3838    fn process_request_body(&mut self, _: RequestId) {}
3839
3840    fn process_response(
3841        &mut self,
3842        cx: &mut JSContext,
3843        _: RequestId,
3844        metadata: Result<FetchMetadata, NetworkError>,
3845    ) {
3846        let element = self.element.root();
3847
3848        let (metadata, origin_clean) = match metadata {
3849            Ok(fetch_metadata) => match fetch_metadata {
3850                FetchMetadata::Unfiltered(metadata) => (Some(metadata), true),
3851                FetchMetadata::Filtered { filtered, unsafe_ } => (
3852                    Some(unsafe_),
3853                    matches!(
3854                        filtered,
3855                        FilteredMetadata::Basic(_) | FilteredMetadata::Cors(_)
3856                    ),
3857                ),
3858            },
3859            Err(_) => (None, true),
3860        };
3861
3862        let (status_is_success, is_seekable) =
3863            metadata.as_ref().map_or((false, false), |metadata| {
3864                let status = &metadata.status;
3865                (status.is_success(), *status == StatusCode::PARTIAL_CONTENT)
3866            });
3867
3868        // <https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list>
3869        if !status_is_success {
3870            if element.ready_state.get() == ReadyState::HaveNothing {
3871                // => "If the media data cannot be fetched at all, due to network errors..."
3872                element.media_data_processing_failure_steps(cx);
3873            } else {
3874                // => "If the connection is interrupted after some media data has been received..."
3875                element.media_data_processing_fatal_steps(MEDIA_ERR_NETWORK, cx);
3876            }
3877            return;
3878        }
3879
3880        if let Some(ref mut current_fetch_context) = *element.current_fetch_context.borrow_mut() {
3881            current_fetch_context.set_seekable(is_seekable);
3882            current_fetch_context.set_origin_clean(origin_clean);
3883        }
3884
3885        if let Some(metadata) = metadata.as_ref() &&
3886            let Some(headers) = metadata.headers.as_ref()
3887        {
3888            // For range requests we get the size of the media asset from the Content-Range
3889            // header. Otherwise, we get it from the Content-Length header.
3890            let content_length = if let Some(content_range) = headers.typed_get::<ContentRange>() {
3891                content_range.bytes_len()
3892            } else {
3893                headers
3894                    .typed_get::<ContentLength>()
3895                    .map(|content_length| content_length.0)
3896            };
3897
3898            // We only set the expected input size if it changes.
3899            if content_length != self.expected_content_length &&
3900                let Some(content_length) = content_length
3901            {
3902                self.expected_content_length = Some(content_length);
3903            }
3904        }
3905
3906        // Explicit media player initialization with live/seekable source.
3907        if let Err(e) = element
3908            .player
3909            .borrow()
3910            .as_ref()
3911            .unwrap()
3912            .lock()
3913            .unwrap()
3914            .set_seekable(is_seekable)
3915        {
3916            warn!("Could not set player seekable {:?}", e);
3917        }
3918
3919        if let Some(expected_content_length) = self.expected_content_length &&
3920            let Err(e) = element
3921                .player
3922                .borrow()
3923                .as_ref()
3924                .unwrap()
3925                .lock()
3926                .unwrap()
3927                .set_input_size(expected_content_length)
3928        {
3929            warn!("Could not set player input size {:?}", e);
3930        }
3931    }
3932
3933    fn process_response_chunk(&mut self, _: &mut JSContext, _: RequestId, chunk: Vec<u8>) {
3934        let element = self.element.root();
3935
3936        self.fetched_content_length += chunk.len() as u64;
3937
3938        // If an error was received previously, we skip processing the payload.
3939        if let Some(ref mut current_fetch_context) = *element.current_fetch_context.borrow_mut() {
3940            if let Some(CancelReason::Backoff) = current_fetch_context.cancel_reason() {
3941                return;
3942            }
3943
3944            // Discard chunk of the response body if fetch context doesn't support range requests.
3945            let payload = if !current_fetch_context.is_seekable() &&
3946                self.content_length_to_discard != 0
3947            {
3948                if chunk.len() as u64 > self.content_length_to_discard {
3949                    let shrink_chunk = chunk[self.content_length_to_discard as usize..].to_vec();
3950                    self.content_length_to_discard = 0;
3951                    shrink_chunk
3952                } else {
3953                    // Completely discard this response chunk.
3954                    self.content_length_to_discard -= chunk.len() as u64;
3955                    return;
3956                }
3957            } else {
3958                chunk
3959            };
3960
3961            if let Err(e) = {
3962                let mut data_source = current_fetch_context.data_source().borrow_mut();
3963                data_source.add_buffer_to_queue(DataBuffer::Payload(payload));
3964                data_source
3965                    .process_into_player_from_queue(element.player.borrow().as_ref().unwrap())
3966            } {
3967                // If we are pushing too much data and we know that we can
3968                // restart the download later from where we left, we cancel
3969                // the current request. Otherwise, we continue the request
3970                // assuming that we may drop some frames.
3971                if e == PlayerError::EnoughData {
3972                    current_fetch_context.cancel(CancelReason::Backoff);
3973                }
3974                return;
3975            }
3976        }
3977
3978        // <https://html.spec.whatwg.org/multipage/#concept-media-load-resource>
3979        // While the load is not suspended (see below), every 350ms (±200ms) or for every byte
3980        // received, whichever is least frequent, queue a media element task given the media element
3981        // to fire an event named progress at the element.
3982        if Instant::now() > self.next_progress_event {
3983            element.queue_media_element_task_to_fire_event(atom!("progress"));
3984            self.next_progress_event = Instant::now() + Duration::from_millis(350);
3985        }
3986    }
3987
3988    fn process_response_eof(
3989        self,
3990        cx: &mut JSContext,
3991        _: RequestId,
3992        status: Result<(), NetworkError>,
3993        timing: ResourceFetchTiming,
3994    ) {
3995        let element = self.element.root();
3996
3997        // <https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list>
3998        if status.is_ok() && self.fetched_content_length != 0 {
3999            // => "Once the entire media resource has been fetched..."
4000
4001            // There are no more chunks of the response body forthcoming, so we can
4002            // go ahead and notify the media backend not to expect any further data.
4003            if let Some(ref mut current_fetch_context) = *element.current_fetch_context.borrow_mut()
4004            {
4005                // On initial state change READY -> PAUSED the media player perform
4006                // seek to initial position by event with seek segment (TIME format)
4007                // while media stack operates in BYTES format and configuring segment
4008                // start and stop positions without the total size of the stream is not
4009                // possible. As fallback the media player perform seek with BYTES format
4010                // and initiate seek request via "seek-data" callback with required offset.
4011                if self.expected_content_length.is_none() &&
4012                    let Err(e) = element
4013                        .player
4014                        .borrow()
4015                        .as_ref()
4016                        .unwrap()
4017                        .lock()
4018                        .unwrap()
4019                        .set_input_size(self.fetched_content_length)
4020                {
4021                    warn!("Could not set player input size {:?}", e);
4022                }
4023
4024                let mut data_source = current_fetch_context.data_source().borrow_mut();
4025
4026                data_source.add_buffer_to_queue(DataBuffer::EndOfStream);
4027                let _ = data_source
4028                    .process_into_player_from_queue(element.player.borrow().as_ref().unwrap());
4029            }
4030
4031            // Step 1. Fire an event named progress at the media element.
4032            element
4033                .upcast::<EventTarget>()
4034                .fire_event(cx, atom!("progress"));
4035
4036            // Step 2. Set the networkState to NETWORK_IDLE and fire an event named suspend at the
4037            // media element.
4038            element.network_state.set(NetworkState::Idle);
4039
4040            element
4041                .upcast::<EventTarget>()
4042                .fire_event(cx, atom!("suspend"));
4043        } else if status.is_err() && element.ready_state.get() != ReadyState::HaveNothing {
4044            // => "If the connection is interrupted after some media data has been received..."
4045            element.media_data_processing_fatal_steps(MEDIA_ERR_NETWORK, cx);
4046        } else {
4047            // => "If the media data can be fetched but is found by inspection to be in an
4048            // unsupported format, or can otherwise not be rendered at all"
4049            element.media_data_processing_failure_steps(cx);
4050        }
4051
4052        network_listener::submit_timing(cx, &self, &status, &timing);
4053    }
4054
4055    fn process_csp_violations(
4056        &mut self,
4057        cx: &mut js::context::JSContext,
4058        _request_id: RequestId,
4059        violations: Vec<Violation>,
4060    ) {
4061        let global = &self.resource_timing_global();
4062        global.report_csp_violations(cx, violations, None, None);
4063    }
4064
4065    fn should_invoke(&self) -> bool {
4066        let element = self.element.root();
4067
4068        if element.generation_id.get() != self.generation_id || element.player.borrow().is_none() {
4069            return false;
4070        }
4071
4072        let Some(ref current_fetch_context) = *element.current_fetch_context.borrow() else {
4073            return false;
4074        };
4075
4076        // Whether the new fetch request was triggered.
4077        if current_fetch_context.request_id() != self.request_id {
4078            return false;
4079        }
4080
4081        // Whether the current fetch request was cancelled due to a network or decoding error, or
4082        // was aborted by the user.
4083        if let Some(cancel_reason) = current_fetch_context.cancel_reason() &&
4084            matches!(*cancel_reason, CancelReason::Error | CancelReason::Abort)
4085        {
4086            return false;
4087        }
4088
4089        true
4090    }
4091}
4092
4093impl ResourceTimingListener for HTMLMediaElementFetchListener {
4094    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
4095        let initiator_type = InitiatorType::LocalName(
4096            self.element
4097                .root()
4098                .upcast::<Element>()
4099                .local_name()
4100                .to_string(),
4101        );
4102        (initiator_type, self.url.clone())
4103    }
4104
4105    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
4106        self.element.root().owner_document().global()
4107    }
4108}
4109
4110impl HTMLMediaElementFetchListener {
4111    fn new(element: &HTMLMediaElement, request_id: RequestId, url: ServoUrl, offset: u64) -> Self {
4112        Self {
4113            element: Trusted::new(element),
4114            generation_id: element.generation_id.get(),
4115            request_id,
4116            next_progress_event: Instant::now() + Duration::from_millis(350),
4117            url,
4118            expected_content_length: None,
4119            fetched_content_length: 0,
4120            content_length_to_discard: offset,
4121        }
4122    }
4123}
4124
4125/// The [`HTMLMediaElementEventHandler`] is a structure responsible for handling media events for
4126/// the [`HTMLMediaElement`] and exists to decouple ownership of the [`HTMLMediaElement`] from IPC
4127/// router callback.
4128#[derive(JSTraceable, MallocSizeOf)]
4129struct HTMLMediaElementEventHandler {
4130    element: WeakRef<HTMLMediaElement>,
4131}
4132
4133#[expect(unsafe_code)]
4134unsafe impl Send for HTMLMediaElementEventHandler {}
4135
4136impl HTMLMediaElementEventHandler {
4137    fn new(element: &HTMLMediaElement) -> Self {
4138        Self {
4139            element: WeakRef::new(element),
4140        }
4141    }
4142
4143    fn handle_player_event(&self, player_id: usize, event: PlayerEvent, cx: &mut JSContext) {
4144        let Some(element) = self.element.root() else {
4145            return;
4146        };
4147
4148        // Abort event processing if the associated media player is outdated.
4149        if element.player_id().is_none_or(|id| id != player_id) {
4150            return;
4151        }
4152
4153        match event {
4154            PlayerEvent::DurationChanged(duration) => element.playback_duration_changed(duration),
4155            PlayerEvent::EndOfStream => element.playback_end(),
4156            PlayerEvent::EnoughData => element.playback_enough_data(),
4157            PlayerEvent::Error(ref error) => element.playback_error(error, cx),
4158            PlayerEvent::MetadataUpdated(ref metadata) => {
4159                element.playback_metadata_updated(cx, metadata)
4160            },
4161            PlayerEvent::NeedData => element.playback_need_data(),
4162            PlayerEvent::PositionChanged(position) => {
4163                element.playback_position_changed(cx, position)
4164            },
4165            PlayerEvent::SeekData(offset, seek_lock) => {
4166                element.fetch_request(cx, Some(offset), Some(seek_lock))
4167            },
4168            PlayerEvent::SeekDone(position) => element.playback_seek_done(cx, position),
4169            PlayerEvent::StateChanged(ref state) => element.playback_state_changed(cx, state),
4170            PlayerEvent::VideoFrameUpdated => element.playback_video_frame_updated(),
4171        }
4172    }
4173}
4174
4175impl Drop for HTMLMediaElementEventHandler {
4176    fn drop(&mut self) {
4177        // The weak reference to the media element is not thread-safe and MUST be deleted on the
4178        // script thread, which is guaranteed by ownership of the `event handler` in the IPC router
4179        // callback (queued task to the media element task source) and the media element itself.
4180        assert_in_script();
4181    }
4182}