| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| use std::cell::Cell; |
| use std::collections::VecDeque; |
| use std::rc::Rc; |
| use std::sync::{Arc, Mutex}; |
| use std::time::{Duration, Instant}; |
| use std::{f64, mem}; |
| |
| use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData}; |
| use content_security_policy::sandboxing_directive::SandboxingFlagSet; |
| use dom_struct::dom_struct; |
| use embedder_traits::{MediaPositionState, MediaSessionEvent, MediaSessionPlaybackState}; |
| use euclid::default::Size2D; |
| use headers::{ContentLength, ContentRange, HeaderMapExt}; |
| use html5ever::{LocalName, Prefix, local_name, ns}; |
| use http::StatusCode; |
| use http::header::{self, HeaderMap, HeaderValue}; |
| use ipc_channel::ipc::{self, IpcSharedMemory, channel}; |
| use ipc_channel::router::ROUTER; |
| use js::jsapi::JSAutoRealm; |
| use layout_api::MediaFrame; |
| use media::{GLPlayerMsg, GLPlayerMsgForward, WindowGLContext}; |
| use net_traits::request::{Destination, RequestId}; |
| use net_traits::{ |
| CoreResourceThread, FetchMetadata, FetchResponseListener, FilteredMetadata, Metadata, |
| NetworkError, ResourceFetchTiming, ResourceTimingType, |
| }; |
| use pixels::RasterImage; |
| use script_bindings::codegen::GenericBindings::TimeRangesBinding::TimeRangesMethods; |
| use script_bindings::codegen::InheritTypes::{ |
| ElementTypeId, HTMLElementTypeId, HTMLMediaElementTypeId, NodeTypeId, |
| }; |
| use servo_config::pref; |
| use servo_media::player::audio::AudioRenderer; |
| use servo_media::player::video::{VideoFrame, VideoFrameRenderer}; |
| use servo_media::player::{PlaybackState, Player, PlayerError, PlayerEvent, SeekLock, StreamType}; |
| use servo_media::{ClientContextId, ServoMedia, SupportsMediaType}; |
| use servo_url::ServoUrl; |
| use webrender_api::{ |
| ExternalImageData, ExternalImageId, ExternalImageType, ImageBufferKind, ImageDescriptor, |
| ImageDescriptorFlags, ImageFormat, ImageKey, |
| }; |
| |
| use crate::document_loader::{LoadBlocker, LoadType}; |
| use crate::dom::attr::Attr; |
| use crate::dom::audio::audiotrack::AudioTrack; |
| use crate::dom::audio::audiotracklist::AudioTrackList; |
| use crate::dom::bindings::cell::DomRefCell; |
| use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; |
| use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::{ |
| CanPlayTypeResult, HTMLMediaElementConstants, HTMLMediaElementMethods, |
| }; |
| use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*; |
| use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods; |
| use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods; |
| use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; |
| use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode}; |
| use crate::dom::bindings::codegen::Bindings::URLBinding::URLMethods; |
| use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; |
| use crate::dom::bindings::codegen::UnionTypes::{ |
| MediaStreamOrBlob, VideoTrackOrAudioTrackOrTextTrack, |
| }; |
| use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; |
| use crate::dom::bindings::inheritance::Castable; |
| use crate::dom::bindings::num::Finite; |
| use crate::dom::bindings::refcounted::Trusted; |
| use crate::dom::bindings::reflector::DomGlobal; |
| use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; |
| use crate::dom::bindings::str::{DOMString, USVString}; |
| use crate::dom::blob::Blob; |
| use crate::dom::csp::{GlobalCspReporting, Violation}; |
| use crate::dom::document::Document; |
| use crate::dom::element::{ |
| AttributeMutation, Element, ElementCreator, cors_setting_for_element, |
| reflect_cross_origin_attribute, set_cross_origin_attribute, |
| }; |
| use crate::dom::event::Event; |
| use crate::dom::eventtarget::EventTarget; |
| use crate::dom::globalscope::GlobalScope; |
| use crate::dom::html::htmlelement::HTMLElement; |
| use crate::dom::html::htmlscriptelement::HTMLScriptElement; |
| use crate::dom::html::htmlsourceelement::HTMLSourceElement; |
| use crate::dom::html::htmlstyleelement::HTMLStyleElement; |
| use crate::dom::html::htmlvideoelement::HTMLVideoElement; |
| use crate::dom::mediaerror::MediaError; |
| use crate::dom::mediafragmentparser::MediaFragmentParser; |
| use crate::dom::mediastream::MediaStream; |
| use crate::dom::node::{Node, NodeDamage, NodeTraits, UnbindContext}; |
| use crate::dom::performanceresourcetiming::InitiatorType; |
| use crate::dom::promise::Promise; |
| use crate::dom::texttrack::TextTrack; |
| use crate::dom::texttracklist::TextTrackList; |
| use crate::dom::timeranges::{TimeRanges, TimeRangesContainer}; |
| use crate::dom::trackevent::TrackEvent; |
| use crate::dom::url::URL; |
| use crate::dom::videotrack::VideoTrack; |
| use crate::dom::videotracklist::VideoTrackList; |
| use crate::dom::virtualmethods::VirtualMethods; |
| use crate::fetch::{FetchCanceller, create_a_potential_cors_request}; |
| use crate::microtask::{Microtask, MicrotaskRunnable}; |
| use crate::network_listener::{self, PreInvoke, ResourceTimingListener}; |
| use crate::realms::{InRealm, enter_realm}; |
| use crate::script_runtime::CanGc; |
| use crate::script_thread::ScriptThread; |
| |
| /// A CSS file to style the media controls. |
| static MEDIA_CONTROL_CSS: &str = include_str!("../../resources/media-controls.css"); |
| |
| /// A JS file to control the media controls. |
| static MEDIA_CONTROL_JS: &str = include_str!("../../resources/media-controls.js"); |
| |
| #[derive(MallocSizeOf, PartialEq)] |
| enum FrameStatus { |
| Locked, |
| Unlocked, |
| } |
| |
| #[derive(MallocSizeOf)] |
| struct FrameHolder( |
| FrameStatus, |
| #[ignore_malloc_size_of = "defined in servo-media"] VideoFrame, |
| ); |
| |
| impl FrameHolder { |
| fn new(frame: VideoFrame) -> FrameHolder { |
| FrameHolder(FrameStatus::Unlocked, frame) |
| } |
| |
| fn lock(&mut self) { |
| if self.0 == FrameStatus::Unlocked { |
| self.0 = FrameStatus::Locked; |
| }; |
| } |
| |
| fn unlock(&mut self) { |
| if self.0 == FrameStatus::Locked { |
| self.0 = FrameStatus::Unlocked; |
| }; |
| } |
| |
| fn set(&mut self, new_frame: VideoFrame) { |
| if self.0 == FrameStatus::Unlocked { |
| self.1 = new_frame |
| }; |
| } |
| |
| fn get(&self) -> (u32, Size2D<i32>, usize) { |
| if self.0 == FrameStatus::Locked { |
| ( |
| self.1.get_texture_id(), |
| Size2D::new(self.1.get_width(), self.1.get_height()), |
| 0, |
| ) |
| } else { |
| unreachable!(); |
| } |
| } |
| |
| fn get_frame(&self) -> VideoFrame { |
| self.1.clone() |
| } |
| } |
| |
| #[derive(MallocSizeOf)] |
| pub(crate) struct MediaFrameRenderer { |
| player_id: Option<u64>, |
| compositor_api: CrossProcessCompositorApi, |
| current_frame: Option<MediaFrame>, |
| old_frame: Option<ImageKey>, |
| very_old_frame: Option<ImageKey>, |
| current_frame_holder: Option<FrameHolder>, |
| /// <https://html.spec.whatwg.org/multipage/#poster-frame> |
| poster_frame: Option<MediaFrame>, |
| } |
| |
| impl MediaFrameRenderer { |
| fn new(compositor_api: CrossProcessCompositorApi) -> Self { |
| Self { |
| player_id: None, |
| compositor_api, |
| current_frame: None, |
| old_frame: None, |
| very_old_frame: None, |
| current_frame_holder: None, |
| poster_frame: None, |
| } |
| } |
| |
| fn set_poster_frame(&mut self, image: Option<Arc<RasterImage>>) { |
| self.poster_frame = image.and_then(|image| { |
| image.id.map(|image_key| MediaFrame { |
| image_key, |
| width: image.metadata.width as i32, |
| height: image.metadata.height as i32, |
| }) |
| }); |
| } |
| } |
| |
| impl VideoFrameRenderer for MediaFrameRenderer { |
| fn render(&mut self, frame: VideoFrame) { |
| let mut updates = smallvec::smallvec![]; |
| |
| if let Some(old_image_key) = mem::replace(&mut self.very_old_frame, self.old_frame.take()) { |
| updates.push(ImageUpdate::DeleteImage(old_image_key)); |
| } |
| |
| let descriptor = ImageDescriptor::new( |
| frame.get_width(), |
| frame.get_height(), |
| ImageFormat::BGRA8, |
| ImageDescriptorFlags::empty(), |
| ); |
| |
| match &mut self.current_frame { |
| Some(current_frame) |
| if current_frame.width == frame.get_width() && |
| current_frame.height == frame.get_height() => |
| { |
| if !frame.is_gl_texture() { |
| updates.push(ImageUpdate::UpdateImage( |
| current_frame.image_key, |
| descriptor, |
| SerializableImageData::Raw(IpcSharedMemory::from_bytes(&frame.get_data())), |
| None, |
| )); |
| } |
| |
| self.current_frame_holder |
| .get_or_insert_with(|| FrameHolder::new(frame.clone())) |
| .set(frame); |
| |
| if let Some(old_image_key) = self.old_frame.take() { |
| updates.push(ImageUpdate::DeleteImage(old_image_key)); |
| } |
| }, |
| Some(current_frame) => { |
| self.old_frame = Some(current_frame.image_key); |
| |
| let Some(new_image_key) = self.compositor_api.generate_image_key_blocking() else { |
| return; |
| }; |
| |
| /* update current_frame */ |
| current_frame.image_key = new_image_key; |
| current_frame.width = frame.get_width(); |
| current_frame.height = frame.get_height(); |
| |
| let image_data = if frame.is_gl_texture() && self.player_id.is_some() { |
| let texture_target = if frame.is_external_oes() { |
| ImageBufferKind::TextureExternal |
| } else { |
| ImageBufferKind::Texture2D |
| }; |
| |
| SerializableImageData::External(ExternalImageData { |
| id: ExternalImageId(self.player_id.unwrap()), |
| channel_index: 0, |
| image_type: ExternalImageType::TextureHandle(texture_target), |
| normalized_uvs: false, |
| }) |
| } else { |
| SerializableImageData::Raw(IpcSharedMemory::from_bytes(&frame.get_data())) |
| }; |
| |
| self.current_frame_holder |
| .get_or_insert_with(|| FrameHolder::new(frame.clone())) |
| .set(frame); |
| |
| updates.push(ImageUpdate::AddImage(new_image_key, descriptor, image_data)); |
| }, |
| None => { |
| let Some(image_key) = self.compositor_api.generate_image_key_blocking() else { |
| return; |
| }; |
| |
| self.current_frame = Some(MediaFrame { |
| image_key, |
| width: frame.get_width(), |
| height: frame.get_height(), |
| }); |
| |
| let image_data = if frame.is_gl_texture() && self.player_id.is_some() { |
| let texture_target = if frame.is_external_oes() { |
| ImageBufferKind::TextureExternal |
| } else { |
| ImageBufferKind::Texture2D |
| }; |
| |
| SerializableImageData::External(ExternalImageData { |
| id: ExternalImageId(self.player_id.unwrap()), |
| channel_index: 0, |
| image_type: ExternalImageType::TextureHandle(texture_target), |
| normalized_uvs: false, |
| }) |
| } else { |
| SerializableImageData::Raw(IpcSharedMemory::from_bytes(&frame.get_data())) |
| }; |
| |
| self.current_frame_holder = Some(FrameHolder::new(frame)); |
| |
| updates.push(ImageUpdate::AddImage(image_key, descriptor, image_data)); |
| }, |
| } |
| self.compositor_api.update_images(updates); |
| } |
| } |
| |
| #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] |
| #[derive(JSTraceable, MallocSizeOf)] |
| enum SrcObject { |
| MediaStream(Dom<MediaStream>), |
| Blob(Dom<Blob>), |
| } |
| |
| impl From<MediaStreamOrBlob> for SrcObject { |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| fn from(src_object: MediaStreamOrBlob) -> SrcObject { |
| match src_object { |
| MediaStreamOrBlob::Blob(blob) => SrcObject::Blob(Dom::from_ref(&*blob)), |
| MediaStreamOrBlob::MediaStream(stream) => { |
| SrcObject::MediaStream(Dom::from_ref(&*stream)) |
| }, |
| } |
| } |
| } |
| |
| #[derive(JSTraceable, MallocSizeOf)] |
| struct DroppableHtmlMediaElement { |
| /// Player Id reported the player thread |
| player_id: Cell<u64>, |
| #[ignore_malloc_size_of = "Defined in other crates"] |
| #[no_trace] |
| player_context: WindowGLContext, |
| } |
| |
| impl DroppableHtmlMediaElement { |
| fn new(player_id: Cell<u64>, player_context: WindowGLContext) -> Self { |
| Self { |
| player_id, |
| player_context, |
| } |
| } |
| |
| pub(crate) fn set_player_id(&self, id: u64) { |
| self.player_id.set(id); |
| } |
| } |
| |
| impl Drop for DroppableHtmlMediaElement { |
| fn drop(&mut self) { |
| self.player_context |
| .send(GLPlayerMsg::UnregisterPlayer(self.player_id.get())); |
| } |
| } |
| #[dom_struct] |
| #[allow(non_snake_case)] |
| pub(crate) struct HTMLMediaElement { |
| htmlelement: HTMLElement, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate> |
| network_state: Cell<NetworkState>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-readystate> |
| ready_state: Cell<ReadyState>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-srcobject> |
| src_object: DomRefCell<Option<SrcObject>>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-currentsrc> |
| current_src: DomRefCell<String>, |
| /// Incremented whenever tasks associated with this element are cancelled. |
| generation_id: Cell<u32>, |
| /// <https://html.spec.whatwg.org/multipage/#fire-loadeddata> |
| /// |
| /// Reset to false every time the load algorithm is invoked. |
| fired_loadeddata_event: Cell<bool>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-error> |
| error: MutNullableDom<MediaError>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-paused> |
| paused: Cell<bool>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-defaultplaybackrate> |
| defaultPlaybackRate: Cell<f64>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-playbackrate> |
| playbackRate: Cell<f64>, |
| /// <https://html.spec.whatwg.org/multipage/#attr-media-autoplay> |
| autoplaying: Cell<bool>, |
| /// <https://html.spec.whatwg.org/multipage/#delaying-the-load-event-flag> |
| delaying_the_load_event_flag: DomRefCell<Option<LoadBlocker>>, |
| /// <https://html.spec.whatwg.org/multipage/#list-of-pending-play-promises> |
| #[ignore_malloc_size_of = "promises are hard"] |
| pending_play_promises: DomRefCell<Vec<Rc<Promise>>>, |
| /// Play promises which are soon to be fulfilled by a queued task. |
| #[allow(clippy::type_complexity)] |
| #[ignore_malloc_size_of = "promises are hard"] |
| in_flight_play_promises_queue: DomRefCell<VecDeque<(Box<[Rc<Promise>]>, ErrorResult)>>, |
| #[ignore_malloc_size_of = "servo_media"] |
| #[no_trace] |
| player: DomRefCell<Option<Arc<Mutex<dyn Player>>>>, |
| #[conditional_malloc_size_of] |
| #[no_trace] |
| video_renderer: Arc<Mutex<MediaFrameRenderer>>, |
| #[ignore_malloc_size_of = "Arc"] |
| #[no_trace] |
| audio_renderer: DomRefCell<Option<Arc<Mutex<dyn AudioRenderer>>>>, |
| /// <https://html.spec.whatwg.org/multipage/#show-poster-flag> |
| show_poster: Cell<bool>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-duration> |
| duration: Cell<f64>, |
| /// <https://html.spec.whatwg.org/multipage/#official-playback-position> |
| playback_position: Cell<f64>, |
| /// <https://html.spec.whatwg.org/multipage/#default-playback-start-position> |
| default_playback_start_position: Cell<f64>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-volume> |
| volume: Cell<f64>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-seeking> |
| seeking: Cell<bool>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-muted> |
| muted: Cell<bool>, |
| /// URL of the media resource, if any. |
| #[no_trace] |
| resource_url: DomRefCell<Option<ServoUrl>>, |
| /// URL of the media resource, if the resource is set through the src_object attribute and it |
| /// is a blob. |
| #[no_trace] |
| blob_url: DomRefCell<Option<ServoUrl>>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-played> |
| played: DomRefCell<TimeRangesContainer>, |
| // https://html.spec.whatwg.org/multipage/#dom-media-audiotracks |
| audio_tracks_list: MutNullableDom<AudioTrackList>, |
| // https://html.spec.whatwg.org/multipage/#dom-media-videotracks |
| video_tracks_list: MutNullableDom<VideoTrackList>, |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-texttracks> |
| text_tracks_list: MutNullableDom<TextTrackList>, |
| /// Time of last timeupdate notification. |
| #[ignore_malloc_size_of = "Defined in std::time"] |
| next_timeupdate_event: Cell<Instant>, |
| /// Latest fetch request context. |
| current_fetch_context: DomRefCell<Option<HTMLMediaElementFetchContext>>, |
| /// Media controls id. |
| /// In order to workaround the lack of privileged JS context, we secure the |
| /// the access to the "privileged" document.servoGetMediaControls(id) API by |
| /// keeping a whitelist of media controls identifiers. |
| media_controls_id: DomRefCell<Option<String>>, |
| droppable: DroppableHtmlMediaElement, |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate> |
| #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] |
| #[repr(u8)] |
| pub(crate) enum NetworkState { |
| Empty = HTMLMediaElementConstants::NETWORK_EMPTY as u8, |
| Idle = HTMLMediaElementConstants::NETWORK_IDLE as u8, |
| Loading = HTMLMediaElementConstants::NETWORK_LOADING as u8, |
| NoSource = HTMLMediaElementConstants::NETWORK_NO_SOURCE as u8, |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-readystate> |
| #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)] |
| #[repr(u8)] |
| #[allow(clippy::enum_variant_names)] // Clippy warning silenced here because these names are from the specification. |
| pub(crate) enum ReadyState { |
| HaveNothing = HTMLMediaElementConstants::HAVE_NOTHING as u8, |
| HaveMetadata = HTMLMediaElementConstants::HAVE_METADATA as u8, |
| HaveCurrentData = HTMLMediaElementConstants::HAVE_CURRENT_DATA as u8, |
| HaveFutureData = HTMLMediaElementConstants::HAVE_FUTURE_DATA as u8, |
| HaveEnoughData = HTMLMediaElementConstants::HAVE_ENOUGH_DATA as u8, |
| } |
| |
| impl HTMLMediaElement { |
| pub(crate) fn new_inherited( |
| tag_name: LocalName, |
| prefix: Option<Prefix>, |
| document: &Document, |
| ) -> Self { |
| Self { |
| htmlelement: HTMLElement::new_inherited(tag_name, prefix, document), |
| network_state: Cell::new(NetworkState::Empty), |
| ready_state: Cell::new(ReadyState::HaveNothing), |
| src_object: Default::default(), |
| current_src: DomRefCell::new("".to_owned()), |
| generation_id: Cell::new(0), |
| fired_loadeddata_event: Cell::new(false), |
| error: Default::default(), |
| paused: Cell::new(true), |
| defaultPlaybackRate: Cell::new(1.0), |
| playbackRate: Cell::new(1.0), |
| muted: Cell::new(false), |
| // FIXME(nox): Why is this initialised to true? |
| autoplaying: Cell::new(true), |
| delaying_the_load_event_flag: Default::default(), |
| pending_play_promises: Default::default(), |
| in_flight_play_promises_queue: Default::default(), |
| player: Default::default(), |
| video_renderer: Arc::new(Mutex::new(MediaFrameRenderer::new( |
| document.window().compositor_api().clone(), |
| ))), |
| audio_renderer: Default::default(), |
| show_poster: Cell::new(true), |
| duration: Cell::new(f64::NAN), |
| playback_position: Cell::new(0.), |
| default_playback_start_position: Cell::new(0.), |
| volume: Cell::new(1.0), |
| seeking: Cell::new(false), |
| resource_url: DomRefCell::new(None), |
| blob_url: DomRefCell::new(None), |
| played: DomRefCell::new(TimeRangesContainer::default()), |
| audio_tracks_list: Default::default(), |
| video_tracks_list: Default::default(), |
| text_tracks_list: Default::default(), |
| next_timeupdate_event: Cell::new(Instant::now() + Duration::from_millis(250)), |
| current_fetch_context: DomRefCell::new(None), |
| media_controls_id: DomRefCell::new(None), |
| droppable: DroppableHtmlMediaElement::new( |
| Cell::new(0), |
| document.window().get_player_context(), |
| ), |
| } |
| } |
| |
| pub(crate) fn network_state(&self) -> NetworkState { |
| self.network_state.get() |
| } |
| |
| pub(crate) fn get_ready_state(&self) -> ReadyState { |
| self.ready_state.get() |
| } |
| |
| fn media_type_id(&self) -> HTMLMediaElementTypeId { |
| match self.upcast::<Node>().type_id() { |
| NodeTypeId::Element(ElementTypeId::HTMLElement( |
| HTMLElementTypeId::HTMLMediaElement(media_type_id), |
| )) => media_type_id, |
| _ => unreachable!(), |
| } |
| } |
| |
| fn play_media(&self) { |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(e) = player.lock().unwrap().set_rate(self.playbackRate.get()) { |
| warn!("Could not set the playback rate {:?}", e); |
| } |
| if let Err(e) = player.lock().unwrap().play() { |
| warn!("Could not play media {:?}", e); |
| } |
| } |
| } |
| |
| /// Marks that element as delaying the load event or not. |
| /// |
| /// Nothing happens if the element was already delaying the load event and |
| /// we pass true to that method again. |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#delaying-the-load-event-flag> |
| pub(crate) fn delay_load_event(&self, delay: bool, can_gc: CanGc) { |
| let blocker = &self.delaying_the_load_event_flag; |
| if delay && blocker.borrow().is_none() { |
| *blocker.borrow_mut() = Some(LoadBlocker::new(&self.owner_document(), LoadType::Media)); |
| } else if !delay && blocker.borrow().is_some() { |
| LoadBlocker::terminate(blocker, can_gc); |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#time-marches-on> |
| fn time_marches_on(&self) { |
| // Step 6. |
| if Instant::now() > self.next_timeupdate_event.get() { |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("timeupdate")); |
| self.next_timeupdate_event |
| .set(Instant::now() + Duration::from_millis(350)); |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#internal-pause-steps> |
| fn internal_pause_steps(&self) { |
| // Step 1. |
| self.autoplaying.set(false); |
| |
| // Step 2. |
| if !self.Paused() { |
| // Step 2.1. |
| self.paused.set(true); |
| |
| // Step 2.2. |
| self.take_pending_play_promises(Err(Error::Abort)); |
| |
| // Step 2.3. |
| let this = Trusted::new(self); |
| let generation_id = self.generation_id.get(); |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue(task!(internal_pause_steps: move || { |
| let this = this.root(); |
| if generation_id != this.generation_id.get() { |
| return; |
| } |
| |
| this.fulfill_in_flight_play_promises(|| { |
| // Step 2.3.1. |
| this.upcast::<EventTarget>().fire_event(atom!("timeupdate"), CanGc::note()); |
| |
| // Step 2.3.2. |
| this.upcast::<EventTarget>().fire_event(atom!("pause"), CanGc::note()); |
| |
| if let Some(ref player) = *this.player.borrow() { |
| if let Err(e) = player.lock().unwrap().pause() { |
| eprintln!("Could not pause player {:?}", e); |
| } |
| } |
| |
| // Step 2.3.3. |
| // Done after running this closure in |
| // `fulfill_in_flight_play_promises`. |
| }); |
| })); |
| |
| // Step 2.4. |
| // FIXME(nox): Set the official playback position to the current |
| // playback position. |
| } |
| } |
| // https://html.spec.whatwg.org/multipage/#allowed-to-play |
| fn is_allowed_to_play(&self) -> bool { |
| true |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#notify-about-playing |
| fn notify_about_playing(&self) { |
| // Step 1. |
| self.take_pending_play_promises(Ok(())); |
| |
| // Step 2. |
| let this = Trusted::new(self); |
| let generation_id = self.generation_id.get(); |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue(task!(notify_about_playing: move || { |
| let this = this.root(); |
| if generation_id != this.generation_id.get() { |
| return; |
| } |
| |
| this.fulfill_in_flight_play_promises(|| { |
| // Step 2.1. |
| this.upcast::<EventTarget>().fire_event(atom!("playing"), CanGc::note()); |
| this.play_media(); |
| |
| // Step 2.2. |
| // Done after running this closure in |
| // `fulfill_in_flight_play_promises`. |
| }); |
| |
| })); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#ready-states |
| fn change_ready_state(&self, ready_state: ReadyState) { |
| let old_ready_state = self.ready_state.get(); |
| self.ready_state.set(ready_state); |
| |
| if self.network_state.get() == NetworkState::Empty { |
| return; |
| } |
| |
| if old_ready_state == ready_state { |
| return; |
| } |
| |
| let owner_global = self.owner_global(); |
| let task_manager = owner_global.task_manager(); |
| let task_source = task_manager.media_element_task_source(); |
| |
| // Step 1. |
| match (old_ready_state, ready_state) { |
| (ReadyState::HaveNothing, ReadyState::HaveMetadata) => { |
| task_source.queue_simple_event(self.upcast(), atom!("loadedmetadata")); |
| // No other steps are applicable in this case. |
| return; |
| }, |
| (ReadyState::HaveMetadata, new) if new >= ReadyState::HaveCurrentData => { |
| if !self.fired_loadeddata_event.get() { |
| self.fired_loadeddata_event.set(true); |
| let this = Trusted::new(self); |
| task_source.queue(task!(media_reached_current_data: move || { |
| let this = this.root(); |
| this.upcast::<EventTarget>().fire_event(atom!("loadeddata"), CanGc::note()); |
| this.delay_load_event(false, CanGc::note()); |
| })); |
| } |
| |
| // Steps for the transition from HaveMetadata to HaveCurrentData |
| // or HaveFutureData also apply here, as per the next match |
| // expression. |
| }, |
| (ReadyState::HaveFutureData, new) if new <= ReadyState::HaveCurrentData => { |
| // FIXME(nox): Queue a task to fire timeupdate and waiting |
| // events if the conditions call from the spec are met. |
| |
| // No other steps are applicable in this case. |
| return; |
| }, |
| |
| _ => (), |
| } |
| |
| if old_ready_state <= ReadyState::HaveCurrentData && |
| ready_state >= ReadyState::HaveFutureData |
| { |
| task_source.queue_simple_event(self.upcast(), atom!("canplay")); |
| |
| if !self.Paused() { |
| self.notify_about_playing(); |
| } |
| } |
| |
| if ready_state == ReadyState::HaveEnoughData { |
| // FIXME(nox): Review this block. |
| if self.eligible_for_autoplay() { |
| // Step 1 |
| self.paused.set(false); |
| // Step 2 |
| if self.show_poster.get() { |
| self.show_poster.set(false); |
| self.time_marches_on(); |
| } |
| // Step 3 |
| task_source.queue_simple_event(self.upcast(), atom!("play")); |
| // Step 4 |
| self.notify_about_playing(); |
| // Step 5 |
| self.autoplaying.set(false); |
| } |
| |
| // FIXME(nox): According to the spec, this should come *before* the |
| // "play" event. |
| task_source.queue_simple_event(self.upcast(), atom!("canplaythrough")); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm |
| fn invoke_resource_selection_algorithm(&self, can_gc: CanGc) { |
| // Step 1. |
| self.network_state.set(NetworkState::NoSource); |
| |
| // Step 2. |
| self.show_poster.set(true); |
| |
| // Step 3. |
| self.delay_load_event(true, can_gc); |
| |
| // Step 4. |
| // If the resource selection mode in the synchronous section is |
| // "attribute", the URL of the resource to fetch is relative to the |
| // media element's node document when the src attribute was last |
| // changed, which is why we need to pass the base URL in the task |
| // right here. |
| let doc = self.owner_document(); |
| let task = MediaElementMicrotask::ResourceSelection { |
| elem: DomRoot::from_ref(self), |
| generation_id: self.generation_id.get(), |
| base_url: doc.base_url(), |
| }; |
| |
| // FIXME(nox): This will later call the resource_selection_algorithm_sync |
| // method from below, if microtasks were trait objects, we would be able |
| // to put the code directly in this method, without the boilerplate |
| // indirections. |
| ScriptThread::await_stable_state(Microtask::MediaElement(task)); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm |
| fn resource_selection_algorithm_sync(&self, base_url: ServoUrl, can_gc: CanGc) { |
| // Step 5. |
| // FIXME(ferjm): Implement blocked_on_parser logic |
| // https://html.spec.whatwg.org/multipage/#blocked-on-parser |
| // FIXME(nox): Maybe populate the list of pending text tracks. |
| |
| // Step 6. |
| enum Mode { |
| Object, |
| Attribute(String), |
| Children(DomRoot<HTMLSourceElement>), |
| } |
| fn mode(media: &HTMLMediaElement) -> Option<Mode> { |
| if media.src_object.borrow().is_some() { |
| return Some(Mode::Object); |
| } |
| if let Some(attr) = media |
| .upcast::<Element>() |
| .get_attribute(&ns!(), &local_name!("src")) |
| { |
| return Some(Mode::Attribute(attr.Value().into())); |
| } |
| let source_child_element = media |
| .upcast::<Node>() |
| .children() |
| .filter_map(DomRoot::downcast::<HTMLSourceElement>) |
| .next(); |
| if let Some(element) = source_child_element { |
| return Some(Mode::Children(element)); |
| } |
| None |
| } |
| let mode = if let Some(mode) = mode(self) { |
| mode |
| } else { |
| self.network_state.set(NetworkState::Empty); |
| // https://github.com/whatwg/html/issues/3065 |
| self.delay_load_event(false, can_gc); |
| return; |
| }; |
| |
| // Step 7. |
| self.network_state.set(NetworkState::Loading); |
| |
| // Step 8. |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("loadstart")); |
| |
| // Step 9. |
| match mode { |
| // Step 9.obj. |
| Mode::Object => { |
| // Step 9.obj.1. |
| "".clone_into(&mut self.current_src.borrow_mut()); |
| |
| // Step 9.obj.2. |
| // FIXME(nox): The rest of the steps should be ran in parallel. |
| |
| // Step 9.obj.3. |
| // Note that the resource fetch algorithm itself takes care |
| // of the cleanup in case of failure itself. |
| self.resource_fetch_algorithm(Resource::Object); |
| }, |
| Mode::Attribute(src) => { |
| // Step 9.attr.1. |
| if src.is_empty() { |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| } |
| |
| // Step 9.attr.2. |
| let url_record = match base_url.join(&src) { |
| Ok(url) => url, |
| Err(_) => { |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| }, |
| }; |
| |
| // Step 9.attr.3. |
| *self.current_src.borrow_mut() = url_record.as_str().into(); |
| |
| // Step 9.attr.4. |
| // Note that the resource fetch algorithm itself takes care |
| // of the cleanup in case of failure itself. |
| self.resource_fetch_algorithm(Resource::Url(url_record)); |
| }, |
| // Step 9.children. |
| Mode::Children(source) => { |
| // This is only a partial implementation |
| // FIXME: https://github.com/servo/servo/issues/21481 |
| let src = source |
| .upcast::<Element>() |
| .get_attribute(&ns!(), &local_name!("src")); |
| // Step 9.attr.2. |
| let src: String = match src { |
| Some(src) if !src.Value().is_empty() => src.Value().into(), |
| _ => { |
| source |
| .upcast::<EventTarget>() |
| .fire_event(atom!("error"), can_gc); |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| }, |
| }; |
| // Step 9.attr.3. |
| let url_record = match base_url.join(&src) { |
| Ok(url) => url, |
| Err(_) => { |
| source |
| .upcast::<EventTarget>() |
| .fire_event(atom!("error"), can_gc); |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| }, |
| }; |
| // Step 9.attr.7 |
| *self.current_src.borrow_mut() = url_record.as_str().into(); |
| // Step 9.attr.8. |
| self.resource_fetch_algorithm(Resource::Url(url_record)); |
| }, |
| } |
| } |
| |
| fn fetch_request(&self, offset: Option<u64>, seek_lock: Option<SeekLock>) { |
| if self.resource_url.borrow().is_none() && self.blob_url.borrow().is_none() { |
| eprintln!("Missing request url"); |
| if let Some(seek_lock) = seek_lock { |
| seek_lock.unlock(/* successful seek */ false); |
| } |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| } |
| |
| let document = self.owner_document(); |
| let destination = match self.media_type_id() { |
| HTMLMediaElementTypeId::HTMLAudioElement => Destination::Audio, |
| HTMLMediaElementTypeId::HTMLVideoElement => Destination::Video, |
| }; |
| let mut headers = HeaderMap::new(); |
| // FIXME(eijebong): Use typed headers once we have a constructor for the range header |
| headers.insert( |
| header::RANGE, |
| HeaderValue::from_str(&format!("bytes={}-", offset.unwrap_or(0))).unwrap(), |
| ); |
| let url = match self.resource_url.borrow().as_ref() { |
| Some(url) => url.clone(), |
| None => self.blob_url.borrow().as_ref().unwrap().clone(), |
| }; |
| |
| let cors_setting = cors_setting_for_element(self.upcast()); |
| let global = self.global(); |
| let request = create_a_potential_cors_request( |
| Some(document.webview_id()), |
| url.clone(), |
| destination, |
| cors_setting, |
| None, |
| global.get_referrer(), |
| document.insecure_requests_policy(), |
| document.has_trustworthy_ancestor_or_current_origin(), |
| global.policy_container(), |
| ) |
| .headers(headers) |
| .origin(document.origin().immutable().clone()) |
| .pipeline_id(Some(self.global().pipeline_id())) |
| .referrer_policy(document.get_referrer_policy()); |
| |
| let mut current_fetch_context = self.current_fetch_context.borrow_mut(); |
| if let Some(ref mut current_fetch_context) = *current_fetch_context { |
| current_fetch_context.cancel(CancelReason::Overridden); |
| } |
| |
| *current_fetch_context = Some(HTMLMediaElementFetchContext::new( |
| request.id, |
| global.core_resource_thread(), |
| )); |
| let listener = |
| HTMLMediaElementFetchListener::new(self, request.id, url.clone(), offset.unwrap_or(0)); |
| |
| self.owner_document().fetch_background(request, listener); |
| |
| // Since we cancelled the previous fetch, from now on the media element |
| // will only receive response data from the new fetch that's been |
| // initiated. This means the player can resume operation, since all subsequent data |
| // pushes will originate from the new seek offset. |
| if let Some(seek_lock) = seek_lock { |
| seek_lock.unlock(/* successful seek */ true); |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#eligible-for-autoplay> |
| fn eligible_for_autoplay(&self) -> bool { |
| // its can autoplay flag is true; |
| self.autoplaying.get() && |
| |
| // its paused attribute is true; |
| self.Paused() && |
| |
| // it has an autoplay attribute specified; |
| self.Autoplay() && |
| |
| // its node document's active sandboxing flag set does not have the sandboxed automatic |
| // features browsing context flag set; and |
| { |
| let document = self.owner_document(); |
| |
| !document.has_active_sandboxing_flag( |
| SandboxingFlagSet::SANDBOXED_AUTOMATIC_FEATURES_BROWSING_CONTEXT_FLAG, |
| ) |
| } |
| |
| // its node document is allowed to use the "autoplay" feature. |
| // TODO: Feature policy: https://html.spec.whatwg.org/iframe-embed-object.html#allowed-to-use |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#concept-media-load-resource |
| fn resource_fetch_algorithm(&self, resource: Resource) { |
| if let Err(e) = self.setup_media_player(&resource) { |
| eprintln!("Setup media player error {:?}", e); |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| } |
| |
| // Steps 1-2. |
| // Unapplicable, the `resource` variable already conveys which mode |
| // is in use. |
| |
| // Step 3. |
| // FIXME(nox): Remove all media-resource-specific text tracks. |
| |
| // Step 4. |
| match resource { |
| Resource::Url(url) => { |
| // Step 4.remote.1. |
| if self.Preload() == "none" && !self.autoplaying.get() { |
| // Step 4.remote.1.1. |
| self.network_state.set(NetworkState::Idle); |
| |
| // Step 4.remote.1.2. |
| let owner_global = self.owner_global(); |
| let task_manager = owner_global.task_manager(); |
| let task_source = task_manager.media_element_task_source(); |
| task_source.queue_simple_event(self.upcast(), atom!("suspend")); |
| |
| // Step 4.remote.1.3. |
| let this = Trusted::new(self); |
| task_source.queue(task!(set_media_delay_load_event_flag_to_false: move || { |
| this.root().delay_load_event(false, CanGc::note()); |
| })); |
| |
| // Steps 4.remote.1.4. |
| // FIXME(nox): Somehow we should wait for the task from previous |
| // step to be ran before continuing. |
| |
| // Steps 4.remote.1.5-4.remote.1.7. |
| // FIXME(nox): Wait for an implementation-defined event and |
| // then continue with the normal set of steps instead of just |
| // returning. |
| return; |
| } |
| |
| // Step 4.remote.2. |
| *self.resource_url.borrow_mut() = Some(url); |
| self.fetch_request(None, None); |
| }, |
| Resource::Object => { |
| if let Some(ref src_object) = *self.src_object.borrow() { |
| match src_object { |
| SrcObject::Blob(blob) => { |
| let blob_url = URL::CreateObjectURL(&self.global(), blob); |
| *self.blob_url.borrow_mut() = |
| Some(ServoUrl::parse(&blob_url).expect("infallible")); |
| self.fetch_request(None, None); |
| }, |
| SrcObject::MediaStream(stream) => { |
| let tracks = &*stream.get_tracks(); |
| for (pos, track) in tracks.iter().enumerate() { |
| if self |
| .player |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .lock() |
| .unwrap() |
| .set_stream(&track.id(), pos == tracks.len() - 1) |
| .is_err() |
| { |
| self.queue_dedicated_media_source_failure_steps(); |
| } |
| } |
| }, |
| } |
| } |
| }, |
| } |
| } |
| |
| /// Queues a task to run the [dedicated media source failure steps][steps]. |
| /// |
| /// [steps]: https://html.spec.whatwg.org/multipage/#dedicated-media-source-failure-steps |
| fn queue_dedicated_media_source_failure_steps(&self) { |
| let this = Trusted::new(self); |
| let generation_id = self.generation_id.get(); |
| self.take_pending_play_promises(Err(Error::NotSupported)); |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue(task!(dedicated_media_source_failure_steps: move || { |
| let this = this.root(); |
| if generation_id != this.generation_id.get() { |
| return; |
| } |
| |
| this.fulfill_in_flight_play_promises(|| { |
| // Step 1. |
| this.error.set(Some(&*MediaError::new( |
| &this.owner_window(), |
| MEDIA_ERR_SRC_NOT_SUPPORTED, CanGc::note()))); |
| |
| // Step 2. |
| this.AudioTracks().clear(); |
| this.VideoTracks().clear(); |
| |
| // Step 3. |
| this.network_state.set(NetworkState::NoSource); |
| |
| // Step 4. |
| this.show_poster.set(true); |
| |
| // Step 5. |
| this.upcast::<EventTarget>().fire_event(atom!("error"), CanGc::note()); |
| |
| if let Some(ref player) = *this.player.borrow() { |
| if let Err(e) = player.lock().unwrap().stop() { |
| eprintln!("Could not stop player {:?}", e); |
| } |
| } |
| |
| // Step 6. |
| // Done after running this closure in |
| // `fulfill_in_flight_play_promises`. |
| }); |
| |
| // Step 7. |
| this.delay_load_event(false, CanGc::note()); |
| })); |
| } |
| |
| fn queue_ratechange_event(&self) { |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("ratechange")); |
| } |
| |
| fn in_error_state(&self) -> bool { |
| self.error.get().is_some() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#potentially-playing> |
| fn is_potentially_playing(&self) -> bool { |
| !self.paused.get() && |
| !self.Ended() && |
| self.error.get().is_none() && |
| !self.is_blocked_media_element() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#blocked-media-element |
| fn is_blocked_media_element(&self) -> bool { |
| self.ready_state.get() <= ReadyState::HaveCurrentData || |
| self.is_paused_for_user_interaction() || |
| self.is_paused_for_in_band_content() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#paused-for-user-interaction |
| fn is_paused_for_user_interaction(&self) -> bool { |
| // FIXME: we will likely be able to fill this placeholder once (if) we |
| // implement the MediaSession API. |
| false |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#paused-for-in-band-content |
| fn is_paused_for_in_band_content(&self) -> bool { |
| // FIXME: we will likely be able to fill this placeholder once (if) we |
| // implement https://github.com/servo/servo/issues/22314 |
| false |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#media-element-load-algorithm |
| fn media_element_load_algorithm(&self, can_gc: CanGc) { |
| // Reset the flag that signals whether loadeddata was ever fired for |
| // this invokation of the load algorithm. |
| self.fired_loadeddata_event.set(false); |
| |
| // Step 1-2. |
| self.generation_id.set(self.generation_id.get() + 1); |
| |
| // Steps 3-4. |
| while !self.in_flight_play_promises_queue.borrow().is_empty() { |
| self.fulfill_in_flight_play_promises(|| ()); |
| } |
| |
| let global = self.owner_global(); |
| let task_manager = global.task_manager(); |
| let task_source = task_manager.media_element_task_source(); |
| |
| // Step 5. |
| let network_state = self.network_state.get(); |
| if network_state == NetworkState::Loading || network_state == NetworkState::Idle { |
| task_source.queue_simple_event(self.upcast(), atom!("abort")); |
| } |
| |
| // Step 6. |
| if network_state != NetworkState::Empty { |
| // Step 6.1. |
| task_source.queue_simple_event(self.upcast(), atom!("emptied")); |
| |
| // Step 6.2. |
| if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { |
| current_fetch_context.cancel(CancelReason::Error); |
| } |
| |
| // Step 6.3. |
| // FIXME(nox): Detach MediaSource media provider object. |
| |
| // Step 6.4. |
| self.AudioTracks().clear(); |
| self.VideoTracks().clear(); |
| |
| // Step 6.5. |
| if self.ready_state.get() != ReadyState::HaveNothing { |
| self.change_ready_state(ReadyState::HaveNothing); |
| } |
| |
| // Step 6.6. |
| if !self.Paused() { |
| // Step 6.6.1. |
| self.paused.set(true); |
| |
| // Step 6.6.2. |
| self.take_pending_play_promises(Err(Error::Abort)); |
| self.fulfill_in_flight_play_promises(|| ()); |
| } |
| |
| // Step 6.7. If seeking is true, set it to false. |
| if self.seeking.get() { |
| self.seeking.set(false); |
| } |
| |
| // Step 6.8. |
| let queue_timeupdate_event = self.playback_position.get() != 0.; |
| self.playback_position.set(0.); |
| if queue_timeupdate_event { |
| task_source.queue_simple_event(self.upcast(), atom!("timeupdate")); |
| } |
| |
| // Step 6.9. |
| // FIXME(nox): Set timeline offset to NaN. |
| |
| // Step 6.10. |
| self.duration.set(f64::NAN); |
| } |
| |
| // Step 7. |
| self.playbackRate.set(self.defaultPlaybackRate.get()); |
| |
| // Step 8. |
| self.error.set(None); |
| self.autoplaying.set(true); |
| |
| // Step 9. |
| self.invoke_resource_selection_algorithm(can_gc); |
| |
| // Step 10. |
| // FIXME(nox): Stop playback of any previously running media resource. |
| } |
| |
| /// Appends a promise to the list of pending play promises. |
| fn push_pending_play_promise(&self, promise: &Rc<Promise>) { |
| self.pending_play_promises |
| .borrow_mut() |
| .push(promise.clone()); |
| } |
| |
| /// Takes the pending play promises. |
| /// |
| /// The result with which these promises will be fulfilled is passed here |
| /// and this method returns nothing because we actually just move the |
| /// current list of pending play promises to the |
| /// `in_flight_play_promises_queue` field. |
| /// |
| /// Each call to this method must be followed by a call to |
| /// `fulfill_in_flight_play_promises`, to actually fulfill the promises |
| /// which were taken and moved to the in-flight queue. |
| fn take_pending_play_promises(&self, result: ErrorResult) { |
| let pending_play_promises = std::mem::take(&mut *self.pending_play_promises.borrow_mut()); |
| self.in_flight_play_promises_queue |
| .borrow_mut() |
| .push_back((pending_play_promises.into(), result)); |
| } |
| |
| /// Fulfills the next in-flight play promises queue after running a closure. |
| /// |
| /// See the comment on `take_pending_play_promises` for why this method |
| /// does not take a list of promises to fulfill. Callers cannot just pop |
| /// the front list off of `in_flight_play_promises_queue` and later fulfill |
| /// the promises because that would mean putting |
| /// `#[cfg_attr(crown, allow(crown::unrooted_must_root))]` on even more functions, potentially |
| /// hiding actual safety bugs. |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| fn fulfill_in_flight_play_promises<F>(&self, f: F) |
| where |
| F: FnOnce(), |
| { |
| let (promises, result) = self |
| .in_flight_play_promises_queue |
| .borrow_mut() |
| .pop_front() |
| .expect("there should be at least one list of in flight play promises"); |
| f(); |
| for promise in &*promises { |
| match result { |
| Ok(ref value) => promise.resolve_native(value, CanGc::note()), |
| Err(ref error) => promise.reject_error(error.clone(), CanGc::note()), |
| } |
| } |
| } |
| |
| /// Handles insertion of `source` children. |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#the-source-element:nodes-are-inserted> |
| pub(crate) fn handle_source_child_insertion(&self, can_gc: CanGc) { |
| if self.upcast::<Element>().has_attribute(&local_name!("src")) { |
| return; |
| } |
| if self.network_state.get() != NetworkState::Empty { |
| return; |
| } |
| self.media_element_load_algorithm(can_gc); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-seek |
| fn seek(&self, time: f64, _approximate_for_speed: bool) { |
| // Step 1. |
| self.show_poster.set(false); |
| |
| // Step 2. |
| if self.ready_state.get() == ReadyState::HaveNothing { |
| return; |
| } |
| |
| // Step 3. |
| // The fetch request associated with this seek already takes |
| // care of cancelling any previous requests. |
| |
| // Step 4. |
| // The flag will be cleared when the media engine tells us the seek was done. |
| self.seeking.set(true); |
| |
| // Step 5. |
| // XXX(ferjm) The rest of the steps should be run in parallel, so seeking cancelation |
| // can be done properly. No other browser does it yet anyway. |
| |
| // Step 6. |
| let time = f64::min(time, self.Duration()); |
| |
| // Step 7. |
| let time = f64::max(time, 0.); |
| |
| // Step 8. |
| let seekable = self.Seekable(); |
| if seekable.Length() == 0 { |
| self.seeking.set(false); |
| return; |
| } |
| let mut nearest_seekable_position = 0.0; |
| let mut in_seekable_range = false; |
| let mut nearest_seekable_distance = f64::MAX; |
| for i in 0..seekable.Length() { |
| let start = seekable.Start(i).unwrap().abs(); |
| let end = seekable.End(i).unwrap().abs(); |
| if time >= start && time <= end { |
| nearest_seekable_position = time; |
| in_seekable_range = true; |
| break; |
| } else if time < start { |
| let distance = start - time; |
| if distance < nearest_seekable_distance { |
| nearest_seekable_distance = distance; |
| nearest_seekable_position = start; |
| } |
| } else { |
| let distance = time - end; |
| if distance < nearest_seekable_distance { |
| nearest_seekable_distance = distance; |
| nearest_seekable_position = end; |
| } |
| } |
| } |
| let time = if in_seekable_range { |
| time |
| } else { |
| nearest_seekable_position |
| }; |
| |
| // Step 9. |
| // servo-media with gstreamer does not support inaccurate seeking for now. |
| |
| // Step 10. |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("seeking")); |
| |
| // Step 11. |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(e) = player.lock().unwrap().seek(time) { |
| eprintln!("Seek error {:?}", e); |
| } |
| } |
| |
| // The rest of the steps are handled when the media engine signals a |
| // ready state change or otherwise satisfies seek completion and signals |
| // a position change. |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-seek |
| fn seek_end(&self) { |
| // Step 14. |
| self.seeking.set(false); |
| |
| // Step 15. |
| self.time_marches_on(); |
| |
| // Step 16. |
| let global = self.owner_global(); |
| let task_manager = global.task_manager(); |
| let task_source = task_manager.media_element_task_source(); |
| task_source.queue_simple_event(self.upcast(), atom!("timeupdate")); |
| |
| // Step 17. |
| task_source.queue_simple_event(self.upcast(), atom!("seeked")); |
| } |
| |
| fn set_player_id(&self, player_id: u64) { |
| self.droppable.set_player_id(player_id); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#poster-frame> |
| pub(crate) fn set_poster_frame(&self, image: Option<Arc<RasterImage>>) { |
| let queue_postershown_event = pref!(media_testing_enabled) && image.is_some(); |
| |
| self.video_renderer.lock().unwrap().set_poster_frame(image); |
| |
| self.upcast::<Node>().dirty(NodeDamage::Other); |
| |
| if queue_postershown_event { |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("postershown")); |
| } |
| } |
| |
| fn setup_media_player(&self, resource: &Resource) -> Result<(), ()> { |
| let stream_type = match *resource { |
| Resource::Object => { |
| if let Some(ref src_object) = *self.src_object.borrow() { |
| match src_object { |
| SrcObject::MediaStream(_) => StreamType::Stream, |
| _ => StreamType::Seekable, |
| } |
| } else { |
| return Err(()); |
| } |
| }, |
| _ => StreamType::Seekable, |
| }; |
| |
| let window = self.owner_window(); |
| let (action_sender, action_receiver) = ipc::channel::<PlayerEvent>().unwrap(); |
| let video_renderer: Option<Arc<Mutex<dyn VideoFrameRenderer>>> = match self.media_type_id() |
| { |
| HTMLMediaElementTypeId::HTMLAudioElement => None, |
| HTMLMediaElementTypeId::HTMLVideoElement => Some(self.video_renderer.clone()), |
| }; |
| |
| let audio_renderer = self.audio_renderer.borrow().as_ref().cloned(); |
| |
| let pipeline_id = window.pipeline_id(); |
| let client_context_id = |
| ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get()); |
| let player = ServoMedia::get().create_player( |
| &client_context_id, |
| stream_type, |
| action_sender, |
| video_renderer, |
| audio_renderer, |
| Box::new(window.get_player_context()), |
| ); |
| let player_id = { |
| let player_guard = player.lock().unwrap(); |
| |
| if let Err(e) = player_guard.set_mute(self.muted.get()) { |
| log::warn!("Could not set mute state: {:?}", e); |
| } |
| |
| player_guard.get_id() |
| }; |
| |
| *self.player.borrow_mut() = Some(player); |
| |
| let trusted_node = Trusted::new(self); |
| let task_source = self |
| .owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .to_sendable(); |
| ROUTER.add_typed_route( |
| action_receiver, |
| Box::new(move |message| { |
| let event = message.unwrap(); |
| trace!("Player event {:?}", event); |
| let this = trusted_node.clone(); |
| task_source.queue(task!(handle_player_event: move || { |
| this.root().handle_player_event(player_id, &event, CanGc::note()); |
| })); |
| }), |
| ); |
| |
| // GLPlayer thread setup |
| let (player_id, image_receiver) = window |
| .get_player_context() |
| .glplayer_thread_sender |
| .map(|pipeline| { |
| let (image_sender, image_receiver) = channel().unwrap(); |
| pipeline |
| .send(GLPlayerMsg::RegisterPlayer(image_sender)) |
| .unwrap(); |
| match image_receiver.recv().unwrap() { |
| GLPlayerMsgForward::PlayerId(id) => (id, Some(image_receiver)), |
| _ => unreachable!(), |
| } |
| }) |
| .unwrap_or((0, None)); |
| |
| self.set_player_id(player_id); |
| self.video_renderer.lock().unwrap().player_id = Some(player_id); |
| |
| if let Some(image_receiver) = image_receiver { |
| let trusted_node = Trusted::new(self); |
| let task_source = self |
| .owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .to_sendable(); |
| ROUTER.add_typed_route( |
| image_receiver, |
| Box::new(move |message| { |
| let msg = message.unwrap(); |
| let this = trusted_node.clone(); |
| task_source.queue(task!(handle_glplayer_message: move || { |
| trace!("GLPlayer message {:?}", msg); |
| let video_renderer = this.root().video_renderer.clone(); |
| |
| match msg { |
| GLPlayerMsgForward::Lock(sender) => { |
| if let Some(holder) = video_renderer |
| .lock() |
| .unwrap() |
| .current_frame_holder |
| .as_mut() { |
| holder.lock(); |
| sender.send(holder.get()).unwrap(); |
| }; |
| }, |
| GLPlayerMsgForward::Unlock() => { |
| if let Some(holder) = video_renderer |
| .lock() |
| .unwrap() |
| .current_frame_holder |
| .as_mut() { holder.unlock() } |
| }, |
| _ => (), |
| } |
| })); |
| }), |
| ); |
| } |
| |
| Ok(()) |
| } |
| |
| pub(crate) fn set_audio_track(&self, idx: usize, enabled: bool) { |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(err) = player.lock().unwrap().set_audio_track(idx as i32, enabled) { |
| warn!("Could not set audio track {:#?}", err); |
| } |
| } |
| } |
| |
| pub(crate) fn set_video_track(&self, idx: usize, enabled: bool) { |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(err) = player.lock().unwrap().set_video_track(idx as i32, enabled) { |
| warn!("Could not set video track {:#?}", err); |
| } |
| } |
| } |
| |
| fn end_of_playback_in_forwards_direction(&self) { |
| // Step 1. If the media element has a loop attribute specified, then seek to the earliest |
| // posible position of the media resource and return. |
| if self.Loop() { |
| self.seek( |
| self.earliest_possible_position(), |
| /* approximate_for_speed*/ false, |
| ); |
| return; |
| } |
| // Step 2. The ended IDL attribute starts returning true once the event loop returns to |
| // step 1. |
| // The **ended playback** condition is implemented inside of |
| // the HTMLMediaElementMethods::Ended method |
| |
| // Step 3. Queue a media element task given the media element and the following steps: |
| let this = Trusted::new(self); |
| |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue(task!(reaches_the_end_steps: move || { |
| let this = this.root(); |
| // Step 3.1. Fire an event named timeupdate at the media element |
| this.upcast::<EventTarget>().fire_event(atom!("timeupdate"), CanGc::note()); |
| |
| // Step 3.2. If the media element has ended playback, the direction of playback is |
| // forwards, and paused is false, then: |
| if this.Ended() && !this.Paused() { |
| // Step 3.2.1. Set the paused attribute to true |
| this.paused.set(true); |
| |
| // Step 3.2.2. Fire an event named pause at the media element |
| this.upcast::<EventTarget>().fire_event(atom!("pause"), CanGc::note()); |
| |
| // Step 3.2.3. Take pending play promises and reject pending play promises with |
| // the result and an "AbortError" DOMException |
| this.take_pending_play_promises(Err(Error::Abort)); |
| this.fulfill_in_flight_play_promises(|| ()); |
| } |
| |
| // Step 3.3. Fire an event named ended at the media element. |
| this.upcast::<EventTarget>().fire_event(atom!("ended"), CanGc::note()); |
| })); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-have_current_data |
| self.change_ready_state(ReadyState::HaveCurrentData); |
| } |
| |
| fn playback_end(&self) { |
| // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list |
| // => "If the media data can be fetched but is found by inspection to be in |
| // an unsupported format, or can otherwise not be rendered at all" |
| if self.ready_state.get() < ReadyState::HaveMetadata { |
| self.queue_dedicated_media_source_failure_steps(); |
| return; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#reaches-the-end |
| match self.direction_of_playback() { |
| PlaybackDirection::Forwards => self.end_of_playback_in_forwards_direction(), |
| |
| PlaybackDirection::Backwards => { |
| if self.playback_position.get() <= self.earliest_possible_position() { |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("ended")); |
| } |
| }, |
| } |
| } |
| |
| fn playback_error(&self, error: &str, can_gc: CanGc) { |
| error!("Player error: {:?}", error); |
| |
| // If we have already flagged an error condition while processing |
| // the network response, we should silently skip any observable |
| // errors originating while decoding the erroneous response. |
| if self.in_error_state() { |
| return; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#loading-the-media-resource:media-data-13 |
| // 1. The user agent should cancel the fetching process. |
| if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { |
| current_fetch_context.cancel(CancelReason::Error); |
| } |
| // 2. Set the error attribute to the result of creating a MediaError with MEDIA_ERR_DECODE. |
| self.error.set(Some(&*MediaError::new( |
| &self.owner_window(), |
| MEDIA_ERR_DECODE, |
| can_gc, |
| ))); |
| |
| // 3. Set the element's networkState attribute to the NETWORK_IDLE value. |
| self.network_state.set(NetworkState::Idle); |
| |
| // 4. Set the element's delaying-the-load-event flag to false. This stops delaying the load event. |
| self.delay_load_event(false, can_gc); |
| |
| // 5. Fire an event named error at the media element. |
| self.upcast::<EventTarget>() |
| .fire_event(atom!("error"), can_gc); |
| |
| // TODO: 6. Abort the overall resource selection algorithm. |
| } |
| |
| fn playback_metadata_updated( |
| &self, |
| metadata: &servo_media::player::metadata::Metadata, |
| can_gc: CanGc, |
| ) { |
| // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list |
| // => If the media resource is found to have an audio track |
| if !metadata.audio_tracks.is_empty() { |
| for (i, _track) in metadata.audio_tracks.iter().enumerate() { |
| // Step 1. |
| let kind = match i { |
| 0 => DOMString::from("main"), |
| _ => DOMString::new(), |
| }; |
| let window = self.owner_window(); |
| let audio_track = AudioTrack::new( |
| &window, |
| DOMString::new(), |
| kind, |
| DOMString::new(), |
| DOMString::new(), |
| Some(&*self.AudioTracks()), |
| can_gc, |
| ); |
| |
| // Steps 2. & 3. |
| self.AudioTracks().add(&audio_track); |
| |
| // Step 4 |
| if let Some(servo_url) = self.resource_url.borrow().as_ref() { |
| let fragment = MediaFragmentParser::from(servo_url); |
| if let Some(id) = fragment.id() { |
| if audio_track.id() == DOMString::from(id) { |
| self.AudioTracks() |
| .set_enabled(self.AudioTracks().len() - 1, true); |
| } |
| } |
| |
| if fragment.tracks().contains(&audio_track.kind().into()) { |
| self.AudioTracks() |
| .set_enabled(self.AudioTracks().len() - 1, true); |
| } |
| } |
| |
| // Step 5. & 6, |
| if self.AudioTracks().enabled_index().is_none() { |
| self.AudioTracks() |
| .set_enabled(self.AudioTracks().len() - 1, true); |
| } |
| |
| // Steps 7. |
| let event = TrackEvent::new( |
| self.global().as_window(), |
| atom!("addtrack"), |
| false, |
| false, |
| &Some(VideoTrackOrAudioTrackOrTextTrack::AudioTrack(audio_track)), |
| can_gc, |
| ); |
| |
| event |
| .upcast::<Event>() |
| .fire(self.upcast::<EventTarget>(), can_gc); |
| } |
| } |
| |
| // => If the media resource is found to have a video track |
| if !metadata.video_tracks.is_empty() { |
| for (i, _track) in metadata.video_tracks.iter().enumerate() { |
| // Step 1. |
| let kind = match i { |
| 0 => DOMString::from("main"), |
| _ => DOMString::new(), |
| }; |
| let window = self.owner_window(); |
| let video_track = VideoTrack::new( |
| &window, |
| DOMString::new(), |
| kind, |
| DOMString::new(), |
| DOMString::new(), |
| Some(&*self.VideoTracks()), |
| can_gc, |
| ); |
| |
| // Steps 2. & 3. |
| self.VideoTracks().add(&video_track); |
| |
| // Step 4. |
| if let Some(track) = self.VideoTracks().item(0) { |
| if let Some(servo_url) = self.resource_url.borrow().as_ref() { |
| let fragment = MediaFragmentParser::from(servo_url); |
| if let Some(id) = fragment.id() { |
| if track.id() == DOMString::from(id) { |
| self.VideoTracks().set_selected(0, true); |
| } |
| } else if fragment.tracks().contains(&track.kind().into()) { |
| self.VideoTracks().set_selected(0, true); |
| } |
| } |
| } |
| |
| // Step 5. & 6. |
| if self.VideoTracks().selected_index().is_none() { |
| self.VideoTracks() |
| .set_selected(self.VideoTracks().len() - 1, true); |
| } |
| |
| // Steps 7. |
| let event = TrackEvent::new( |
| self.global().as_window(), |
| atom!("addtrack"), |
| false, |
| false, |
| &Some(VideoTrackOrAudioTrackOrTextTrack::VideoTrack(video_track)), |
| can_gc, |
| ); |
| |
| event |
| .upcast::<Event>() |
| .fire(self.upcast::<EventTarget>(), can_gc); |
| } |
| } |
| |
| // => "Once enough of the media data has been fetched to determine the duration..." |
| // Step 1. |
| // servo-media owns the media timeline. |
| |
| // Step 2. |
| // XXX(ferjm) Update the timeline offset. |
| |
| // Step 3. |
| self.playback_position.set(0.); |
| |
| // Step 4. |
| let previous_duration = self.duration.get(); |
| if let Some(duration) = metadata.duration { |
| self.duration.set(duration.as_secs() as f64); |
| } else { |
| self.duration.set(f64::INFINITY); |
| } |
| if previous_duration != self.duration.get() { |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("durationchange")); |
| } |
| |
| // Step 5. |
| self.handle_resize(Some(metadata.width), Some(metadata.height)); |
| |
| // Step 6. |
| self.change_ready_state(ReadyState::HaveMetadata); |
| |
| // Step 7. |
| let mut jumped = false; |
| |
| // Step 8. |
| if self.default_playback_start_position.get() > 0. { |
| self.seek( |
| self.default_playback_start_position.get(), |
| /* approximate_for_speed*/ false, |
| ); |
| jumped = true; |
| } |
| |
| // Step 9. |
| self.default_playback_start_position.set(0.); |
| |
| // Steps 10 and 11. |
| if let Some(servo_url) = self.resource_url.borrow().as_ref() { |
| let fragment = MediaFragmentParser::from(servo_url); |
| if let Some(start) = fragment.start() { |
| if start > 0. && start < self.duration.get() { |
| self.playback_position.set(start); |
| if !jumped { |
| self.seek(self.playback_position.get(), false) |
| } |
| } |
| } |
| } |
| |
| // Step 12 & 13 are already handled by the earlier media track processing. |
| |
| // We wait until we have metadata to render the controls, so we render them |
| // with the appropriate size. |
| if self.Controls() { |
| self.render_controls(can_gc); |
| } |
| |
| let global = self.global(); |
| let window = global.as_window(); |
| |
| // Update the media session metadata title with the obtained metadata. |
| window.Navigator().MediaSession().update_title( |
| metadata |
| .title |
| .clone() |
| .unwrap_or(window.get_url().into_string()), |
| ); |
| } |
| |
| fn playback_video_frame_updated(&self) { |
| // Check if the frame was resized |
| if let Some(frame) = self.video_renderer.lock().unwrap().current_frame { |
| self.handle_resize(Some(frame.width as u32), Some(frame.height as u32)); |
| } |
| } |
| |
| fn playback_need_data(&self) { |
| // The player needs more data. |
| // If we already have a valid fetch request, we do nothing. |
| // Otherwise, if we have no request and the previous request was |
| // cancelled because we got an EnoughData event, we restart |
| // fetching where we left. |
| if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() { |
| if let Some(reason) = current_fetch_context.cancel_reason() { |
| // XXX(ferjm) Ideally we should just create a fetch request from |
| // where we left. But keeping track of the exact next byte that the |
| // media backend expects is not the easiest task, so I'm simply |
| // seeking to the current playback position for now which will create |
| // a new fetch request for the last rendered frame. |
| if *reason == CancelReason::Backoff { |
| self.seek(self.playback_position.get(), false); |
| } |
| return; |
| } |
| } |
| |
| if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { |
| if let Err(e) = { |
| let mut data_source = current_fetch_context.data_source().borrow_mut(); |
| data_source.set_locked(false); |
| data_source.process_into_player_from_queue(self.player.borrow().as_ref().unwrap()) |
| } { |
| // If we are pushing too much data and we know that we can |
| // restart the download later from where we left, we cancel |
| // the current request. Otherwise, we continue the request |
| // assuming that we may drop some frames. |
| if e == PlayerError::EnoughData { |
| current_fetch_context.cancel(CancelReason::Backoff); |
| } |
| } |
| } |
| } |
| |
| fn playback_enough_data(&self) { |
| self.change_ready_state(ReadyState::HaveEnoughData); |
| |
| // The player has enough data and it is asking us to stop pushing |
| // bytes, so we cancel the ongoing fetch request iff we are able |
| // to restart it from where we left. Otherwise, we continue the |
| // current fetch request, assuming that some frames will be dropped. |
| if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { |
| if current_fetch_context.is_seekable() { |
| current_fetch_context.cancel(CancelReason::Backoff); |
| } |
| } |
| } |
| |
| fn playback_position_changed(&self, position: u64) { |
| let position = position as f64; |
| let _ = self |
| .played |
| .borrow_mut() |
| .add(self.playback_position.get(), position); |
| self.playback_position.set(position); |
| self.time_marches_on(); |
| let media_position_state = |
| MediaPositionState::new(self.duration.get(), self.playbackRate.get(), position); |
| debug!( |
| "Sending media session event set position state {:?}", |
| media_position_state |
| ); |
| self.send_media_session_event(MediaSessionEvent::SetPositionState(media_position_state)); |
| } |
| |
| fn playback_seek_done(&self) { |
| // Continuation of |
| // https://html.spec.whatwg.org/multipage/#dom-media-seek |
| |
| // Step 13. |
| let task = MediaElementMicrotask::Seeked { |
| elem: DomRoot::from_ref(self), |
| generation_id: self.generation_id.get(), |
| }; |
| ScriptThread::await_stable_state(Microtask::MediaElement(task)); |
| } |
| |
| fn playback_state_changed(&self, state: &PlaybackState) { |
| let mut media_session_playback_state = MediaSessionPlaybackState::None_; |
| match *state { |
| PlaybackState::Paused => { |
| media_session_playback_state = MediaSessionPlaybackState::Paused; |
| if self.ready_state.get() == ReadyState::HaveMetadata { |
| self.change_ready_state(ReadyState::HaveEnoughData); |
| } |
| }, |
| PlaybackState::Playing => { |
| media_session_playback_state = MediaSessionPlaybackState::Playing; |
| }, |
| PlaybackState::Buffering => { |
| // Do not send the media session playback state change event |
| // in this case as a None_ state is expected to clean up the |
| // session. |
| return; |
| }, |
| _ => {}, |
| }; |
| debug!( |
| "Sending media session event playback state changed to {:?}", |
| media_session_playback_state |
| ); |
| self.send_media_session_event(MediaSessionEvent::PlaybackStateChange( |
| media_session_playback_state, |
| )); |
| } |
| |
| fn handle_player_event(&self, player_id: usize, event: &PlayerEvent, can_gc: CanGc) { |
| // Ignore the asynchronous event from previous player. |
| if self |
| .player |
| .borrow() |
| .as_ref() |
| .is_none_or(|player| player.lock().unwrap().get_id() != player_id) |
| { |
| return; |
| } |
| |
| match *event { |
| PlayerEvent::EndOfStream => self.playback_end(), |
| PlayerEvent::Error(ref error) => self.playback_error(error, can_gc), |
| PlayerEvent::VideoFrameUpdated => self.playback_video_frame_updated(), |
| PlayerEvent::MetadataUpdated(ref metadata) => { |
| self.playback_metadata_updated(metadata, can_gc) |
| }, |
| PlayerEvent::NeedData => self.playback_need_data(), |
| PlayerEvent::EnoughData => self.playback_enough_data(), |
| PlayerEvent::PositionChanged(position) => self.playback_position_changed(position), |
| PlayerEvent::SeekData(p, ref seek_lock) => { |
| self.fetch_request(Some(p), Some(seek_lock.clone())) |
| }, |
| PlayerEvent::SeekDone(_) => self.playback_seek_done(), |
| PlayerEvent::StateChanged(ref state) => self.playback_state_changed(state), |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#earliest-possible-position |
| fn earliest_possible_position(&self) -> f64 { |
| self.played |
| .borrow() |
| .start(0) |
| .unwrap_or_else(|_| self.playback_position.get()) |
| } |
| |
| fn render_controls(&self, can_gc: CanGc) { |
| let element = self.htmlelement.upcast::<Element>(); |
| if self.ready_state.get() < ReadyState::HaveMetadata || element.is_shadow_host() { |
| // Bail out if we have no metadata yet or |
| // if we are already showing the controls. |
| return; |
| } |
| // FIXME(stevennovaryo): Recheck styling of media element to avoid |
| // reparsing styles. |
| let shadow_root = self |
| .upcast::<Element>() |
| .attach_ua_shadow_root(false, can_gc); |
| let document = self.owner_document(); |
| let script = HTMLScriptElement::new( |
| local_name!("script"), |
| None, |
| &document, |
| None, |
| ElementCreator::ScriptCreated, |
| can_gc, |
| ); |
| // This is our hacky way to temporarily workaround the lack of a privileged |
| // JS context. |
| // The media controls UI accesses the document.servoGetMediaControls(id) API |
| // to get an instance to the media controls ShadowRoot. |
| // `id` needs to match the internally generated UUID assigned to a media element. |
| let id = document.register_media_controls(&shadow_root); |
| let media_controls_script = MEDIA_CONTROL_JS.replace("@@@id@@@", &id); |
| *self.media_controls_id.borrow_mut() = Some(id); |
| script |
| .upcast::<Node>() |
| .set_text_content_for_element(Some(DOMString::from(media_controls_script)), can_gc); |
| if let Err(e) = shadow_root |
| .upcast::<Node>() |
| .AppendChild(script.upcast::<Node>(), can_gc) |
| { |
| warn!("Could not render media controls {:?}", e); |
| return; |
| } |
| |
| let style = HTMLStyleElement::new( |
| local_name!("script"), |
| None, |
| &document, |
| None, |
| ElementCreator::ScriptCreated, |
| can_gc, |
| ); |
| style |
| .upcast::<Node>() |
| .set_text_content_for_element(Some(DOMString::from(MEDIA_CONTROL_CSS)), can_gc); |
| |
| if let Err(e) = shadow_root |
| .upcast::<Node>() |
| .AppendChild(style.upcast::<Node>(), can_gc) |
| { |
| warn!("Could not render media controls {:?}", e); |
| } |
| |
| self.upcast::<Node>().dirty(NodeDamage::Other); |
| } |
| |
| fn remove_controls(&self, can_gc: CanGc) { |
| if let Some(id) = self.media_controls_id.borrow_mut().take() { |
| self.owner_document().unregister_media_controls(&id, can_gc); |
| } |
| } |
| |
| /// Gets the video frame at the current playback position. |
| pub(crate) fn get_current_frame(&self) -> Option<VideoFrame> { |
| self.video_renderer |
| .lock() |
| .unwrap() |
| .current_frame_holder |
| .as_ref() |
| .map(|holder| holder.get_frame()) |
| } |
| |
| /// Gets the current frame of the video element to present, if any. |
| /// <https://html.spec.whatwg.org/multipage/#the-video-element:the-video-element-7> |
| pub(crate) fn get_current_frame_to_present(&self) -> Option<MediaFrame> { |
| let (current_frame, poster_frame) = { |
| let renderer = self.video_renderer.lock().unwrap(); |
| (renderer.current_frame, renderer.poster_frame) |
| }; |
| |
| // If the show poster flag is set (or there is no current video frame to |
| // present) AND there is a poster frame, present that. |
| if (self.show_poster.get() || current_frame.is_none()) && poster_frame.is_some() { |
| return poster_frame; |
| } |
| |
| current_frame |
| } |
| |
| pub(crate) fn clear_current_frame_data(&self) { |
| self.handle_resize(None, None); |
| self.video_renderer.lock().unwrap().current_frame = None; |
| } |
| |
| fn handle_resize(&self, width: Option<u32>, height: Option<u32>) { |
| if let Some(video_elem) = self.downcast::<HTMLVideoElement>() { |
| video_elem.resize(width, height); |
| self.upcast::<Node>().dirty(NodeDamage::Other); |
| } |
| } |
| |
| /// By default the audio is rendered through the audio sink automatically |
| /// selected by the servo-media Player instance. However, in some cases, like |
| /// the WebAudio MediaElementAudioSourceNode, we need to set a custom audio |
| /// renderer. |
| pub(crate) fn set_audio_renderer( |
| &self, |
| audio_renderer: Arc<Mutex<dyn AudioRenderer>>, |
| can_gc: CanGc, |
| ) { |
| *self.audio_renderer.borrow_mut() = Some(audio_renderer); |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(e) = player.lock().unwrap().stop() { |
| eprintln!("Could not stop player {:?}", e); |
| } |
| self.media_element_load_algorithm(can_gc); |
| } |
| } |
| |
| fn send_media_session_event(&self, event: MediaSessionEvent) { |
| let global = self.global(); |
| let media_session = global.as_window().Navigator().MediaSession(); |
| |
| media_session.register_media_instance(self); |
| |
| media_session.send_event(event); |
| } |
| |
| pub(crate) fn set_duration(&self, duration: f64) { |
| self.duration.set(duration); |
| } |
| |
| pub(crate) fn reset(&self) { |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(e) = player.lock().unwrap().stop() { |
| eprintln!("Could not stop player {:?}", e); |
| } |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#concept-media-load-resource> |
| pub(crate) fn origin_is_clean(&self) -> bool { |
| // Step 5.local (media provider object). |
| if self.src_object.borrow().is_some() { |
| // The resource described by the current media resource, if any, |
| // contains the media data. It is CORS-same-origin. |
| return true; |
| } |
| |
| // Step 5.remote (URL record). |
| if self.resource_url.borrow().is_some() { |
| // Update the media data with the contents |
| // of response's unsafe response obtained in this fashion. |
| // Response can be CORS-same-origin or CORS-cross-origin; |
| if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() { |
| return current_fetch_context.origin_is_clean(); |
| } |
| } |
| |
| true |
| } |
| } |
| |
| // XXX Placeholder for [https://github.com/servo/servo/issues/22293] |
| #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] |
| enum PlaybackDirection { |
| Forwards, |
| #[allow(dead_code)] |
| Backwards, |
| } |
| |
| // XXX Placeholder implementations for: |
| // |
| // - https://github.com/servo/servo/issues/22293 |
| impl HTMLMediaElement { |
| // https://github.com/servo/servo/issues/22293 |
| fn direction_of_playback(&self) -> PlaybackDirection { |
| PlaybackDirection::Forwards |
| } |
| } |
| |
| impl HTMLMediaElementMethods<crate::DomTypeHolder> for HTMLMediaElement { |
| // https://html.spec.whatwg.org/multipage/#dom-media-networkstate |
| fn NetworkState(&self) -> u16 { |
| self.network_state.get() as u16 |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-readystate |
| fn ReadyState(&self) -> u16 { |
| self.ready_state.get() as u16 |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-autoplay |
| make_bool_getter!(Autoplay, "autoplay"); |
| // https://html.spec.whatwg.org/multipage/#dom-media-autoplay |
| make_bool_setter!(SetAutoplay, "autoplay"); |
| |
| // https://html.spec.whatwg.org/multipage/#attr-media-loop |
| make_bool_getter!(Loop, "loop"); |
| // https://html.spec.whatwg.org/multipage/#attr-media-loop |
| make_bool_setter!(SetLoop, "loop"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-defaultmuted |
| make_bool_getter!(DefaultMuted, "muted"); |
| // https://html.spec.whatwg.org/multipage/#dom-media-defaultmuted |
| make_bool_setter!(SetDefaultMuted, "muted"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-controls |
| make_bool_getter!(Controls, "controls"); |
| // https://html.spec.whatwg.org/multipage/#dom-media-controls |
| make_bool_setter!(SetControls, "controls"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-src |
| make_url_getter!(Src, "src"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-src |
| make_url_setter!(SetSrc, "src"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-crossOrigin |
| fn GetCrossOrigin(&self) -> Option<DOMString> { |
| reflect_cross_origin_attribute(self.upcast::<Element>()) |
| } |
| // https://html.spec.whatwg.org/multipage/#dom-media-crossOrigin |
| fn SetCrossOrigin(&self, value: Option<DOMString>, can_gc: CanGc) { |
| set_cross_origin_attribute(self.upcast::<Element>(), value, can_gc); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-muted |
| fn Muted(&self) -> bool { |
| self.muted.get() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-muted |
| fn SetMuted(&self, value: bool) { |
| if self.muted.get() == value { |
| return; |
| } |
| |
| if let Some(ref player) = *self.player.borrow() { |
| let _ = player.lock().unwrap().set_mute(value); |
| } |
| |
| self.muted.set(value); |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("volumechange")); |
| if !self.is_allowed_to_play() { |
| self.internal_pause_steps(); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-srcobject |
| fn GetSrcObject(&self) -> Option<MediaStreamOrBlob> { |
| (*self.src_object.borrow()) |
| .as_ref() |
| .map(|src_object| match src_object { |
| SrcObject::Blob(blob) => MediaStreamOrBlob::Blob(DomRoot::from_ref(blob)), |
| SrcObject::MediaStream(stream) => { |
| MediaStreamOrBlob::MediaStream(DomRoot::from_ref(stream)) |
| }, |
| }) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-srcobject |
| fn SetSrcObject(&self, value: Option<MediaStreamOrBlob>, can_gc: CanGc) { |
| *self.src_object.borrow_mut() = value.map(|value| value.into()); |
| self.media_element_load_algorithm(can_gc); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#attr-media-preload |
| // Missing/Invalid values are user-agent defined. |
| make_enumerated_getter!( |
| Preload, |
| "preload", |
| "none" | "metadata" | "auto", |
| missing => "auto", |
| invalid => "auto" |
| ); |
| |
| // https://html.spec.whatwg.org/multipage/#attr-media-preload |
| make_setter!(SetPreload, "preload"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-currentsrc |
| fn CurrentSrc(&self) -> USVString { |
| USVString(self.current_src.borrow().clone()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-load |
| fn Load(&self, can_gc: CanGc) { |
| self.media_element_load_algorithm(can_gc); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-navigator-canplaytype |
| fn CanPlayType(&self, type_: DOMString) -> CanPlayTypeResult { |
| match ServoMedia::get().can_play_type(&type_) { |
| SupportsMediaType::No => CanPlayTypeResult::_empty, |
| SupportsMediaType::Maybe => CanPlayTypeResult::Maybe, |
| SupportsMediaType::Probably => CanPlayTypeResult::Probably, |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-error |
| fn GetError(&self) -> Option<DomRoot<MediaError>> { |
| self.error.get() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-play |
| fn Play(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> { |
| let promise = Promise::new_in_current_realm(comp, can_gc); |
| // Step 1. |
| // FIXME(nox): Reject promise if not allowed to play. |
| |
| // Step 2. |
| if self |
| .error |
| .get() |
| .is_some_and(|e| e.Code() == MEDIA_ERR_SRC_NOT_SUPPORTED) |
| { |
| promise.reject_error(Error::NotSupported, can_gc); |
| return promise; |
| } |
| |
| // Step 3. |
| self.push_pending_play_promise(&promise); |
| |
| // Step 4. |
| if self.network_state.get() == NetworkState::Empty { |
| self.invoke_resource_selection_algorithm(can_gc); |
| } |
| |
| // Step 5. |
| if self.Ended() && self.direction_of_playback() == PlaybackDirection::Forwards { |
| self.seek( |
| self.earliest_possible_position(), |
| /* approximate_for_speed */ false, |
| ); |
| } |
| |
| let state = self.ready_state.get(); |
| |
| let global = self.owner_global(); |
| let task_manager = global.task_manager(); |
| let task_source = task_manager.media_element_task_source(); |
| if self.Paused() { |
| // Step 6.1. |
| self.paused.set(false); |
| |
| // Step 6.2. |
| if self.show_poster.get() { |
| self.show_poster.set(false); |
| self.time_marches_on(); |
| } |
| |
| // Step 6.3. |
| task_source.queue_simple_event(self.upcast(), atom!("play")); |
| |
| // Step 6.4. |
| match state { |
| ReadyState::HaveNothing | |
| ReadyState::HaveMetadata | |
| ReadyState::HaveCurrentData => { |
| task_source.queue_simple_event(self.upcast(), atom!("waiting")); |
| }, |
| ReadyState::HaveFutureData | ReadyState::HaveEnoughData => { |
| self.notify_about_playing(); |
| }, |
| } |
| } else if state == ReadyState::HaveFutureData || state == ReadyState::HaveEnoughData { |
| // Step 7. |
| self.take_pending_play_promises(Ok(())); |
| let this = Trusted::new(self); |
| let generation_id = self.generation_id.get(); |
| task_source.queue(task!(resolve_pending_play_promises: move || { |
| let this = this.root(); |
| if generation_id != this.generation_id.get() { |
| return; |
| } |
| |
| this.fulfill_in_flight_play_promises(|| { |
| this.play_media(); |
| }); |
| })); |
| } |
| |
| // Step 8. |
| self.autoplaying.set(false); |
| |
| // Step 9. |
| promise |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-pause |
| fn Pause(&self, can_gc: CanGc) { |
| // Step 1 |
| if self.network_state.get() == NetworkState::Empty { |
| self.invoke_resource_selection_algorithm(can_gc); |
| } |
| |
| // Step 2 |
| self.internal_pause_steps(); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-paused |
| fn Paused(&self) -> bool { |
| self.paused.get() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-defaultplaybackrate> |
| fn GetDefaultPlaybackRate(&self) -> Fallible<Finite<f64>> { |
| Ok(Finite::wrap(self.defaultPlaybackRate.get())) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-defaultplaybackrate> |
| fn SetDefaultPlaybackRate(&self, value: Finite<f64>) -> ErrorResult { |
| let min_allowed = -64.0; |
| let max_allowed = 64.0; |
| if *value < min_allowed || *value > max_allowed { |
| return Err(Error::NotSupported); |
| } |
| |
| if *value != self.defaultPlaybackRate.get() { |
| self.defaultPlaybackRate.set(*value); |
| self.queue_ratechange_event(); |
| } |
| |
| Ok(()) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-playbackrate> |
| fn GetPlaybackRate(&self) -> Fallible<Finite<f64>> { |
| Ok(Finite::wrap(self.playbackRate.get())) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-media-playbackrate> |
| fn SetPlaybackRate(&self, value: Finite<f64>) -> ErrorResult { |
| let min_allowed = -64.0; |
| let max_allowed = 64.0; |
| if *value < min_allowed || *value > max_allowed { |
| return Err(Error::NotSupported); |
| } |
| |
| if *value != self.playbackRate.get() { |
| self.playbackRate.set(*value); |
| self.queue_ratechange_event(); |
| if self.is_potentially_playing() { |
| if let Some(ref player) = *self.player.borrow() { |
| if let Err(e) = player.lock().unwrap().set_rate(*value) { |
| warn!("Could not set the playback rate {:?}", e); |
| } |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-duration |
| fn Duration(&self) -> f64 { |
| self.duration.get() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-currenttime |
| fn CurrentTime(&self) -> Finite<f64> { |
| Finite::wrap(if self.default_playback_start_position.get() != 0. { |
| self.default_playback_start_position.get() |
| } else { |
| self.playback_position.get() |
| }) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-currenttime |
| fn SetCurrentTime(&self, time: Finite<f64>) { |
| if self.ready_state.get() == ReadyState::HaveNothing { |
| self.default_playback_start_position.set(*time); |
| } else { |
| self.playback_position.set(*time); |
| self.seek(*time, /* approximate_for_speed */ false); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-seeking |
| fn Seeking(&self) -> bool { |
| self.seeking.get() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#ended-playback |
| fn Ended(&self) -> bool { |
| if self.ready_state.get() < ReadyState::HaveMetadata { |
| return false; |
| } |
| |
| let playback_pos = self.playback_position.get(); |
| |
| match self.direction_of_playback() { |
| PlaybackDirection::Forwards => playback_pos >= self.Duration() && !self.Loop(), |
| PlaybackDirection::Backwards => playback_pos <= self.earliest_possible_position(), |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-fastseek |
| fn FastSeek(&self, time: Finite<f64>) { |
| self.seek(*time, /* approximate_for_speed */ true); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-played |
| fn Played(&self) -> DomRoot<TimeRanges> { |
| TimeRanges::new( |
| self.global().as_window(), |
| self.played.borrow().clone(), |
| CanGc::note(), |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-seekable |
| fn Seekable(&self) -> DomRoot<TimeRanges> { |
| let mut seekable = TimeRangesContainer::default(); |
| if let Some(ref player) = *self.player.borrow() { |
| if let Ok(ranges) = player.lock().unwrap().seekable() { |
| for range in ranges { |
| let _ = seekable.add(range.start, range.end); |
| } |
| } |
| } |
| TimeRanges::new(self.global().as_window(), seekable, CanGc::note()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-buffered |
| fn Buffered(&self) -> DomRoot<TimeRanges> { |
| let mut buffered = TimeRangesContainer::default(); |
| if let Some(ref player) = *self.player.borrow() { |
| if let Ok(ranges) = player.lock().unwrap().buffered() { |
| for range in ranges { |
| let _ = buffered.add(range.start, range.end); |
| } |
| } |
| } |
| TimeRanges::new(self.global().as_window(), buffered, CanGc::note()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-audiotracks |
| fn AudioTracks(&self) -> DomRoot<AudioTrackList> { |
| let window = self.owner_window(); |
| self.audio_tracks_list |
| .or_init(|| AudioTrackList::new(&window, &[], Some(self), CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-videotracks |
| fn VideoTracks(&self) -> DomRoot<VideoTrackList> { |
| let window = self.owner_window(); |
| self.video_tracks_list |
| .or_init(|| VideoTrackList::new(&window, &[], Some(self), CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-texttracks |
| fn TextTracks(&self) -> DomRoot<TextTrackList> { |
| let window = self.owner_window(); |
| self.text_tracks_list |
| .or_init(|| TextTrackList::new(&window, &[], CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-addtexttrack |
| fn AddTextTrack( |
| &self, |
| kind: TextTrackKind, |
| label: DOMString, |
| language: DOMString, |
| ) -> DomRoot<TextTrack> { |
| let window = self.owner_window(); |
| // Step 1 & 2 |
| // FIXME(#22314, dlrobertson) set the ready state to Loaded |
| let track = TextTrack::new( |
| &window, |
| "".into(), |
| kind, |
| label, |
| language, |
| TextTrackMode::Hidden, |
| None, |
| CanGc::note(), |
| ); |
| // Step 3 & 4 |
| self.TextTracks().add(&track); |
| // Step 5 |
| DomRoot::from_ref(&track) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-volume |
| fn GetVolume(&self) -> Fallible<Finite<f64>> { |
| Ok(Finite::wrap(self.volume.get())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-media-volume |
| fn SetVolume(&self, value: Finite<f64>) -> ErrorResult { |
| let minimum_volume = 0.0; |
| let maximum_volume = 1.0; |
| if *value < minimum_volume || *value > maximum_volume { |
| return Err(Error::IndexSize); |
| } |
| |
| if *value != self.volume.get() { |
| self.volume.set(*value); |
| |
| self.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(self.upcast(), atom!("volumechange")); |
| if !self.is_allowed_to_play() { |
| self.internal_pause_steps(); |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl VirtualMethods for HTMLMediaElement { |
| fn super_type(&self) -> Option<&dyn VirtualMethods> { |
| Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods) |
| } |
| |
| fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) { |
| self.super_type() |
| .unwrap() |
| .attribute_mutated(attr, mutation, can_gc); |
| |
| match *attr.local_name() { |
| local_name!("muted") => { |
| self.SetMuted(mutation.new_value(attr).is_some()); |
| }, |
| local_name!("src") => { |
| if mutation.new_value(attr).is_none() { |
| self.clear_current_frame_data(); |
| return; |
| } |
| self.media_element_load_algorithm(CanGc::note()); |
| }, |
| local_name!("controls") => { |
| if mutation.new_value(attr).is_some() { |
| self.render_controls(can_gc); |
| } else { |
| self.remove_controls(can_gc); |
| } |
| }, |
| _ => (), |
| }; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#playing-the-media-resource:remove-an-element-from-a-document |
| fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { |
| self.super_type().unwrap().unbind_from_tree(context, can_gc); |
| |
| self.remove_controls(can_gc); |
| |
| if context.tree_connected { |
| let task = MediaElementMicrotask::PauseIfNotInDocument { |
| elem: DomRoot::from_ref(self), |
| }; |
| ScriptThread::await_stable_state(Microtask::MediaElement(task)); |
| } |
| } |
| } |
| |
| #[derive(JSTraceable, MallocSizeOf)] |
| pub(crate) enum MediaElementMicrotask { |
| ResourceSelection { |
| elem: DomRoot<HTMLMediaElement>, |
| generation_id: u32, |
| #[no_trace] |
| base_url: ServoUrl, |
| }, |
| PauseIfNotInDocument { |
| elem: DomRoot<HTMLMediaElement>, |
| }, |
| Seeked { |
| elem: DomRoot<HTMLMediaElement>, |
| generation_id: u32, |
| }, |
| } |
| |
| impl MicrotaskRunnable for MediaElementMicrotask { |
| fn handler(&self, can_gc: CanGc) { |
| match self { |
| &MediaElementMicrotask::ResourceSelection { |
| ref elem, |
| generation_id, |
| ref base_url, |
| } => { |
| if generation_id == elem.generation_id.get() { |
| elem.resource_selection_algorithm_sync(base_url.clone(), can_gc); |
| } |
| }, |
| MediaElementMicrotask::PauseIfNotInDocument { elem } => { |
| if !elem.upcast::<Node>().is_connected() { |
| elem.internal_pause_steps(); |
| } |
| }, |
| &MediaElementMicrotask::Seeked { |
| ref elem, |
| generation_id, |
| } => { |
| if generation_id == elem.generation_id.get() { |
| elem.seek_end(); |
| } |
| }, |
| } |
| } |
| |
| fn enter_realm(&self) -> JSAutoRealm { |
| match self { |
| &MediaElementMicrotask::ResourceSelection { ref elem, .. } | |
| &MediaElementMicrotask::PauseIfNotInDocument { ref elem } | |
| &MediaElementMicrotask::Seeked { ref elem, .. } => enter_realm(&**elem), |
| } |
| } |
| } |
| |
| enum Resource { |
| Object, |
| Url(ServoUrl), |
| } |
| |
| #[derive(Debug, MallocSizeOf, PartialEq)] |
| enum DataBuffer { |
| Payload(Vec<u8>), |
| EndOfStream, |
| } |
| |
| #[derive(MallocSizeOf)] |
| struct BufferedDataSource { |
| /// During initial setup and seeking (including clearing the buffer queue |
| /// and resetting the end-of-stream state), the data source should be locked and |
| /// any request for processing should be ignored until the media player informs us |
| /// via the NeedData event that it is ready to accept incoming data. |
| locked: Cell<bool>, |
| /// Temporary storage for incoming data. |
| buffers: VecDeque<DataBuffer>, |
| } |
| |
| impl BufferedDataSource { |
| fn new() -> BufferedDataSource { |
| BufferedDataSource { |
| locked: Cell::new(true), |
| buffers: VecDeque::default(), |
| } |
| } |
| |
| fn set_locked(&self, locked: bool) { |
| self.locked.set(locked) |
| } |
| |
| fn add_buffer_to_queue(&mut self, buffer: DataBuffer) { |
| debug_assert_ne!( |
| self.buffers.back(), |
| Some(&DataBuffer::EndOfStream), |
| "The media backend not expects any further data after end of stream" |
| ); |
| |
| self.buffers.push_back(buffer); |
| } |
| |
| fn process_into_player_from_queue( |
| &mut self, |
| player: &Arc<Mutex<dyn Player>>, |
| ) -> Result<(), PlayerError> { |
| // Early out if any request for processing should be ignored. |
| if self.locked.get() { |
| return Ok(()); |
| } |
| |
| while let Some(buffer) = self.buffers.pop_front() { |
| match buffer { |
| DataBuffer::Payload(payload) => { |
| if let Err(e) = player.lock().unwrap().push_data(payload) { |
| warn!("Could not push input data to player {:?}", e); |
| return Err(e); |
| } |
| }, |
| DataBuffer::EndOfStream => { |
| if let Err(e) = player.lock().unwrap().end_of_stream() { |
| warn!("Could not signal EOS to player {:?}", e); |
| return Err(e); |
| } |
| }, |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn reset(&mut self) { |
| self.locked.set(true); |
| self.buffers.clear(); |
| } |
| } |
| |
| /// Indicates the reason why a fetch request was cancelled. |
| #[derive(Debug, MallocSizeOf, PartialEq)] |
| enum CancelReason { |
| /// We were asked to stop pushing data to the player. |
| Backoff, |
| /// An error ocurred while fetching the media data. |
| Error, |
| /// A new request overrode this one. |
| Overridden, |
| } |
| |
| #[derive(MallocSizeOf)] |
| pub(crate) struct HTMLMediaElementFetchContext { |
| /// The fetch request id. |
| request_id: RequestId, |
| /// Some if the request has been cancelled. |
| cancel_reason: Option<CancelReason>, |
| /// Indicates whether the fetched stream is seekable. |
| is_seekable: bool, |
| /// Indicates whether the fetched stream is origin clean. |
| origin_clean: bool, |
| /// The buffered data source which to be processed by media backend. |
| data_source: DomRefCell<BufferedDataSource>, |
| /// Fetch canceller. Allows cancelling the current fetch request by |
| /// manually calling its .cancel() method or automatically on Drop. |
| fetch_canceller: FetchCanceller, |
| } |
| |
| impl HTMLMediaElementFetchContext { |
| fn new( |
| request_id: RequestId, |
| core_resource_thread: CoreResourceThread, |
| ) -> HTMLMediaElementFetchContext { |
| HTMLMediaElementFetchContext { |
| request_id, |
| cancel_reason: None, |
| is_seekable: false, |
| origin_clean: true, |
| data_source: DomRefCell::new(BufferedDataSource::new()), |
| fetch_canceller: FetchCanceller::new(request_id, core_resource_thread.clone()), |
| } |
| } |
| |
| fn request_id(&self) -> RequestId { |
| self.request_id |
| } |
| |
| fn is_seekable(&self) -> bool { |
| self.is_seekable |
| } |
| |
| fn set_seekable(&mut self, seekable: bool) { |
| self.is_seekable = seekable; |
| } |
| |
| pub(crate) fn origin_is_clean(&self) -> bool { |
| self.origin_clean |
| } |
| |
| fn set_origin_unclean(&mut self) { |
| self.origin_clean = false; |
| } |
| |
| fn data_source(&self) -> &DomRefCell<BufferedDataSource> { |
| &self.data_source |
| } |
| |
| fn cancel(&mut self, reason: CancelReason) { |
| if self.cancel_reason.is_some() { |
| return; |
| } |
| self.cancel_reason = Some(reason); |
| self.data_source.borrow_mut().reset(); |
| self.fetch_canceller.cancel(); |
| } |
| |
| fn cancel_reason(&self) -> &Option<CancelReason> { |
| &self.cancel_reason |
| } |
| } |
| |
| struct HTMLMediaElementFetchListener { |
| /// The element that initiated the request. |
| elem: Trusted<HTMLMediaElement>, |
| /// The response metadata received to date. |
| metadata: Option<Metadata>, |
| /// The generation of the media element when this fetch started. |
| generation_id: u32, |
| /// The fetch request id. |
| request_id: RequestId, |
| /// Time of last progress notification. |
| next_progress_event: Instant, |
| /// Timing data for this resource. |
| resource_timing: ResourceFetchTiming, |
| /// Url for the resource. |
| url: ServoUrl, |
| /// Expected content length of the media asset being fetched or played. |
| expected_content_length: Option<u64>, |
| /// Actual content length of the media asset was fetched. |
| fetched_content_length: u64, |
| /// Discarded content length from the network for the ongoing |
| /// request if range requests are not supported. Seek requests set it |
| /// to the required position (in bytes). |
| content_length_to_discard: u64, |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list |
| impl FetchResponseListener for HTMLMediaElementFetchListener { |
| fn process_request_body(&mut self, _: RequestId) {} |
| |
| fn process_request_eof(&mut self, _: RequestId) {} |
| |
| fn process_response(&mut self, _: RequestId, metadata: Result<FetchMetadata, NetworkError>) { |
| let elem = self.elem.root(); |
| |
| if let Ok(FetchMetadata::Filtered { |
| filtered: FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_), |
| .. |
| }) = metadata |
| { |
| if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { |
| current_fetch_context.set_origin_unclean(); |
| } |
| } |
| |
| self.metadata = metadata.ok().map(|m| match m { |
| FetchMetadata::Unfiltered(m) => m, |
| FetchMetadata::Filtered { unsafe_, .. } => unsafe_, |
| }); |
| |
| if let Some(metadata) = self.metadata.as_ref() { |
| if let Some(headers) = metadata.headers.as_ref() { |
| // For range requests we get the size of the media asset from the Content-Range |
| // header. Otherwise, we get it from the Content-Length header. |
| let content_length = |
| if let Some(content_range) = headers.typed_get::<ContentRange>() { |
| content_range.bytes_len() |
| } else { |
| headers |
| .typed_get::<ContentLength>() |
| .map(|content_length| content_length.0) |
| }; |
| |
| // We only set the expected input size if it changes. |
| if content_length != self.expected_content_length { |
| if let Some(content_length) = content_length { |
| self.expected_content_length = Some(content_length); |
| } |
| } |
| } |
| } |
| |
| // Explicit media player initialization with live/seekable source. |
| if let Err(e) = elem |
| .player |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .lock() |
| .unwrap() |
| .set_input_size(self.expected_content_length.unwrap_or_default()) |
| { |
| warn!("Could not set player input size {:?}", e); |
| } |
| |
| let (status_is_ok, is_seekable) = self.metadata.as_ref().map_or((true, false), |s| { |
| let status = &s.status; |
| ( |
| status.is_success(), |
| *status == StatusCode::PARTIAL_CONTENT || |
| *status == StatusCode::RANGE_NOT_SATISFIABLE, |
| ) |
| }); |
| |
| if is_seekable { |
| // The server supports range requests, |
| if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { |
| current_fetch_context.set_seekable(true); |
| } |
| } |
| |
| // => "If the media data cannot be fetched at all..." |
| if !status_is_ok { |
| // Ensure that the element doesn't receive any further notifications |
| // of the aborted fetch. |
| if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { |
| current_fetch_context.cancel(CancelReason::Error); |
| } |
| elem.queue_dedicated_media_source_failure_steps(); |
| } |
| } |
| |
| fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) { |
| let elem = self.elem.root(); |
| |
| self.fetched_content_length += chunk.len() as u64; |
| |
| // If an error was received previously, we skip processing the payload. |
| if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { |
| if current_fetch_context.cancel_reason().is_some() { |
| return; |
| } |
| |
| // Discard chunk of the response body if fetch context doesn't |
| // support range requests. |
| let payload = if !current_fetch_context.is_seekable() && |
| self.content_length_to_discard != 0 |
| { |
| if chunk.len() as u64 > self.content_length_to_discard { |
| let shrink_chunk = chunk[self.content_length_to_discard as usize..].to_vec(); |
| self.content_length_to_discard = 0; |
| shrink_chunk |
| } else { |
| // Completely discard this response chunk. |
| self.content_length_to_discard -= chunk.len() as u64; |
| return; |
| } |
| } else { |
| chunk |
| }; |
| |
| if let Err(e) = { |
| let mut data_source = current_fetch_context.data_source().borrow_mut(); |
| data_source.add_buffer_to_queue(DataBuffer::Payload(payload)); |
| data_source.process_into_player_from_queue(elem.player.borrow().as_ref().unwrap()) |
| } { |
| // If we are pushing too much data and we know that we can |
| // restart the download later from where we left, we cancel |
| // the current request. Otherwise, we continue the request |
| // assuming that we may drop some frames. |
| if e == PlayerError::EnoughData { |
| current_fetch_context.cancel(CancelReason::Backoff); |
| } |
| return; |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#concept-media-load-resource step 4, |
| // => "If mode is remote" step 2 |
| if Instant::now() > self.next_progress_event { |
| elem.owner_global() |
| .task_manager() |
| .media_element_task_source() |
| .queue_simple_event(elem.upcast(), atom!("progress")); |
| self.next_progress_event = Instant::now() + Duration::from_millis(350); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list |
| fn process_response_eof( |
| &mut self, |
| _: RequestId, |
| status: Result<ResourceFetchTiming, NetworkError>, |
| ) { |
| trace!("process response eof"); |
| |
| let elem = self.elem.root(); |
| |
| // There are no more chunks of the response body forthcoming, so we can |
| // go ahead and notify the media backend not to expect any further data. |
| if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { |
| // On initial state change READY -> PAUSED the media player perform |
| // seek to initial position by event with seek segment (TIME format) |
| // while media stack operates in BYTES format and configuring segment |
| // start and stop positions without the total size of the stream is not |
| // possible. As fallback the media player perform seek with BYTES format |
| // and initiate seek request via "seek-data" callback with required offset. |
| if self.expected_content_length.is_none() && self.fetched_content_length != 0 { |
| if let Err(e) = elem |
| .player |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .lock() |
| .unwrap() |
| .set_input_size(self.fetched_content_length) |
| { |
| warn!("Could not set player input size {:?}", e); |
| } |
| } |
| |
| let mut data_source = current_fetch_context.data_source().borrow_mut(); |
| |
| data_source.add_buffer_to_queue(DataBuffer::EndOfStream); |
| let _ = |
| data_source.process_into_player_from_queue(elem.player.borrow().as_ref().unwrap()); |
| |
| // If an error was previously received we skip processing the payload. |
| if let Some(CancelReason::Error) = current_fetch_context.cancel_reason() { |
| return; |
| } |
| } |
| |
| if status.is_ok() && self.fetched_content_length != 0 { |
| elem.upcast::<EventTarget>() |
| .fire_event(atom!("progress"), CanGc::note()); |
| |
| elem.network_state.set(NetworkState::Idle); |
| |
| elem.upcast::<EventTarget>() |
| .fire_event(atom!("suspend"), CanGc::note()); |
| } |
| // => "If the connection is interrupted after some media data has been received..." |
| else if elem.ready_state.get() != ReadyState::HaveNothing { |
| // If the media backend has already flagged an error, skip any observable |
| // network-related errors. |
| if elem.in_error_state() { |
| return; |
| } |
| |
| // Step 1 |
| if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { |
| current_fetch_context.cancel(CancelReason::Error); |
| } |
| |
| // Step 2 |
| elem.error.set(Some(&*MediaError::new( |
| &elem.owner_window(), |
| MEDIA_ERR_NETWORK, |
| CanGc::note(), |
| ))); |
| |
| // Step 3 |
| elem.network_state.set(NetworkState::Idle); |
| |
| // Step 4. |
| elem.delay_load_event(false, CanGc::note()); |
| |
| // Step 5 |
| elem.upcast::<EventTarget>() |
| .fire_event(atom!("error"), CanGc::note()); |
| } else { |
| // => "If the media data cannot be fetched at all..." |
| elem.queue_dedicated_media_source_failure_steps(); |
| } |
| } |
| |
| fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming { |
| &mut self.resource_timing |
| } |
| |
| fn resource_timing(&self) -> &ResourceFetchTiming { |
| &self.resource_timing |
| } |
| |
| fn submit_resource_timing(&mut self) { |
| network_listener::submit_timing(self, CanGc::note()) |
| } |
| |
| fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) { |
| let global = &self.resource_timing_global(); |
| global.report_csp_violations(violations, None, None); |
| } |
| } |
| |
| impl ResourceTimingListener for HTMLMediaElementFetchListener { |
| fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { |
| let initiator_type = InitiatorType::LocalName( |
| self.elem |
| .root() |
| .upcast::<Element>() |
| .local_name() |
| .to_string(), |
| ); |
| (initiator_type, self.url.clone()) |
| } |
| |
| fn resource_timing_global(&self) -> DomRoot<GlobalScope> { |
| self.elem.root().owner_document().global() |
| } |
| } |
| |
| impl PreInvoke for HTMLMediaElementFetchListener { |
| fn should_invoke(&self) -> bool { |
| let elem = self.elem.root(); |
| |
| if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() { |
| return false; |
| } |
| |
| // A new fetch request was triggered, so we skip processing previous request. |
| elem.current_fetch_context |
| .borrow() |
| .as_ref() |
| .is_some_and(|context| context.request_id() == self.request_id) |
| } |
| } |
| |
| impl HTMLMediaElementFetchListener { |
| fn new(elem: &HTMLMediaElement, request_id: RequestId, url: ServoUrl, offset: u64) -> Self { |
| Self { |
| elem: Trusted::new(elem), |
| metadata: None, |
| generation_id: elem.generation_id.get(), |
| request_id, |
| next_progress_event: Instant::now() + Duration::from_millis(350), |
| resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), |
| url, |
| expected_content_length: None, |
| fetched_content_length: 0, |
| content_length_to_discard: offset, |
| } |
| } |
| } |