aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--components/compositing/windowing.rs6
-rw-r--r--components/constellation/constellation.rs60
-rw-r--r--components/embedder_traits/lib.rs46
-rw-r--r--components/script/dom/bindings/trace.rs8
-rw-r--r--components/script/dom/htmlmediaelement.rs58
-rw-r--r--components/script/dom/mediametadata.rs97
-rw-r--r--components/script/dom/mediasession.rs213
-rw-r--r--components/script/dom/mod.rs2
-rw-r--r--components/script/dom/navigator.rs19
-rw-r--r--components/script/dom/webidls/MediaMetadata.webidl30
-rw-r--r--components/script/dom/webidls/MediaSession.webidl57
-rw-r--r--components/script/script_thread.rs15
-rw-r--r--components/script_traits/lib.rs52
-rw-r--r--components/script_traits/script_msg.rs6
-rw-r--r--components/servo/lib.rs10
-rw-r--r--ports/glutin/browser.rs4
-rw-r--r--ports/libsimpleservo/api/src/lib.rs30
-rw-r--r--ports/libsimpleservo/capi/src/lib.rs19
-rw-r--r--ports/libsimpleservo/jniapi/src/lib.rs52
-rw-r--r--support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java38
-rw-r--r--support/android/apk/servoapp/src/main/java/org/mozilla/servo/MediaSession.java195
-rwxr-xr-xsupport/android/apk/servoapp/src/main/res/drawable/media_session_icon.pngbin0 -> 213 bytes
-rwxr-xr-xsupport/android/apk/servoapp/src/main/res/drawable/media_session_next.pngbin0 -> 203 bytes
-rwxr-xr-xsupport/android/apk/servoapp/src/main/res/drawable/media_session_pause.pngbin0 -> 114 bytes
-rwxr-xr-xsupport/android/apk/servoapp/src/main/res/drawable/media_session_play.pngbin0 -> 227 bytes
-rwxr-xr-xsupport/android/apk/servoapp/src/main/res/drawable/media_session_prev.pngbin0 -> 205 bytes
-rwxr-xr-xsupport/android/apk/servoapp/src/main/res/drawable/media_session_stop.pngbin0 -> 106 bytes
-rw-r--r--support/android/apk/servoapp/src/main/res/values/strings.xml4
-rw-r--r--support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java6
-rw-r--r--support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java16
-rw-r--r--support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java6
-rw-r--r--tests/wpt/include.ini2
-rw-r--r--tests/wpt/metadata/mediasession/idlharness.window.js.ini16
-rw-r--r--tests/wpt/metadata/mediasession/mediametadata.html.ini43
-rw-r--r--tests/wpt/metadata/mediasession/positionstate.html.ini19
-rw-r--r--tests/wpt/mozilla/meta/MANIFEST.json2
-rw-r--r--tests/wpt/mozilla/tests/mozilla/interfaces.html2
37 files changed, 1112 insertions, 21 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
+ );
+ }
+ },
}
}
diff --git a/ports/glutin/browser.rs b/ports/glutin/browser.rs
index e3da4d13d61..976026a483b 100644
--- a/ports/glutin/browser.rs
+++ b/ports/glutin/browser.rs
@@ -449,6 +449,10 @@ where
error!("Failed to store profile: {}", e);
}
},
+ EmbedderMsg::MediaSessionEvent(_) => {
+ debug!("MediaSessionEvent received");
+ // TODO(ferjm): MediaSession support for Glutin based browsers.
+ },
}
}
}
diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs
index 0b415ac669c..c7986e05623 100644
--- a/ports/libsimpleservo/api/src/lib.rs
+++ b/ports/libsimpleservo/api/src/lib.rs
@@ -15,7 +15,7 @@ use servo::compositing::windowing::{
WindowMethods,
};
use servo::embedder_traits::resources::{self, Resource, ResourceReaderMethods};
-use servo::embedder_traits::EmbedderMsg;
+use servo::embedder_traits::{EmbedderMsg, MediaSessionEvent};
use servo::euclid::{Point2D, Rect, Scale, Size2D, Vector2D};
use servo::keyboard_types::{Key, KeyState, KeyboardEvent};
use servo::msg::constellation_msg::TraversalDirection;
@@ -126,10 +126,14 @@ pub trait HostTrait {
fn on_shutdown_complete(&self);
/// A text input is focused.
fn on_ime_state_changed(&self, show: bool);
- /// Gets sytem clipboard contents
+ /// Gets sytem clipboard contents.
fn get_clipboard_contents(&self) -> Option<String>;
- /// Sets system clipboard contents
+ /// Sets system clipboard contents.
fn set_clipboard_contents(&self, contents: String);
+ /// Called when we get the media session metadata/
+ fn on_media_session_metadata(&self, title: String, artist: String, album: String);
+ /// Called when the media sessoin playback state changes.
+ fn on_media_session_playback_state_change(&self, state: i32);
}
pub struct ServoGlue {
@@ -466,6 +470,11 @@ impl ServoGlue {
self.process_event(WindowEvent::Keyboard(key_event))
}
+ pub fn media_session_action(&mut self, action: i32) -> Result<(), &'static str> {
+ info!("Media session action {:?}", action);
+ self.process_event(WindowEvent::MediaSessionAction(action.into()))
+ }
+
fn process_event(&mut self, event: WindowEvent) -> Result<(), &'static str> {
self.events.push(event);
if !self.batch_mode {
@@ -572,6 +581,21 @@ impl ServoGlue {
EmbedderMsg::HideIME => {
self.callbacks.host_callbacks.on_ime_state_changed(false);
},
+ EmbedderMsg::MediaSessionEvent(event) => {
+ match event {
+ MediaSessionEvent::SetMetadata(metadata) => {
+ self.callbacks.host_callbacks.on_media_session_metadata(
+ metadata.title,
+ metadata.artist,
+ metadata.album,
+ )
+ },
+ MediaSessionEvent::PlaybackStateChange(state) => self
+ .callbacks
+ .host_callbacks
+ .on_media_session_playback_state_change(state as i32),
+ };
+ },
EmbedderMsg::Status(..) |
EmbedderMsg::SelectFiles(..) |
EmbedderMsg::MoveTo(..) |
diff --git a/ports/libsimpleservo/capi/src/lib.rs b/ports/libsimpleservo/capi/src/lib.rs
index c6b27ae6d48..956c7e3a380 100644
--- a/ports/libsimpleservo/capi/src/lib.rs
+++ b/ports/libsimpleservo/capi/src/lib.rs
@@ -216,6 +216,9 @@ pub struct CHostCallbacks {
pub on_ime_state_changed: extern "C" fn(show: bool),
pub get_clipboard_contents: extern "C" fn() -> *const c_char,
pub set_clipboard_contents: extern "C" fn(contents: *const c_char),
+ pub on_media_session_metadata:
+ extern "C" fn(title: *const c_char, album: *const c_char, artist: *const c_char),
+ pub on_media_session_playback_state_change: extern "C" fn(state: i32),
}
/// Servo options
@@ -708,4 +711,20 @@ impl HostTrait for HostCallbacks {
let contents = CString::new(contents).expect("Can't create string");
(self.0.set_clipboard_contents)(contents.as_ptr());
}
+
+ fn on_media_session_metadata(&self, title: String, artist: String, album: String) {
+ debug!(
+ "on_media_session_metadata ({:?} {:?} {:?})",
+ title, artist, album
+ );
+ let title = CString::new(title).expect("Can't create string");
+ let artist = CString::new(artist).expect("Can't create string");
+ let album = CString::new(album).expect("Can't create string");
+ (self.0.on_media_session_metadata)(title.as_ptr(), artist.as_ptr(), album.as_ptr());
+ }
+
+ fn on_media_session_playback_state_change(&self, state: i32) {
+ debug!("on_media_session_playback_state_change {:?}", state);
+ (self.0.on_media_session_playback_state_change)(state);
+ }
}
diff --git a/ports/libsimpleservo/jniapi/src/lib.rs b/ports/libsimpleservo/jniapi/src/lib.rs
index 963e01dec6d..1125f3a5680 100644
--- a/ports/libsimpleservo/jniapi/src/lib.rs
+++ b/ports/libsimpleservo/jniapi/src/lib.rs
@@ -333,6 +333,16 @@ pub fn Java_org_mozilla_servoview_JNIServo_click(env: JNIEnv, _: JClass, x: jint
call(&env, |s| s.click(x as f32, y as f32));
}
+#[no_mangle]
+pub fn Java_org_mozilla_servoview_JNIServo_mediaSessionAction(
+ env: JNIEnv,
+ _: JClass,
+ action: jint,
+) {
+ debug!("mediaSessionAction");
+ call(&env, |s| s.media_session_action(action as i32));
+}
+
pub struct WakeupCallback {
callback: GlobalRef,
jvm: Arc<JavaVM>,
@@ -508,6 +518,48 @@ impl HostTrait for HostCallbacks {
}
fn set_clipboard_contents(&self, _contents: String) {}
+
+ fn on_media_session_metadata(&self, title: String, artist: String, album: String) {
+ info!("on_media_session_metadata");
+ let env = self.jvm.get_env().unwrap();
+ let title = match new_string(&env, &title) {
+ Ok(s) => s,
+ Err(_) => return,
+ };
+ let title = JValue::Object(JObject::from(title));
+
+ let artist = match new_string(&env, &artist) {
+ Ok(s) => s,
+ Err(_) => return,
+ };
+ let artist = JValue::Object(JObject::from(artist));
+
+ let album = match new_string(&env, &album) {
+ Ok(s) => s,
+ Err(_) => return,
+ };
+ let album = JValue::Object(JObject::from(album));
+ env.call_method(
+ self.callbacks.as_obj(),
+ "onMediaSessionMetadata",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
+ &[title, artist, album],
+ )
+ .unwrap();
+ }
+
+ fn on_media_session_playback_state_change(&self, state: i32) {
+ info!("on_media_session_playback_state_change {:?}", state);
+ let env = self.jvm.get_env().unwrap();
+ let state = JValue::Int(state as jint);
+ env.call_method(
+ self.callbacks.as_obj(),
+ "onMediaSessionPlaybackStateChange",
+ "(I)V",
+ &[state],
+ )
+ .unwrap();
+ }
}
fn initialize_android_glue(env: &JNIEnv, activity: JObject) {
diff --git a/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java b/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java
index e4ef5a51b52..21a6e7f996f 100644
--- a/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java
+++ b/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MainActivity.java
@@ -23,6 +23,7 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.util.Log;
+import org.mozilla.servo.MediaSession;
import org.mozilla.servoview.ServoView;
import org.mozilla.servoview.Servo;
@@ -41,6 +42,7 @@ public class MainActivity extends Activity implements Servo.Client {
ProgressBar mProgressBar;
TextView mIdleText;
boolean mCanGoBack;
+ MediaSession mMediaSession;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -85,6 +87,12 @@ public class MainActivity extends Activity implements Servo.Client {
setupUrlField();
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mMediaSession.hideMediaSessionControls();
+ }
+
private void setupUrlField() {
mUrlField.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
@@ -203,6 +211,7 @@ public class MainActivity extends Activity implements Servo.Client {
mServoView.onPause();
super.onPause();
}
+
@Override
public void onResume() {
mServoView.onResume();
@@ -217,4 +226,33 @@ public class MainActivity extends Activity implements Servo.Client {
super.onBackPressed();
}
}
+
+ @Override
+ public void onMediaSessionMetadata(String title, String artist, String album) {
+ if (mMediaSession == null) {
+ mMediaSession = new MediaSession(mServoView, this, getApplicationContext());
+ }
+ Log.d("onMediaSessionMetadata", title + " " + artist + " " + album);
+ mMediaSession.updateMetadata(title, artist, album);
+ }
+
+ @Override
+ public void onMediaSessionPlaybackStateChange(int state) {
+ Log.d("onMediaSessionPlaybackStateChange", String.valueOf(state));
+ if (mMediaSession == null) {
+ mMediaSession = new MediaSession(mServoView, this, getApplicationContext());
+ }
+
+ mMediaSession.setPlaybackState(state);
+
+ if (state == MediaSession.PLAYBACK_STATE_NONE) {
+ mMediaSession.hideMediaSessionControls();
+ return;
+ }
+ if (state == MediaSession.PLAYBACK_STATE_PLAYING ||
+ state == MediaSession.PLAYBACK_STATE_PAUSED) {
+ mMediaSession.showMediaSessionControls();
+ return;
+ }
+ }
}
diff --git a/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MediaSession.java b/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MediaSession.java
new file mode 100644
index 00000000000..5b1852d5d3f
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/java/org/mozilla/servo/MediaSession.java
@@ -0,0 +1,195 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.servo;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.util.Log;
+
+import org.mozilla.servoview.ServoView;
+
+public class MediaSession {
+ private class NotificationID {
+ private int lastID = 0;
+ public int getNext() {
+ lastID++;
+ return lastID;
+ }
+
+ public int get() {
+ return lastID;
+ }
+ }
+
+ // https://w3c.github.io/mediasession/#enumdef-mediasessionplaybackstate
+ public static final int PLAYBACK_STATE_NONE = 1;
+ public static final int PLAYBACK_STATE_PLAYING = 2;
+ public static final int PLAYBACK_STATE_PAUSED = 3;
+
+ // https://w3c.github.io/mediasession/#enumdef-mediasessionaction
+ private static final int ACTION_PLAY = 1;
+ private static final int ACTION_PAUSE = 2;
+ private static final int ACTON_SEEK_BACKWARD = 3;
+ private static final int ACTION_SEEK_FORWARD = 4;
+ private static final int ACTION_PREVIOUS_TRACK = 5;
+ private static final int ACTION_NEXT_TRACK = 6;
+ private static final int ACTION_SKIP_AD = 7;
+ private static final int ACTION_STOP = 8;
+ private static final int ACTION_SEEK_TO = 9;
+
+ private static final String MEDIA_CHANNEL_ID = "MediaNotificationChannel";
+ private static final String KEY_MEDIA_PLAY = "org.mozilla.servoview.MainActivity.play";
+ private static final String KEY_MEDIA_PAUSE = "org.mozilla.servoview.MainActivity.pause";
+ private static final String KEY_MEDIA_PREV = "org.mozilla.servoview.MainActivity.prev";
+ private static final String KEY_MEDIA_NEXT = "org.mozilla.servoview.MainActivity.next";
+ private static final String KEY_MEDIA_STOP = "org.mozilla.servoview.MainActivity.stop";
+
+ ServoView mView;
+ MainActivity mActivity;
+ Context mContext;
+
+ NotificationID mNotificationID;
+ BroadcastReceiver mMediaSessionActionReceiver;
+
+ int mPlaybackState = PLAYBACK_STATE_PAUSED;
+
+ String mTitle;
+ String mArtist;
+ String mAlbum;
+
+ public MediaSession(ServoView view, MainActivity activity, Context context) {
+ mView = view;
+ mActivity = activity;
+ mContext = context;
+ mNotificationID = new NotificationID();
+ createMediaNotificationChannel();
+ }
+
+ private void createMediaNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ CharSequence name =
+ mContext.getResources().getString(R.string.media_channel_name);
+ String description =
+ mContext.getResources().getString(R.string.media_channel_description);
+ int importance = NotificationManager.IMPORTANCE_LOW;
+ NotificationChannel channel =
+ new NotificationChannel(MEDIA_CHANNEL_ID, name, importance);
+ channel.setDescription(description);
+ NotificationManager notificationManager =
+ mContext.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ public void showMediaSessionControls() {
+ Log.d("MediaSession", "showMediaSessionControls " + mPlaybackState);
+ IntentFilter filter = new IntentFilter();
+ if (mPlaybackState == PLAYBACK_STATE_PAUSED) {
+ filter.addAction(KEY_MEDIA_PLAY);
+ }
+ if (mPlaybackState == PLAYBACK_STATE_PLAYING) {
+ filter.addAction(KEY_MEDIA_PAUSE);
+ }
+
+ int id;
+ if (mMediaSessionActionReceiver == null) {
+ id = mNotificationID.getNext();
+
+ mMediaSessionActionReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(KEY_MEDIA_PAUSE)) {
+ mView.mediaSessionAction(ACTION_PAUSE);
+ Log.d("MediaSession", "PAUSE action");
+ } else if (intent.getAction().equals(KEY_MEDIA_PLAY)) {
+ mView.mediaSessionAction(ACTION_PLAY);
+ Log.d("MediaSession", "PLAY action");
+ }
+ }
+ };
+ } else {
+ id = mNotificationID.get();
+ }
+
+ mContext.registerReceiver(mMediaSessionActionReceiver, filter);
+
+ Notification.Builder builder = new Notification.Builder(mContext, this.MEDIA_CHANNEL_ID);
+ builder
+ .setSmallIcon(R.drawable.media_session_icon)
+ .setContentTitle(mTitle)
+ .setVisibility(Notification.VISIBILITY_PUBLIC);
+
+ String contentText = new String();
+ if (mArtist != null && !mArtist.isEmpty()) {
+ contentText = mArtist;
+ }
+ if (mAlbum != null && !mAlbum.isEmpty()) {
+ if (!contentText.isEmpty()) {
+ contentText += " - " + mAlbum;
+ } else {
+ contentText = mAlbum;
+ }
+ }
+
+ if (!contentText.isEmpty()) {
+ builder.setContentText(contentText);
+ }
+
+ if (mPlaybackState == PLAYBACK_STATE_PAUSED) {
+ Intent playIntent = new Intent(KEY_MEDIA_PLAY);
+ Notification.Action playAction =
+ new Notification.Action(R.drawable.media_session_play, "Play",
+ PendingIntent.getBroadcast(mContext, 0, playIntent, 0));
+ builder.addAction(playAction);
+ }
+
+ if (mPlaybackState == PLAYBACK_STATE_PLAYING) {
+ Intent pauseIntent = new Intent(KEY_MEDIA_PAUSE);
+ Notification.Action pauseAction =
+ new Notification.Action(R.drawable.media_session_pause, "Pause",
+ PendingIntent.getBroadcast(mContext, 0, pauseIntent, 0));
+ builder.addAction(pauseAction);
+ }
+
+ builder.setStyle(new Notification.MediaStyle()
+ .setShowActionsInCompactView(0));
+
+ NotificationManager notificationManager =
+ mContext.getSystemService(NotificationManager.class);
+ notificationManager.notify(id, builder.build());
+ }
+
+ public void hideMediaSessionControls() {
+ Log.d("MediaSession", "hideMediaSessionControls");
+ NotificationManager notificationManager =
+ mContext.getSystemService(NotificationManager.class);
+ notificationManager.cancel(mNotificationID.get());
+ mContext.unregisterReceiver(mMediaSessionActionReceiver);
+ mMediaSessionActionReceiver = null;
+ }
+
+ public void setPlaybackState(int state) {
+ mPlaybackState = state;
+ }
+
+ public void updateMetadata(String title, String artist, String album) {
+ mTitle = title;
+ mArtist = artist;
+ mAlbum = album;
+
+ if (mMediaSessionActionReceiver != null) {
+ showMediaSessionControls();
+ }
+ }
+}
diff --git a/support/android/apk/servoapp/src/main/res/drawable/media_session_icon.png b/support/android/apk/servoapp/src/main/res/drawable/media_session_icon.png
new file mode 100755
index 00000000000..aafd2bbad02
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/res/drawable/media_session_icon.png
Binary files differ
diff --git a/support/android/apk/servoapp/src/main/res/drawable/media_session_next.png b/support/android/apk/servoapp/src/main/res/drawable/media_session_next.png
new file mode 100755
index 00000000000..b4208472c5d
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/res/drawable/media_session_next.png
Binary files differ
diff --git a/support/android/apk/servoapp/src/main/res/drawable/media_session_pause.png b/support/android/apk/servoapp/src/main/res/drawable/media_session_pause.png
new file mode 100755
index 00000000000..4a33fe9b17a
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/res/drawable/media_session_pause.png
Binary files differ
diff --git a/support/android/apk/servoapp/src/main/res/drawable/media_session_play.png b/support/android/apk/servoapp/src/main/res/drawable/media_session_play.png
new file mode 100755
index 00000000000..65b46201718
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/res/drawable/media_session_play.png
Binary files differ
diff --git a/support/android/apk/servoapp/src/main/res/drawable/media_session_prev.png b/support/android/apk/servoapp/src/main/res/drawable/media_session_prev.png
new file mode 100755
index 00000000000..8bd9feef195
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/res/drawable/media_session_prev.png
Binary files differ
diff --git a/support/android/apk/servoapp/src/main/res/drawable/media_session_stop.png b/support/android/apk/servoapp/src/main/res/drawable/media_session_stop.png
new file mode 100755
index 00000000000..b9321def65d
--- /dev/null
+++ b/support/android/apk/servoapp/src/main/res/drawable/media_session_stop.png
Binary files differ
diff --git a/support/android/apk/servoapp/src/main/res/values/strings.xml b/support/android/apk/servoapp/src/main/res/values/strings.xml
index ed1fb279213..4f3d30a0a84 100644
--- a/support/android/apk/servoapp/src/main/res/values/strings.xml
+++ b/support/android/apk/servoapp/src/main/res/values/strings.xml
@@ -1,3 +1,5 @@
<resources>
- <string name="app_name">Servo</string>
+ <string name="app_name">Servo</string>
+ <string name="media_channel_name">ServoMedia</string>
+ <string name="media_channel_description">Notication channel for multimedia activity</string>
</resources>
diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java
index 7bfcf233d8d..6181a298f0a 100644
--- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java
+++ b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/JNIServo.java
@@ -66,6 +66,8 @@ public class JNIServo {
public native void click(float x, float y);
+ public native void mediaSessionAction(int action);
+
public static class ServoOptions {
public String args;
public String url;
@@ -109,6 +111,10 @@ public class JNIServo {
void onHistoryChanged(boolean canGoBack, boolean canGoForward);
void onShutdownComplete();
+
+ void onMediaSessionMetadata(String title, String artist, String album);
+
+ void onMediaSessionPlaybackStateChange(int state);
}
}
diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java
index dac970d1acf..f45f55e79f5 100644
--- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java
+++ b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/Servo.java
@@ -168,6 +168,10 @@ public class Servo {
mSuspended = suspended;
}
+ public void mediaSessionAction(int action) {
+ mRunCallback.inGLThread(() -> mJNI.mediaSessionAction(action));
+ }
+
public interface Client {
void onAlert(String message);
@@ -184,6 +188,10 @@ public class Servo {
void onHistoryChanged(boolean canGoBack, boolean canGoForward);
void onRedrawing(boolean redrawing);
+
+ void onMediaSessionMetadata(String title, String artist, String album);
+
+ void onMediaSessionPlaybackStateChange(int state);
}
public interface RunCallback {
@@ -269,5 +277,13 @@ public class Servo {
public void onRedrawing(boolean redrawing) {
mRunCallback.inUIThread(() -> mClient.onRedrawing(redrawing));
}
+
+ public void onMediaSessionMetadata(String title, String artist, String album) {
+ mRunCallback.inUIThread(() -> mClient.onMediaSessionMetadata(title, artist, album));
+ }
+
+ public void onMediaSessionPlaybackStateChange(int state) {
+ mRunCallback.inUIThread(() -> mClient.onMediaSessionPlaybackStateChange(state));
+ }
}
}
diff --git a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java
index 33431afa19a..3784bbf58cb 100644
--- a/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java
+++ b/support/android/apk/servoview/src/main/java/org/mozilla/servoview/ServoView.java
@@ -134,8 +134,12 @@ public class ServoView extends GLSurfaceView
}
}
+ public void mediaSessionAction(int action) {
+ mServo.mediaSessionAction(action);
+ }
+
public void flushGLBuffers() {
- requestRender();
+ requestRender();
}
// Scroll and click
diff --git a/tests/wpt/include.ini b/tests/wpt/include.ini
index 6fded483450..2f662095640 100644
--- a/tests/wpt/include.ini
+++ b/tests/wpt/include.ini
@@ -119,6 +119,8 @@ skip: true
skip: true
[js]
skip: false
+[mediasession]
+ skip: false
[navigation-timing]
skip: false
[offscreen-canvas]
diff --git a/tests/wpt/metadata/mediasession/idlharness.window.js.ini b/tests/wpt/metadata/mediasession/idlharness.window.js.ini
new file mode 100644
index 00000000000..5e65e535b96
--- /dev/null
+++ b/tests/wpt/metadata/mediasession/idlharness.window.js.ini
@@ -0,0 +1,16 @@
+[idlharness.window.html]
+ [MediaSession interface: calling setPositionState(MediaPositionState) on navigator.mediaSession with too few arguments must throw TypeError]
+ expected: FAIL
+
+ [MediaSession interface: navigator.mediaSession must inherit property "setPositionState(MediaPositionState)" with the proper type]
+ expected: FAIL
+
+ [MediaSession interface: operation setPositionState(MediaPositionState)]
+ expected: FAIL
+
+ [MediaMetadata interface: attribute artwork]
+ expected: FAIL
+
+ [MediaMetadata interface: new MediaMetadata() must inherit property "artwork" with the proper type]
+ expected: FAIL
+
diff --git a/tests/wpt/metadata/mediasession/mediametadata.html.ini b/tests/wpt/metadata/mediasession/mediametadata.html.ini
new file mode 100644
index 00000000000..2a2b3e73c85
--- /dev/null
+++ b/tests/wpt/metadata/mediasession/mediametadata.html.ini
@@ -0,0 +1,43 @@
+[mediametadata.html]
+ [Test that MediaMetadata.artwork is Frozen]
+ expected: FAIL
+
+ [Test that MediaMetadat.artwork can't be modified]
+ expected: FAIL
+
+ [Test that resetting metadata to null is reflected]
+ expected: FAIL
+
+ [Test the default values for MediaMetadata with empty init dictionary]
+ expected: FAIL
+
+ [Test MediaImage default values]
+ expected: FAIL
+
+ [Test that mediaSession.metadata is properly set]
+ expected: FAIL
+
+ [Test that changes to metadata propagate properly]
+ expected: FAIL
+
+ [Test that MediaMetadata.artwork returns parsed urls]
+ expected: FAIL
+
+ [Test the different values allowed in MediaMetadata init dictionary]
+ expected: FAIL
+
+ [Test the default values for MediaMetadata with no init dictionary]
+ expected: FAIL
+
+ [Test that MediaImage.src is required]
+ expected: FAIL
+
+ [Test that MediaMetadata throws when setting an invalid url]
+ expected: FAIL
+
+ [Test that MediaMetadata.artwork will not expose unknown properties]
+ expected: FAIL
+
+ [Test that the base URL of MediaImage is the base URL of entry setting object]
+ expected: FAIL
+
diff --git a/tests/wpt/metadata/mediasession/positionstate.html.ini b/tests/wpt/metadata/mediasession/positionstate.html.ini
new file mode 100644
index 00000000000..ca73e31fb84
--- /dev/null
+++ b/tests/wpt/metadata/mediasession/positionstate.html.ini
@@ -0,0 +1,19 @@
+[positionstate.html]
+ [Test setPositionState with a null value]
+ expected: FAIL
+
+ [Test setPositionState with zero duration]
+ expected: FAIL
+
+ [Test setPositionState with a valid value for forward playback]
+ expected: FAIL
+
+ [Test setPositionState with optional position]
+ expected: FAIL
+
+ [Test setPositionState with only duration]
+ expected: FAIL
+
+ [Test setPositionState with optional playback rate]
+ expected: FAIL
+
diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json
index e675b665aa6..409d33b4c58 100644
--- a/tests/wpt/mozilla/meta/MANIFEST.json
+++ b/tests/wpt/mozilla/meta/MANIFEST.json
@@ -19023,7 +19023,7 @@
"testharness"
],
"mozilla/interfaces.html": [
- "4006cae2d79ba4ca21c229084fcb528b8a4156f1",
+ "f1d58732adafef4afc9f9b7f16d6961e4b74a5e9",
"testharness"
],
"mozilla/interfaces.js": [
diff --git a/tests/wpt/mozilla/tests/mozilla/interfaces.html b/tests/wpt/mozilla/tests/mozilla/interfaces.html
index 4006cae2d79..f1d58732ada 100644
--- a/tests/wpt/mozilla/tests/mozilla/interfaces.html
+++ b/tests/wpt/mozilla/tests/mozilla/interfaces.html
@@ -166,8 +166,10 @@ test_interfaces([
"MediaElementAudioSourceNode",
"MediaError",
"MediaList",
+ "MediaMetadata",
"MediaQueryList",
"MediaQueryListEvent",
+ "MediaSession",
"MessageChannel",
"MessageEvent",
"MessagePort",