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