Skip to main content

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