Skip to main content

script/dom/html/embedded_content/
htmlmediaelement.rs

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