diff options
Diffstat (limited to 'components')
-rw-r--r-- | components/compositing/windowing.rs | 6 | ||||
-rw-r--r-- | components/constellation/constellation.rs | 60 | ||||
-rw-r--r-- | components/embedder_traits/lib.rs | 46 | ||||
-rw-r--r-- | components/script/dom/bindings/trace.rs | 8 | ||||
-rw-r--r-- | components/script/dom/htmlmediaelement.rs | 58 | ||||
-rw-r--r-- | components/script/dom/mediametadata.rs | 97 | ||||
-rw-r--r-- | components/script/dom/mediasession.rs | 213 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 2 | ||||
-rw-r--r-- | components/script/dom/navigator.rs | 19 | ||||
-rw-r--r-- | components/script/dom/webidls/MediaMetadata.webidl | 30 | ||||
-rw-r--r-- | components/script/dom/webidls/MediaSession.webidl | 57 | ||||
-rw-r--r-- | components/script/script_thread.rs | 15 | ||||
-rw-r--r-- | components/script_traits/lib.rs | 52 | ||||
-rw-r--r-- | components/script_traits/script_msg.rs | 6 | ||||
-rw-r--r-- | components/servo/lib.rs | 10 |
15 files changed, 664 insertions, 15 deletions
diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index decece701f4..cdc1b7ff883 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -10,7 +10,7 @@ use euclid::Scale; use gleam::gl; use keyboard_types::KeyboardEvent; use msg::constellation_msg::{PipelineId, TopLevelBrowsingContextId, TraversalDirection}; -use script_traits::{MouseButton, TouchEventType, TouchId, WheelDelta}; +use script_traits::{MediaSessionActionType, MouseButton, TouchEventType, TouchId, WheelDelta}; use servo_geometry::DeviceIndependentPixel; use servo_media::player::context::{GlApi, GlContext, NativeDisplay}; use servo_url::ServoUrl; @@ -102,6 +102,9 @@ pub enum WindowEvent { CaptureWebRender, /// Toggle sampling profiler with the given sampling rate and max duration. ToggleSamplingProfiler(Duration, Duration), + /// Sent when the user triggers a media action through the UA exposed media UI + /// (play, pause, seek, etc.). + MediaSessionAction(MediaSessionActionType), } impl Debug for WindowEvent { @@ -132,6 +135,7 @@ impl Debug for WindowEvent { WindowEvent::CaptureWebRender => write!(f, "CaptureWebRender"), WindowEvent::ToggleSamplingProfiler(..) => write!(f, "ToggleSamplingProfiler"), WindowEvent::ExitFullScreen(..) => write!(f, "ExitFullScreen"), + WindowEvent::MediaSessionAction(..) => write!(f, "MediaSessionAction"), } } } diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index c24883f99e5..82b99938f3b 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -112,6 +112,7 @@ use compositing::SendableFrameTree; use crossbeam_channel::{after, never, unbounded, Receiver, Sender}; use devtools_traits::{ChromeToDevtoolsControlMsg, DevtoolsControlMsg}; use embedder_traits::{Cursor, EmbedderMsg, EmbedderProxy, EventLoopWaker}; +use embedder_traits::{MediaSessionEvent, MediaSessionPlaybackState}; use euclid::{default::Size2D as UntypedSize2D, Size2D}; use gfx::font_cache_thread::FontCacheThread; use gfx_traits::Epoch; @@ -139,7 +140,6 @@ use net_traits::{self, FetchResponseMsg, IpcSend, ResourceThreads}; use profile_traits::mem; use profile_traits::time; use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent}; -use script_traits::MouseEventType; use script_traits::{webdriver_msg, LogEntry, ScriptToConstellationChan, ServiceWorkerMsg}; use script_traits::{ AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo, CompositorEvent, @@ -153,6 +153,7 @@ use script_traits::{ IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, TimerSchedulerMsg, }; use script_traits::{LayoutMsg as FromLayoutMsg, ScriptMsg as FromScriptMsg, ScriptThreadFactory}; +use script_traits::{MediaSessionActionType, MouseEventType}; use script_traits::{MessagePortMsg, PortMessageTask, StructuredSerializedData}; use script_traits::{SWManagerMsg, ScopeThings, UpdatePipelineIdReason, WebDriverCommandMsg}; use serde::{Deserialize, Serialize}; @@ -474,6 +475,9 @@ pub struct Constellation<Message, LTF, STF> { /// Mechanism to force the compositor to process events. event_loop_waker: Option<Box<dyn EventLoopWaker>>, + + /// Pipeline ID of the active media session. + active_media_session: Option<PipelineId>, } /// State needed to construct a constellation. @@ -843,6 +847,7 @@ where glplayer_threads: state.glplayer_threads, player_context: state.player_context, event_loop_waker: state.event_loop_waker, + active_media_session: None, }; constellation.run(); @@ -1541,6 +1546,9 @@ where FromCompositorMsg::ExitFullScreen(top_level_browsing_context_id) => { self.handle_exit_fullscreen_msg(top_level_browsing_context_id); }, + FromCompositorMsg::MediaSessionAction(action) => { + self.handle_media_session_action_msg(action); + }, } } @@ -1771,6 +1779,31 @@ where new_value, ); }, + FromScriptMsg::MediaSessionEvent(pipeline_id, event) => { + // Unlikely at this point, but we may receive events coming from + // different media sessions, so we set the active media session based + // on Playing events. + // The last media session claiming to be in playing state is set to + // the active media session. + // Events coming from inactive media sessions are discarded. + if self.active_media_session.is_some() { + match event { + MediaSessionEvent::PlaybackStateChange(ref state) => { + match state { + MediaSessionPlaybackState::Playing | + MediaSessionPlaybackState::Paused => (), + _ => return, + }; + }, + _ => (), + }; + } + self.active_media_session = Some(pipeline_id); + self.embedder_proxy.send(( + Some(source_top_ctx_id), + EmbedderMsg::MediaSessionEvent(event), + )); + }, } } @@ -5019,4 +5052,29 @@ where .send(ToCompositorMsg::SetFrameTree(frame_tree)); } } + + fn handle_media_session_action_msg(&mut self, action: MediaSessionActionType) { + if let Some(media_session_pipeline_id) = self.active_media_session { + let result = match self.pipelines.get(&media_session_pipeline_id) { + None => { + return warn!( + "Pipeline {} got media session action request after closure.", + media_session_pipeline_id, + ) + }, + Some(pipeline) => { + let msg = ConstellationControlMsg::MediaSessionAction( + media_session_pipeline_id, + action, + ); + pipeline.event_loop.send(msg) + }, + }; + if let Err(e) = result { + self.handle_send_error(media_session_pipeline_id, e); + } + } else { + error!("Got a media session action but no active media session is registered"); + } + } } diff --git a/components/embedder_traits/lib.rs b/components/embedder_traits/lib.rs index 949b3ba5acb..9d4e56dd297 100644 --- a/components/embedder_traits/lib.rs +++ b/components/embedder_traits/lib.rs @@ -162,6 +162,9 @@ pub enum EmbedderMsg { Shutdown, /// Report a complete sampled profile ReportProfile(Vec<u8>), + /// Notifies the embedder about media session events + /// (i.e. when there is metadata for the active media session, playback state changes...). + MediaSessionEvent(MediaSessionEvent), } impl Debug for EmbedderMsg { @@ -194,6 +197,7 @@ impl Debug for EmbedderMsg { EmbedderMsg::AllowOpeningBrowser(..) => write!(f, "AllowOpeningBrowser"), EmbedderMsg::BrowserCreated(..) => write!(f, "BrowserCreated"), EmbedderMsg::ReportProfile(..) => write!(f, "ReportProfile"), + EmbedderMsg::MediaSessionEvent(..) => write!(f, "MediaSessionEvent"), } } } @@ -202,3 +206,45 @@ impl Debug for EmbedderMsg { /// the `String` content is expected to be extension (e.g, "doc", without the prefixing ".") #[derive(Clone, Debug, Deserialize, Serialize)] pub struct FilterPattern(pub String); + +/// https://w3c.github.io/mediasession/#mediametadata +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MediaMetadata { + /// Title + pub title: String, + /// Artist + pub artist: String, + /// Album + pub album: String, +} + +impl MediaMetadata { + pub fn new(title: String) -> Self { + Self { + title, + artist: "".to_owned(), + album: "".to_owned(), + } + } +} + +/// https://w3c.github.io/mediasession/#enumdef-mediasessionplaybackstate +#[repr(i32)] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum MediaSessionPlaybackState { + /// The browsing context does not specify whether it’s playing or paused. + None_ = 1, + /// The browsing context is currently playing media and it can be paused. + Playing, + /// The browsing context has paused media and it can be resumed. + Paused, +} + +/// Type of events sent from script to the embedder about the media session. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum MediaSessionEvent { + /// Indicates that the media metadata is available. + SetMetadata(MediaMetadata), + /// Indicates that the playback state has changed. + PlaybackStateChange(MediaSessionPlaybackState), +} diff --git a/components/script/dom/bindings/trace.rs b/components/script/dom/bindings/trace.rs index 74e6996b67a..2051a1a19d8 100644 --- a/components/script/dom/bindings/trace.rs +++ b/components/script/dom/bindings/trace.rs @@ -57,7 +57,7 @@ use content_security_policy::CspList; use crossbeam_channel::{Receiver, Sender}; use cssparser::RGBA; use devtools_traits::{CSSError, TimelineMarkerType, WorkerId}; -use embedder_traits::EventLoopWaker; +use embedder_traits::{EventLoopWaker, MediaMetadata}; use encoding_rs::{Decoder, Encoding}; use euclid::default::{Point2D, Rect, Rotation3D, Transform2D, Transform3D}; use euclid::Length as EuclidLength; @@ -94,8 +94,8 @@ use profile_traits::time::ProfilerChan as TimeProfilerChan; use script_layout_interface::rpc::LayoutRPC; use script_layout_interface::OpaqueStyleAndLayoutData; use script_traits::transferable::MessagePortImpl; -use script_traits::DrawAPaintImageResult; -use script_traits::{DocumentActivity, ScriptToConstellationChan, TimerEventId, TimerSource}; +use script_traits::{DocumentActivity, DrawAPaintImageResult}; +use script_traits::{MediaSessionActionType, ScriptToConstellationChan, TimerEventId, TimerSource}; use script_traits::{UntrustedNodeAddress, WindowSizeData, WindowSizeType}; use selectors::matching::ElementSelectorFlags; use serde::{Deserialize, Serialize}; @@ -536,6 +536,8 @@ unsafe_no_jsmanaged_fields!(WindowGLContext); unsafe_no_jsmanaged_fields!(VideoFrame); unsafe_no_jsmanaged_fields!(WebGLContextId); unsafe_no_jsmanaged_fields!(Arc<Mutex<dyn AudioRenderer>>); +unsafe_no_jsmanaged_fields!(MediaSessionActionType); +unsafe_no_jsmanaged_fields!(MediaMetadata); unsafe impl<'a> JSTraceable for &'a str { #[inline] diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index d10801c5779..f8f707a0dd9 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -15,8 +15,10 @@ use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::HTMLMediaE use crate::dom::bindings::codegen::Bindings::HTMLSourceElementBinding::HTMLSourceElementMethods; use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*; use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods; +use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorBinding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode}; +use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::InheritTypes::{ElementTypeId, HTMLElementTypeId}; use crate::dom::bindings::codegen::InheritTypes::{HTMLMediaElementTypeId, NodeTypeId}; use crate::dom::bindings::codegen::UnionTypes::{ @@ -65,6 +67,7 @@ use crate::script_thread::ScriptThread; use crate::task_source::TaskSource; use dom_struct::dom_struct; use embedder_traits::resources::{self, Resource as EmbedderResource}; +use embedder_traits::{MediaSessionEvent, MediaSessionPlaybackState}; use euclid::default::Size2D; use headers::{ContentLength, ContentRange, HeaderMapExt}; use html5ever::{LocalName, Prefix}; @@ -592,7 +595,6 @@ impl HTMLMediaElement { match (old_ready_state, ready_state) { (ReadyState::HaveNothing, ReadyState::HaveMetadata) => { task_source.queue_simple_event(self.upcast(), atom!("loadedmetadata"), &window); - // No other steps are applicable in this case. return; }, @@ -1725,6 +1727,17 @@ impl HTMLMediaElement { if self.Controls() { self.render_controls(); } + + 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()), + ); }, PlayerEvent::NeedData => { // The player needs more data. @@ -1782,13 +1795,33 @@ impl HTMLMediaElement { }; ScriptThread::await_stable_state(Microtask::MediaElement(task)); }, - PlayerEvent::StateChanged(ref state) => match *state { - PlaybackState::Paused => { - if self.ready_state.get() == ReadyState::HaveMetadata { - self.change_ready_state(ReadyState::HaveEnoughData); - } - }, - _ => {}, + PlayerEvent::StateChanged(ref state) => { + 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, + )); }, } } @@ -1883,6 +1916,15 @@ impl HTMLMediaElement { self.media_element_load_algorithm(); } } + + 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); + } } // XXX Placeholder for [https://github.com/servo/servo/issues/22293] diff --git a/components/script/dom/mediametadata.rs b/components/script/dom/mediametadata.rs new file mode 100644 index 00000000000..f2e94abfaa1 --- /dev/null +++ b/components/script/dom/mediametadata.rs @@ -0,0 +1,97 @@ +/* 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 crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::MediaMetadataBinding; +use crate::dom::bindings::codegen::Bindings::MediaMetadataBinding::MediaMetadataInit; +use crate::dom::bindings::codegen::Bindings::MediaMetadataBinding::MediaMetadataMethods; +use crate::dom::bindings::error::Fallible; +use crate::dom::bindings::reflector::{reflect_dom_object, Reflector}; +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; +use crate::dom::bindings::str::DOMString; +use crate::dom::mediasession::MediaSession; +use crate::dom::window::Window; +use dom_struct::dom_struct; + +#[dom_struct] +pub struct MediaMetadata { + reflector_: Reflector, + session: MutNullableDom<MediaSession>, + title: DomRefCell<DOMString>, + artist: DomRefCell<DOMString>, + album: DomRefCell<DOMString>, +} + +impl MediaMetadata { + fn new_inherited(init: &MediaMetadataInit) -> MediaMetadata { + MediaMetadata { + reflector_: Reflector::new(), + session: Default::default(), + title: DomRefCell::new(init.title.clone()), + artist: DomRefCell::new(init.artist.clone()), + album: DomRefCell::new(init.album.clone()), + } + } + + pub fn new(global: &Window, init: &MediaMetadataInit) -> DomRoot<MediaMetadata> { + reflect_dom_object( + Box::new(MediaMetadata::new_inherited(init)), + global, + MediaMetadataBinding::Wrap, + ) + } + + /// https://w3c.github.io/mediasession/#dom-mediametadata-mediametadata + pub fn Constructor( + window: &Window, + init: &MediaMetadataInit, + ) -> Fallible<DomRoot<MediaMetadata>> { + Ok(MediaMetadata::new(window, init)) + } + + fn queue_update_metadata_algorithm(&self) { + if self.session.get().is_none() { + return; + } + } + + pub fn set_session(&self, session: &MediaSession) { + self.session.set(Some(&session)); + } +} + +impl MediaMetadataMethods for MediaMetadata { + /// https://w3c.github.io/mediasession/#dom-mediametadata-title + fn Title(&self) -> DOMString { + self.title.borrow().clone() + } + + /// https://w3c.github.io/mediasession/#dom-mediametadata-title + fn SetTitle(&self, value: DOMString) { + *self.title.borrow_mut() = value; + self.queue_update_metadata_algorithm(); + } + + /// https://w3c.github.io/mediasession/#dom-mediametadata-artist + fn Artist(&self) -> DOMString { + self.artist.borrow().clone() + } + + /// https://w3c.github.io/mediasession/#dom-mediametadata-artist + fn SetArtist(&self, value: DOMString) { + *self.artist.borrow_mut() = value; + self.queue_update_metadata_algorithm(); + } + + /// https://w3c.github.io/mediasession/#dom-mediametadata-album + fn Album(&self) -> DOMString { + self.album.borrow().clone() + } + + /// https://w3c.github.io/mediasession/#dom-mediametadata-album + fn SetAlbum(&self, value: DOMString) { + *self.album.borrow_mut() = value; + self.queue_update_metadata_algorithm(); + } +} diff --git a/components/script/dom/mediasession.rs b/components/script/dom/mediasession.rs new file mode 100644 index 00000000000..1523e9a0ae6 --- /dev/null +++ b/components/script/dom/mediasession.rs @@ -0,0 +1,213 @@ +/* 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 crate::compartments::{AlreadyInCompartment, InCompartment}; +use crate::dom::bindings::callback::ExceptionHandling; +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::HTMLMediaElementMethods; +use crate::dom::bindings::codegen::Bindings::MediaMetadataBinding::MediaMetadataInit; +use crate::dom::bindings::codegen::Bindings::MediaMetadataBinding::MediaMetadataMethods; +use crate::dom::bindings::codegen::Bindings::MediaSessionBinding; +use crate::dom::bindings::codegen::Bindings::MediaSessionBinding::MediaSessionAction; +use crate::dom::bindings::codegen::Bindings::MediaSessionBinding::MediaSessionActionHandler; +use crate::dom::bindings::codegen::Bindings::MediaSessionBinding::MediaSessionMethods; +use crate::dom::bindings::codegen::Bindings::MediaSessionBinding::MediaSessionPlaybackState; +use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector}; +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; +use crate::dom::bindings::str::DOMString; +use crate::dom::htmlmediaelement::HTMLMediaElement; +use crate::dom::mediametadata::MediaMetadata; +use crate::dom::window::Window; +use dom_struct::dom_struct; +use embedder_traits::MediaMetadata as EmbedderMediaMetadata; +use embedder_traits::MediaSessionEvent; +use script_traits::MediaSessionActionType; +use script_traits::ScriptMsg; +use std::collections::HashMap; +use std::rc::Rc; + +#[dom_struct] +pub struct MediaSession { + reflector_: Reflector, + /// https://w3c.github.io/mediasession/#dom-mediasession-metadata + #[ignore_malloc_size_of = "defined in embedder_traits"] + metadata: DomRefCell<Option<EmbedderMediaMetadata>>, + /// https://w3c.github.io/mediasession/#dom-mediasession-playbackstate + playback_state: DomRefCell<MediaSessionPlaybackState>, + /// https://w3c.github.io/mediasession/#supported-media-session-actions + #[ignore_malloc_size_of = "Rc"] + action_handlers: DomRefCell<HashMap<MediaSessionActionType, Rc<MediaSessionActionHandler>>>, + /// The media instance controlled by this media session. + /// For now only HTMLMediaElements are controlled by media sessions. + media_instance: MutNullableDom<HTMLMediaElement>, +} + +impl MediaSession { + #[allow(unrooted_must_root)] + fn new_inherited() -> MediaSession { + let media_session = MediaSession { + reflector_: Reflector::new(), + metadata: DomRefCell::new(None), + playback_state: DomRefCell::new(MediaSessionPlaybackState::None), + action_handlers: DomRefCell::new(HashMap::new()), + media_instance: Default::default(), + }; + media_session + } + + pub fn new(window: &Window) -> DomRoot<MediaSession> { + reflect_dom_object( + Box::new(MediaSession::new_inherited()), + window, + MediaSessionBinding::Wrap, + ) + } + + pub fn register_media_instance(&self, media_instance: &HTMLMediaElement) { + self.media_instance.set(Some(media_instance)); + } + + pub fn handle_action(&self, action: MediaSessionActionType) { + debug!("Handle media session action {:?}", action); + + if let Some(handler) = self.action_handlers.borrow().get(&action) { + if handler.Call__(ExceptionHandling::Report).is_err() { + warn!("Error calling MediaSessionActionHandler callback"); + } + return; + } + + // Default action. + if let Some(media) = self.media_instance.get() { + match action { + MediaSessionActionType::Play => { + let in_compartment_proof = AlreadyInCompartment::assert(&self.global()); + media.Play(InCompartment::Already(&in_compartment_proof)); + }, + MediaSessionActionType::Pause => { + media.Pause(); + }, + MediaSessionActionType::SeekBackward => {}, + MediaSessionActionType::SeekForward => {}, + MediaSessionActionType::PreviousTrack => {}, + MediaSessionActionType::NextTrack => {}, + MediaSessionActionType::SkipAd => {}, + MediaSessionActionType::Stop => {}, + MediaSessionActionType::SeekTo => {}, + } + } + } + + pub fn send_event(&self, event: MediaSessionEvent) { + let global = self.global(); + let window = global.as_window(); + let pipeline_id = window + .pipeline_id() + .expect("Cannot send media session event outside of a pipeline"); + window.send_to_constellation(ScriptMsg::MediaSessionEvent(pipeline_id, event)); + } + + pub fn update_title(&self, title: String) { + let mut metadata = self.metadata.borrow_mut(); + if let Some(ref mut metadata) = *metadata { + // We only update the title with the data provided by the media + // player and iff the user did not provide a title. + if !metadata.title.is_empty() { + return; + } + metadata.title = title; + } else { + *metadata = Some(EmbedderMediaMetadata::new(title)); + } + self.send_event(MediaSessionEvent::SetMetadata( + metadata.as_ref().unwrap().clone(), + )); + } +} + +impl MediaSessionMethods for MediaSession { + /// https://w3c.github.io/mediasession/#dom-mediasession-metadata + fn GetMetadata(&self) -> Option<DomRoot<MediaMetadata>> { + if let Some(ref metadata) = *self.metadata.borrow() { + let mut init = MediaMetadataInit::empty(); + init.title = DOMString::from_string(metadata.title.clone()); + init.artist = DOMString::from_string(metadata.artist.clone()); + init.album = DOMString::from_string(metadata.album.clone()); + let global = self.global(); + Some(MediaMetadata::new(&global.as_window(), &init)) + } else { + None + } + } + + /// https://w3c.github.io/mediasession/#dom-mediasession-metadata + fn SetMetadata(&self, metadata: Option<&MediaMetadata>) { + if let Some(ref metadata) = metadata { + metadata.set_session(self); + } + + let global = self.global(); + let window = global.as_window(); + let _metadata = match metadata { + Some(m) => { + let title = if m.Title().is_empty() { + window.get_url().into_string() + } else { + m.Title().into() + }; + EmbedderMediaMetadata { + title, + artist: m.Artist().into(), + album: m.Album().into(), + } + }, + None => EmbedderMediaMetadata::new(window.get_url().into_string()), + }; + + *self.metadata.borrow_mut() = Some(_metadata.clone()); + + self.send_event(MediaSessionEvent::SetMetadata(_metadata)); + } + + /// https://w3c.github.io/mediasession/#dom-mediasession-playbackstate + fn PlaybackState(&self) -> MediaSessionPlaybackState { + *self.playback_state.borrow() + } + + /// https://w3c.github.io/mediasession/#dom-mediasession-playbackstate + fn SetPlaybackState(&self, state: MediaSessionPlaybackState) { + *self.playback_state.borrow_mut() = state; + } + + /// https://w3c.github.io/mediasession/#update-action-handler-algorithm + fn SetActionHandler( + &self, + action: MediaSessionAction, + handler: Option<Rc<MediaSessionActionHandler>>, + ) { + match handler { + Some(handler) => self + .action_handlers + .borrow_mut() + .insert(action.into(), handler.clone()), + None => self.action_handlers.borrow_mut().remove(&action.into()), + }; + } +} + +impl From<MediaSessionAction> for MediaSessionActionType { + fn from(action: MediaSessionAction) -> MediaSessionActionType { + match action { + MediaSessionAction::Play => MediaSessionActionType::Play, + MediaSessionAction::Pause => MediaSessionActionType::Pause, + MediaSessionAction::Seekbackward => MediaSessionActionType::SeekBackward, + MediaSessionAction::Seekforward => MediaSessionActionType::SeekForward, + MediaSessionAction::Previoustrack => MediaSessionActionType::PreviousTrack, + MediaSessionAction::Nexttrack => MediaSessionActionType::NextTrack, + MediaSessionAction::Skipad => MediaSessionActionType::SkipAd, + MediaSessionAction::Stop => MediaSessionActionType::Stop, + MediaSessionAction::Seekto => MediaSessionActionType::SeekTo, + } + } +} diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index fbc4d457482..ef562931bee 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -398,8 +398,10 @@ pub mod mediaelementaudiosourcenode; pub mod mediaerror; pub mod mediafragmentparser; pub mod medialist; +pub mod mediametadata; pub mod mediaquerylist; pub mod mediaquerylistevent; +pub mod mediasession; pub mod mediastream; pub mod mediastreamtrack; pub mod messagechannel; diff --git a/components/script/dom/navigator.rs b/components/script/dom/navigator.rs index 8a0f6a21d98..127883dd956 100644 --- a/components/script/dom/navigator.rs +++ b/components/script/dom/navigator.rs @@ -12,6 +12,7 @@ use crate::dom::bindings::str::DOMString; use crate::dom::bluetooth::Bluetooth; use crate::dom::gamepadlist::GamepadList; use crate::dom::mediadevices::MediaDevices; +use crate::dom::mediasession::MediaSession; use crate::dom::mimetypearray::MimeTypeArray; use crate::dom::navigatorinfo; use crate::dom::permissions::Permissions; @@ -34,6 +35,7 @@ pub struct Navigator { mediadevices: MutNullableDom<MediaDevices>, gamepads: MutNullableDom<GamepadList>, permissions: MutNullableDom<Permissions>, + mediasession: MutNullableDom<MediaSession>, } impl Navigator { @@ -48,6 +50,7 @@ impl Navigator { mediadevices: Default::default(), gamepads: Default::default(), permissions: Default::default(), + mediasession: Default::default(), } } @@ -186,4 +189,20 @@ impl NavigatorMethods for Navigator { self.mediadevices .or_init(|| MediaDevices::new(&self.global())) } + + /// https://w3c.github.io/mediasession/#dom-navigator-mediasession + fn MediaSession(&self) -> DomRoot<MediaSession> { + self.mediasession.or_init(|| { + // There is a single MediaSession instance per Pipeline + // and only one active MediaSession globally. + // + // MediaSession creation can happen in two cases: + // + // - If content gets `navigator.mediaSession` + // - If a media instance (HTMLMediaElement so far) starts playing media. + let global = self.global(); + let window = global.as_window(); + MediaSession::new(window) + }) + } } diff --git a/components/script/dom/webidls/MediaMetadata.webidl b/components/script/dom/webidls/MediaMetadata.webidl new file mode 100644 index 00000000000..495aeef8e35 --- /dev/null +++ b/components/script/dom/webidls/MediaMetadata.webidl @@ -0,0 +1,30 @@ +/* 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/. */ +/* + * The origin of this IDL file is + * https://w3c.github.io/mediasession/#mediametadata + */ + +dictionary MediaImage { + required USVString src; + DOMString sizes = ""; + DOMString type = ""; +}; + +[Exposed=Window] +interface MediaMetadata { + [Throws] constructor(optional MediaMetadataInit init = {}); + attribute DOMString title; + attribute DOMString artist; + attribute DOMString album; + // TODO: https://github.com/servo/servo/issues/10072 + // attribute FrozenArray<MediaImage> artwork; +}; + +dictionary MediaMetadataInit { + DOMString title = ""; + DOMString artist = ""; + DOMString album = ""; + sequence<MediaImage> artwork = []; +}; diff --git a/components/script/dom/webidls/MediaSession.webidl b/components/script/dom/webidls/MediaSession.webidl new file mode 100644 index 00000000000..12b3fe062ba --- /dev/null +++ b/components/script/dom/webidls/MediaSession.webidl @@ -0,0 +1,57 @@ +/* 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/. */ +/* + * The origin of this IDL file is + * https://w3c.github.io/mediasession/#mediasession + */ + +[Exposed=Window] +partial interface Navigator { + [SameObject] readonly attribute MediaSession mediaSession; +}; + +enum MediaSessionPlaybackState { + "none", + "paused", + "playing" +}; + +enum MediaSessionAction { + "play", + "pause", + "seekbackward", + "seekforward", + "previoustrack", + "nexttrack", + "skipad", + "stop", + "seekto" +}; + +dictionary MediaSessionActionDetails { + required MediaSessionAction action; +}; + +dictionary MediaSessionSeekActionDetails : MediaSessionActionDetails { + double? seekOffset; +}; + +dictionary MediaSessionSeekToActionDetails : MediaSessionActionDetails { + required double seekTime; + boolean? fastSeek; +}; + +callback MediaSessionActionHandler = void(/*MediaSessionActionDetails details*/); + +[Exposed=Window] +interface MediaSession { + attribute MediaMetadata? metadata; + + attribute MediaSessionPlaybackState playbackState; + + void setActionHandler(MediaSessionAction action, MediaSessionActionHandler? handler); + + //void setPositionState(optional MediaPositionState? state); +}; + diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index dbc4f0fb444..c0155ea63a5 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -138,7 +138,7 @@ use script_traits::{ DiscardBrowsingContext, DocumentActivity, EventResult, HistoryEntryReplacement, }; use script_traits::{InitialScriptState, JsEvalResult, LayoutMsg, LoadData, LoadOrigin}; -use script_traits::{MouseButton, MouseEventType, NewLayoutInfo}; +use script_traits::{MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo}; use script_traits::{Painter, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory}; use script_traits::{ScriptToConstellationChan, TimerEvent, TimerSchedulerMsg}; use script_traits::{TimerSource, TouchEventType, TouchId, UntrustedNodeAddress, WheelDelta}; @@ -1713,6 +1713,7 @@ impl ScriptThread { WebVREvents(id, ..) => Some(id), PaintMetric(..) => None, ExitFullScreen(id, ..) => Some(id), + MediaSessionAction(..) => None, } }, MixedMessage::FromDevtools(_) => None, @@ -1942,6 +1943,9 @@ impl ScriptThread { ConstellationControlMsg::PaintMetric(pipeline_id, metric_type, metric_value) => { self.handle_paint_metric(pipeline_id, metric_type, metric_value) }, + ConstellationControlMsg::MediaSessionAction(pipeline_id, action) => { + self.handle_media_session_action(pipeline_id, action) + }, msg @ ConstellationControlMsg::AttachLayout(..) | msg @ ConstellationControlMsg::Viewport(..) | msg @ ConstellationControlMsg::SetScrollState(..) | @@ -3925,6 +3929,15 @@ impl ScriptThread { } } + fn handle_media_session_action(&self, pipeline_id: PipelineId, action: MediaSessionActionType) { + if let Some(window) = self.documents.borrow().find_window(pipeline_id) { + let media_session = window.Navigator().MediaSession(); + media_session.handle_action(action); + } else { + warn!("No MediaSession for this pipeline ID"); + }; + } + pub fn enqueue_microtask(job: Microtask) { SCRIPT_THREAD_ROOT.with(|root| { let script_thread = unsafe { &*root.get().unwrap() }; diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 0212ae021b6..a8a19b5a40a 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -388,6 +388,8 @@ pub enum ConstellationControlMsg { WebVREvents(PipelineId, Vec<WebVREvent>), /// Notifies the script thread about a new recorded paint metric. PaintMetric(PipelineId, ProgressiveWebMetricType, u64), + /// Notifies the media session about a user requested media session action. + MediaSessionAction(PipelineId, MediaSessionActionType), } impl fmt::Debug for ConstellationControlMsg { @@ -426,6 +428,7 @@ impl fmt::Debug for ConstellationControlMsg { WebVREvents(..) => "WebVREvents", PaintMetric(..) => "PaintMetric", ExitFullScreen(..) => "ExitFullScreen", + MediaSessionAction(..) => "MediaSessionAction", }; write!(formatter, "ConstellationControlMsg::{}", variant) } @@ -877,6 +880,8 @@ pub enum ConstellationMsg { DisableProfiler, /// Request to exit from fullscreen mode ExitFullScreen(TopLevelBrowsingContextId), + /// Media session action. + MediaSessionAction(MediaSessionActionType), } impl fmt::Debug for ConstellationMsg { @@ -907,6 +912,7 @@ impl fmt::Debug for ConstellationMsg { EnableProfiler(..) => "EnableProfiler", DisableProfiler => "DisableProfiler", ExitFullScreen(..) => "ExitFullScreen", + MediaSessionAction(..) => "MediaSessionAction", }; write!(formatter, "ConstellationMsg::{}", variant) } @@ -1053,3 +1059,49 @@ pub enum MessagePortMsg { /// Handle a new port-message-task. NewTask(MessagePortId, PortMessageTask), } + +/// The type of MediaSession action. +/// https://w3c.github.io/mediasession/#enumdef-mediasessionaction +#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)] +pub enum MediaSessionActionType { + /// The action intent is to resume playback. + Play, + /// The action intent is to pause the currently active playback. + Pause, + /// The action intent is to move the playback time backward by a short period (i.e. a few + /// seconds). + SeekBackward, + /// The action intent is to move the playback time forward by a short period (i.e. a few + /// seconds). + SeekForward, + /// The action intent is to either start the current playback from the beginning if the + /// playback has a notion, of beginning, or move to the previous item in the playlist if the + /// playback has a notion of playlist. + PreviousTrack, + /// The action is to move to the playback to the next item in the playlist if the playback has + /// a notion of playlist. + NextTrack, + /// The action intent is to skip the advertisement that is currently playing. + SkipAd, + /// The action intent is to stop the playback and clear the state if appropriate. + Stop, + /// The action intent is to move the playback time to a specific time. + SeekTo, +} + +impl From<i32> for MediaSessionActionType { + fn from(value: i32) -> MediaSessionActionType { + match value { + 1 => MediaSessionActionType::Play, + 2 => MediaSessionActionType::Pause, + 3 => MediaSessionActionType::SeekBackward, + 4 => MediaSessionActionType::SeekForward, + 5 => MediaSessionActionType::PreviousTrack, + 6 => MediaSessionActionType::NextTrack, + 7 => MediaSessionActionType::SkipAd, + 8 => MediaSessionActionType::Stop, + 9 => MediaSessionActionType::SeekTo, + _ => panic!("Unknown MediaSessionActionType"), + } + } +} diff --git a/components/script_traits/script_msg.rs b/components/script_traits/script_msg.rs index a12101380c9..1eccc794398 100644 --- a/components/script_traits/script_msg.rs +++ b/components/script_traits/script_msg.rs @@ -16,7 +16,7 @@ use crate::WorkerGlobalScopeInit; use crate::WorkerScriptLoadOrigin; use canvas_traits::canvas::{CanvasId, CanvasMsg}; use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId}; -use embedder_traits::EmbedderMsg; +use embedder_traits::{EmbedderMsg, MediaSessionEvent}; use euclid::default::Size2D as UntypedSize2D; use euclid::Size2D; use gfx_traits::Epoch; @@ -254,6 +254,9 @@ pub enum ScriptMsg { GetScreenSize(IpcSender<DeviceIntSize>), /// Get the available screen size (pixel) GetScreenAvailSize(IpcSender<DeviceIntSize>), + /// Notifies the constellation about media session events + /// (i.e. when there is metadata for the active media session, playback state changes...). + MediaSessionEvent(PipelineId, MediaSessionEvent), } impl fmt::Debug for ScriptMsg { @@ -305,6 +308,7 @@ impl fmt::Debug for ScriptMsg { GetClientWindow(..) => "GetClientWindow", GetScreenSize(..) => "GetScreenSize", GetScreenAvailSize(..) => "GetScreenAvailSize", + MediaSessionEvent(..) => "MediaSessionEvent", }; write!(formatter, "ScriptMsg::{}", variant) } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 3d4b7bf14d3..e32c28ef740 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -713,6 +713,16 @@ where ); } }, + + WindowEvent::MediaSessionAction(a) => { + let msg = ConstellationMsg::MediaSessionAction(a); + if let Err(e) = self.constellation_chan.send(msg) { + warn!( + "Sending MediaSessionAction message to constellation failed ({:?}).", + e + ); + } + }, } } |