Skip to main content

script/dom/html/
htmlmediaelement.rs

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