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