mirror of
https://github.com/servo/servo.git
synced 2025-08-03 20:50:07 +01:00
Auto merge of #24499 - ferjm:media.session.api, r=Manishearth
Media Session API - [X] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors - [x] These changes fix #24172 - [x] There are tests for these changes This PR introduces all the pieces required to prove an end to end media session flow with Android as a test platform.
This commit is contained in:
commit
f6348b8b54
37 changed files with 1112 additions and 21 deletions
|
@ -10,7 +10,7 @@ use euclid::Scale;
|
||||||
use gleam::gl;
|
use gleam::gl;
|
||||||
use keyboard_types::KeyboardEvent;
|
use keyboard_types::KeyboardEvent;
|
||||||
use msg::constellation_msg::{PipelineId, TopLevelBrowsingContextId, TraversalDirection};
|
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_geometry::DeviceIndependentPixel;
|
||||||
use servo_media::player::context::{GlApi, GlContext, NativeDisplay};
|
use servo_media::player::context::{GlApi, GlContext, NativeDisplay};
|
||||||
use servo_url::ServoUrl;
|
use servo_url::ServoUrl;
|
||||||
|
@ -102,6 +102,9 @@ pub enum WindowEvent {
|
||||||
CaptureWebRender,
|
CaptureWebRender,
|
||||||
/// Toggle sampling profiler with the given sampling rate and max duration.
|
/// Toggle sampling profiler with the given sampling rate and max duration.
|
||||||
ToggleSamplingProfiler(Duration, 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 {
|
impl Debug for WindowEvent {
|
||||||
|
@ -132,6 +135,7 @@ impl Debug for WindowEvent {
|
||||||
WindowEvent::CaptureWebRender => write!(f, "CaptureWebRender"),
|
WindowEvent::CaptureWebRender => write!(f, "CaptureWebRender"),
|
||||||
WindowEvent::ToggleSamplingProfiler(..) => write!(f, "ToggleSamplingProfiler"),
|
WindowEvent::ToggleSamplingProfiler(..) => write!(f, "ToggleSamplingProfiler"),
|
||||||
WindowEvent::ExitFullScreen(..) => write!(f, "ExitFullScreen"),
|
WindowEvent::ExitFullScreen(..) => write!(f, "ExitFullScreen"),
|
||||||
|
WindowEvent::MediaSessionAction(..) => write!(f, "MediaSessionAction"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ use compositing::SendableFrameTree;
|
||||||
use crossbeam_channel::{after, never, unbounded, Receiver, Sender};
|
use crossbeam_channel::{after, never, unbounded, Receiver, Sender};
|
||||||
use devtools_traits::{ChromeToDevtoolsControlMsg, DevtoolsControlMsg};
|
use devtools_traits::{ChromeToDevtoolsControlMsg, DevtoolsControlMsg};
|
||||||
use embedder_traits::{Cursor, EmbedderMsg, EmbedderProxy, EventLoopWaker};
|
use embedder_traits::{Cursor, EmbedderMsg, EmbedderProxy, EventLoopWaker};
|
||||||
|
use embedder_traits::{MediaSessionEvent, MediaSessionPlaybackState};
|
||||||
use euclid::{default::Size2D as UntypedSize2D, Size2D};
|
use euclid::{default::Size2D as UntypedSize2D, Size2D};
|
||||||
use gfx::font_cache_thread::FontCacheThread;
|
use gfx::font_cache_thread::FontCacheThread;
|
||||||
use gfx_traits::Epoch;
|
use gfx_traits::Epoch;
|
||||||
|
@ -139,7 +140,6 @@ use net_traits::{self, FetchResponseMsg, IpcSend, ResourceThreads};
|
||||||
use profile_traits::mem;
|
use profile_traits::mem;
|
||||||
use profile_traits::time;
|
use profile_traits::time;
|
||||||
use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent};
|
use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent};
|
||||||
use script_traits::MouseEventType;
|
|
||||||
use script_traits::{webdriver_msg, LogEntry, ScriptToConstellationChan, ServiceWorkerMsg};
|
use script_traits::{webdriver_msg, LogEntry, ScriptToConstellationChan, ServiceWorkerMsg};
|
||||||
use script_traits::{
|
use script_traits::{
|
||||||
AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo, CompositorEvent,
|
AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo, CompositorEvent,
|
||||||
|
@ -153,6 +153,7 @@ use script_traits::{
|
||||||
IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, TimerSchedulerMsg,
|
IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, TimerSchedulerMsg,
|
||||||
};
|
};
|
||||||
use script_traits::{LayoutMsg as FromLayoutMsg, ScriptMsg as FromScriptMsg, ScriptThreadFactory};
|
use script_traits::{LayoutMsg as FromLayoutMsg, ScriptMsg as FromScriptMsg, ScriptThreadFactory};
|
||||||
|
use script_traits::{MediaSessionActionType, MouseEventType};
|
||||||
use script_traits::{MessagePortMsg, PortMessageTask, StructuredSerializedData};
|
use script_traits::{MessagePortMsg, PortMessageTask, StructuredSerializedData};
|
||||||
use script_traits::{SWManagerMsg, ScopeThings, UpdatePipelineIdReason, WebDriverCommandMsg};
|
use script_traits::{SWManagerMsg, ScopeThings, UpdatePipelineIdReason, WebDriverCommandMsg};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -474,6 +475,9 @@ pub struct Constellation<Message, LTF, STF> {
|
||||||
|
|
||||||
/// Mechanism to force the compositor to process events.
|
/// Mechanism to force the compositor to process events.
|
||||||
event_loop_waker: Option<Box<dyn EventLoopWaker>>,
|
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.
|
/// State needed to construct a constellation.
|
||||||
|
@ -843,6 +847,7 @@ where
|
||||||
glplayer_threads: state.glplayer_threads,
|
glplayer_threads: state.glplayer_threads,
|
||||||
player_context: state.player_context,
|
player_context: state.player_context,
|
||||||
event_loop_waker: state.event_loop_waker,
|
event_loop_waker: state.event_loop_waker,
|
||||||
|
active_media_session: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
constellation.run();
|
constellation.run();
|
||||||
|
@ -1541,6 +1546,9 @@ where
|
||||||
FromCompositorMsg::ExitFullScreen(top_level_browsing_context_id) => {
|
FromCompositorMsg::ExitFullScreen(top_level_browsing_context_id) => {
|
||||||
self.handle_exit_fullscreen_msg(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,
|
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));
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,6 +162,9 @@ pub enum EmbedderMsg {
|
||||||
Shutdown,
|
Shutdown,
|
||||||
/// Report a complete sampled profile
|
/// Report a complete sampled profile
|
||||||
ReportProfile(Vec<u8>),
|
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 {
|
impl Debug for EmbedderMsg {
|
||||||
|
@ -194,6 +197,7 @@ impl Debug for EmbedderMsg {
|
||||||
EmbedderMsg::AllowOpeningBrowser(..) => write!(f, "AllowOpeningBrowser"),
|
EmbedderMsg::AllowOpeningBrowser(..) => write!(f, "AllowOpeningBrowser"),
|
||||||
EmbedderMsg::BrowserCreated(..) => write!(f, "BrowserCreated"),
|
EmbedderMsg::BrowserCreated(..) => write!(f, "BrowserCreated"),
|
||||||
EmbedderMsg::ReportProfile(..) => write!(f, "ReportProfile"),
|
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 ".")
|
/// the `String` content is expected to be extension (e.g, "doc", without the prefixing ".")
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct FilterPattern(pub String);
|
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),
|
||||||
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ use content_security_policy::CspList;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use cssparser::RGBA;
|
use cssparser::RGBA;
|
||||||
use devtools_traits::{CSSError, TimelineMarkerType, WorkerId};
|
use devtools_traits::{CSSError, TimelineMarkerType, WorkerId};
|
||||||
use embedder_traits::EventLoopWaker;
|
use embedder_traits::{EventLoopWaker, MediaMetadata};
|
||||||
use encoding_rs::{Decoder, Encoding};
|
use encoding_rs::{Decoder, Encoding};
|
||||||
use euclid::default::{Point2D, Rect, Rotation3D, Transform2D, Transform3D};
|
use euclid::default::{Point2D, Rect, Rotation3D, Transform2D, Transform3D};
|
||||||
use euclid::Length as EuclidLength;
|
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::rpc::LayoutRPC;
|
||||||
use script_layout_interface::OpaqueStyleAndLayoutData;
|
use script_layout_interface::OpaqueStyleAndLayoutData;
|
||||||
use script_traits::transferable::MessagePortImpl;
|
use script_traits::transferable::MessagePortImpl;
|
||||||
use script_traits::DrawAPaintImageResult;
|
use script_traits::{DocumentActivity, DrawAPaintImageResult};
|
||||||
use script_traits::{DocumentActivity, ScriptToConstellationChan, TimerEventId, TimerSource};
|
use script_traits::{MediaSessionActionType, ScriptToConstellationChan, TimerEventId, TimerSource};
|
||||||
use script_traits::{UntrustedNodeAddress, WindowSizeData, WindowSizeType};
|
use script_traits::{UntrustedNodeAddress, WindowSizeData, WindowSizeType};
|
||||||
use selectors::matching::ElementSelectorFlags;
|
use selectors::matching::ElementSelectorFlags;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -536,6 +536,8 @@ unsafe_no_jsmanaged_fields!(WindowGLContext);
|
||||||
unsafe_no_jsmanaged_fields!(VideoFrame);
|
unsafe_no_jsmanaged_fields!(VideoFrame);
|
||||||
unsafe_no_jsmanaged_fields!(WebGLContextId);
|
unsafe_no_jsmanaged_fields!(WebGLContextId);
|
||||||
unsafe_no_jsmanaged_fields!(Arc<Mutex<dyn AudioRenderer>>);
|
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 {
|
unsafe impl<'a> JSTraceable for &'a str {
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -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::HTMLSourceElementBinding::HTMLSourceElementMethods;
|
||||||
use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*;
|
use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*;
|
||||||
use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods;
|
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::NodeBinding::NodeBinding::NodeMethods;
|
||||||
use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode};
|
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::{ElementTypeId, HTMLElementTypeId};
|
||||||
use crate::dom::bindings::codegen::InheritTypes::{HTMLMediaElementTypeId, NodeTypeId};
|
use crate::dom::bindings::codegen::InheritTypes::{HTMLMediaElementTypeId, NodeTypeId};
|
||||||
use crate::dom::bindings::codegen::UnionTypes::{
|
use crate::dom::bindings::codegen::UnionTypes::{
|
||||||
|
@ -65,6 +67,7 @@ use crate::script_thread::ScriptThread;
|
||||||
use crate::task_source::TaskSource;
|
use crate::task_source::TaskSource;
|
||||||
use dom_struct::dom_struct;
|
use dom_struct::dom_struct;
|
||||||
use embedder_traits::resources::{self, Resource as EmbedderResource};
|
use embedder_traits::resources::{self, Resource as EmbedderResource};
|
||||||
|
use embedder_traits::{MediaSessionEvent, MediaSessionPlaybackState};
|
||||||
use euclid::default::Size2D;
|
use euclid::default::Size2D;
|
||||||
use headers::{ContentLength, ContentRange, HeaderMapExt};
|
use headers::{ContentLength, ContentRange, HeaderMapExt};
|
||||||
use html5ever::{LocalName, Prefix};
|
use html5ever::{LocalName, Prefix};
|
||||||
|
@ -592,7 +595,6 @@ impl HTMLMediaElement {
|
||||||
match (old_ready_state, ready_state) {
|
match (old_ready_state, ready_state) {
|
||||||
(ReadyState::HaveNothing, ReadyState::HaveMetadata) => {
|
(ReadyState::HaveNothing, ReadyState::HaveMetadata) => {
|
||||||
task_source.queue_simple_event(self.upcast(), atom!("loadedmetadata"), &window);
|
task_source.queue_simple_event(self.upcast(), atom!("loadedmetadata"), &window);
|
||||||
|
|
||||||
// No other steps are applicable in this case.
|
// No other steps are applicable in this case.
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
@ -1725,6 +1727,17 @@ impl HTMLMediaElement {
|
||||||
if self.Controls() {
|
if self.Controls() {
|
||||||
self.render_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 => {
|
PlayerEvent::NeedData => {
|
||||||
// The player needs more data.
|
// The player needs more data.
|
||||||
|
@ -1782,13 +1795,33 @@ impl HTMLMediaElement {
|
||||||
};
|
};
|
||||||
ScriptThread::await_stable_state(Microtask::MediaElement(task));
|
ScriptThread::await_stable_state(Microtask::MediaElement(task));
|
||||||
},
|
},
|
||||||
PlayerEvent::StateChanged(ref state) => match *state {
|
PlayerEvent::StateChanged(ref state) => {
|
||||||
|
let mut media_session_playback_state = MediaSessionPlaybackState::None_;
|
||||||
|
match *state {
|
||||||
PlaybackState::Paused => {
|
PlaybackState::Paused => {
|
||||||
|
media_session_playback_state = MediaSessionPlaybackState::Paused;
|
||||||
if self.ready_state.get() == ReadyState::HaveMetadata {
|
if self.ready_state.get() == ReadyState::HaveMetadata {
|
||||||
self.change_ready_state(ReadyState::HaveEnoughData);
|
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();
|
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]
|
// XXX Placeholder for [https://github.com/servo/servo/issues/22293]
|
||||||
|
|
97
components/script/dom/mediametadata.rs
Normal file
97
components/script/dom/mediametadata.rs
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
213
components/script/dom/mediasession.rs
Normal file
213
components/script/dom/mediasession.rs
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -398,8 +398,10 @@ pub mod mediaelementaudiosourcenode;
|
||||||
pub mod mediaerror;
|
pub mod mediaerror;
|
||||||
pub mod mediafragmentparser;
|
pub mod mediafragmentparser;
|
||||||
pub mod medialist;
|
pub mod medialist;
|
||||||
|
pub mod mediametadata;
|
||||||
pub mod mediaquerylist;
|
pub mod mediaquerylist;
|
||||||
pub mod mediaquerylistevent;
|
pub mod mediaquerylistevent;
|
||||||
|
pub mod mediasession;
|
||||||
pub mod mediastream;
|
pub mod mediastream;
|
||||||
pub mod mediastreamtrack;
|
pub mod mediastreamtrack;
|
||||||
pub mod messagechannel;
|
pub mod messagechannel;
|
||||||
|
|
|
@ -12,6 +12,7 @@ use crate::dom::bindings::str::DOMString;
|
||||||
use crate::dom::bluetooth::Bluetooth;
|
use crate::dom::bluetooth::Bluetooth;
|
||||||
use crate::dom::gamepadlist::GamepadList;
|
use crate::dom::gamepadlist::GamepadList;
|
||||||
use crate::dom::mediadevices::MediaDevices;
|
use crate::dom::mediadevices::MediaDevices;
|
||||||
|
use crate::dom::mediasession::MediaSession;
|
||||||
use crate::dom::mimetypearray::MimeTypeArray;
|
use crate::dom::mimetypearray::MimeTypeArray;
|
||||||
use crate::dom::navigatorinfo;
|
use crate::dom::navigatorinfo;
|
||||||
use crate::dom::permissions::Permissions;
|
use crate::dom::permissions::Permissions;
|
||||||
|
@ -34,6 +35,7 @@ pub struct Navigator {
|
||||||
mediadevices: MutNullableDom<MediaDevices>,
|
mediadevices: MutNullableDom<MediaDevices>,
|
||||||
gamepads: MutNullableDom<GamepadList>,
|
gamepads: MutNullableDom<GamepadList>,
|
||||||
permissions: MutNullableDom<Permissions>,
|
permissions: MutNullableDom<Permissions>,
|
||||||
|
mediasession: MutNullableDom<MediaSession>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Navigator {
|
impl Navigator {
|
||||||
|
@ -48,6 +50,7 @@ impl Navigator {
|
||||||
mediadevices: Default::default(),
|
mediadevices: Default::default(),
|
||||||
gamepads: Default::default(),
|
gamepads: Default::default(),
|
||||||
permissions: Default::default(),
|
permissions: Default::default(),
|
||||||
|
mediasession: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,4 +189,20 @@ impl NavigatorMethods for Navigator {
|
||||||
self.mediadevices
|
self.mediadevices
|
||||||
.or_init(|| MediaDevices::new(&self.global()))
|
.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
30
components/script/dom/webidls/MediaMetadata.webidl
Normal file
30
components/script/dom/webidls/MediaMetadata.webidl
Normal file
|
@ -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 = [];
|
||||||
|
};
|
57
components/script/dom/webidls/MediaSession.webidl
Normal file
57
components/script/dom/webidls/MediaSession.webidl
Normal file
|
@ -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);
|
||||||
|
};
|
||||||
|
|
|
@ -138,7 +138,7 @@ use script_traits::{
|
||||||
DiscardBrowsingContext, DocumentActivity, EventResult, HistoryEntryReplacement,
|
DiscardBrowsingContext, DocumentActivity, EventResult, HistoryEntryReplacement,
|
||||||
};
|
};
|
||||||
use script_traits::{InitialScriptState, JsEvalResult, LayoutMsg, LoadData, LoadOrigin};
|
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::{Painter, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory};
|
||||||
use script_traits::{ScriptToConstellationChan, TimerEvent, TimerSchedulerMsg};
|
use script_traits::{ScriptToConstellationChan, TimerEvent, TimerSchedulerMsg};
|
||||||
use script_traits::{TimerSource, TouchEventType, TouchId, UntrustedNodeAddress, WheelDelta};
|
use script_traits::{TimerSource, TouchEventType, TouchId, UntrustedNodeAddress, WheelDelta};
|
||||||
|
@ -1713,6 +1713,7 @@ impl ScriptThread {
|
||||||
WebVREvents(id, ..) => Some(id),
|
WebVREvents(id, ..) => Some(id),
|
||||||
PaintMetric(..) => None,
|
PaintMetric(..) => None,
|
||||||
ExitFullScreen(id, ..) => Some(id),
|
ExitFullScreen(id, ..) => Some(id),
|
||||||
|
MediaSessionAction(..) => None,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MixedMessage::FromDevtools(_) => None,
|
MixedMessage::FromDevtools(_) => None,
|
||||||
|
@ -1942,6 +1943,9 @@ impl ScriptThread {
|
||||||
ConstellationControlMsg::PaintMetric(pipeline_id, metric_type, metric_value) => {
|
ConstellationControlMsg::PaintMetric(pipeline_id, metric_type, metric_value) => {
|
||||||
self.handle_paint_metric(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::AttachLayout(..) |
|
||||||
msg @ ConstellationControlMsg::Viewport(..) |
|
msg @ ConstellationControlMsg::Viewport(..) |
|
||||||
msg @ ConstellationControlMsg::SetScrollState(..) |
|
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) {
|
pub fn enqueue_microtask(job: Microtask) {
|
||||||
SCRIPT_THREAD_ROOT.with(|root| {
|
SCRIPT_THREAD_ROOT.with(|root| {
|
||||||
let script_thread = unsafe { &*root.get().unwrap() };
|
let script_thread = unsafe { &*root.get().unwrap() };
|
||||||
|
|
|
@ -388,6 +388,8 @@ pub enum ConstellationControlMsg {
|
||||||
WebVREvents(PipelineId, Vec<WebVREvent>),
|
WebVREvents(PipelineId, Vec<WebVREvent>),
|
||||||
/// Notifies the script thread about a new recorded paint metric.
|
/// Notifies the script thread about a new recorded paint metric.
|
||||||
PaintMetric(PipelineId, ProgressiveWebMetricType, u64),
|
PaintMetric(PipelineId, ProgressiveWebMetricType, u64),
|
||||||
|
/// Notifies the media session about a user requested media session action.
|
||||||
|
MediaSessionAction(PipelineId, MediaSessionActionType),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ConstellationControlMsg {
|
impl fmt::Debug for ConstellationControlMsg {
|
||||||
|
@ -426,6 +428,7 @@ impl fmt::Debug for ConstellationControlMsg {
|
||||||
WebVREvents(..) => "WebVREvents",
|
WebVREvents(..) => "WebVREvents",
|
||||||
PaintMetric(..) => "PaintMetric",
|
PaintMetric(..) => "PaintMetric",
|
||||||
ExitFullScreen(..) => "ExitFullScreen",
|
ExitFullScreen(..) => "ExitFullScreen",
|
||||||
|
MediaSessionAction(..) => "MediaSessionAction",
|
||||||
};
|
};
|
||||||
write!(formatter, "ConstellationControlMsg::{}", variant)
|
write!(formatter, "ConstellationControlMsg::{}", variant)
|
||||||
}
|
}
|
||||||
|
@ -877,6 +880,8 @@ pub enum ConstellationMsg {
|
||||||
DisableProfiler,
|
DisableProfiler,
|
||||||
/// Request to exit from fullscreen mode
|
/// Request to exit from fullscreen mode
|
||||||
ExitFullScreen(TopLevelBrowsingContextId),
|
ExitFullScreen(TopLevelBrowsingContextId),
|
||||||
|
/// Media session action.
|
||||||
|
MediaSessionAction(MediaSessionActionType),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ConstellationMsg {
|
impl fmt::Debug for ConstellationMsg {
|
||||||
|
@ -907,6 +912,7 @@ impl fmt::Debug for ConstellationMsg {
|
||||||
EnableProfiler(..) => "EnableProfiler",
|
EnableProfiler(..) => "EnableProfiler",
|
||||||
DisableProfiler => "DisableProfiler",
|
DisableProfiler => "DisableProfiler",
|
||||||
ExitFullScreen(..) => "ExitFullScreen",
|
ExitFullScreen(..) => "ExitFullScreen",
|
||||||
|
MediaSessionAction(..) => "MediaSessionAction",
|
||||||
};
|
};
|
||||||
write!(formatter, "ConstellationMsg::{}", variant)
|
write!(formatter, "ConstellationMsg::{}", variant)
|
||||||
}
|
}
|
||||||
|
@ -1053,3 +1059,49 @@ pub enum MessagePortMsg {
|
||||||
/// Handle a new port-message-task.
|
/// Handle a new port-message-task.
|
||||||
NewTask(MessagePortId, PortMessageTask),
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::WorkerGlobalScopeInit;
|
||||||
use crate::WorkerScriptLoadOrigin;
|
use crate::WorkerScriptLoadOrigin;
|
||||||
use canvas_traits::canvas::{CanvasId, CanvasMsg};
|
use canvas_traits::canvas::{CanvasId, CanvasMsg};
|
||||||
use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId};
|
use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId};
|
||||||
use embedder_traits::EmbedderMsg;
|
use embedder_traits::{EmbedderMsg, MediaSessionEvent};
|
||||||
use euclid::default::Size2D as UntypedSize2D;
|
use euclid::default::Size2D as UntypedSize2D;
|
||||||
use euclid::Size2D;
|
use euclid::Size2D;
|
||||||
use gfx_traits::Epoch;
|
use gfx_traits::Epoch;
|
||||||
|
@ -254,6 +254,9 @@ pub enum ScriptMsg {
|
||||||
GetScreenSize(IpcSender<DeviceIntSize>),
|
GetScreenSize(IpcSender<DeviceIntSize>),
|
||||||
/// Get the available screen size (pixel)
|
/// Get the available screen size (pixel)
|
||||||
GetScreenAvailSize(IpcSender<DeviceIntSize>),
|
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 {
|
impl fmt::Debug for ScriptMsg {
|
||||||
|
@ -305,6 +308,7 @@ impl fmt::Debug for ScriptMsg {
|
||||||
GetClientWindow(..) => "GetClientWindow",
|
GetClientWindow(..) => "GetClientWindow",
|
||||||
GetScreenSize(..) => "GetScreenSize",
|
GetScreenSize(..) => "GetScreenSize",
|
||||||
GetScreenAvailSize(..) => "GetScreenAvailSize",
|
GetScreenAvailSize(..) => "GetScreenAvailSize",
|
||||||
|
MediaSessionEvent(..) => "MediaSessionEvent",
|
||||||
};
|
};
|
||||||
write!(formatter, "ScriptMsg::{}", variant)
|
write!(formatter, "ScriptMsg::{}", variant)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -449,6 +449,10 @@ where
|
||||||
error!("Failed to store profile: {}", e);
|
error!("Failed to store profile: {}", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
EmbedderMsg::MediaSessionEvent(_) => {
|
||||||
|
debug!("MediaSessionEvent received");
|
||||||
|
// TODO(ferjm): MediaSession support for Glutin based browsers.
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use servo::compositing::windowing::{
|
||||||
WindowMethods,
|
WindowMethods,
|
||||||
};
|
};
|
||||||
use servo::embedder_traits::resources::{self, Resource, ResourceReaderMethods};
|
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::euclid::{Point2D, Rect, Scale, Size2D, Vector2D};
|
||||||
use servo::keyboard_types::{Key, KeyState, KeyboardEvent};
|
use servo::keyboard_types::{Key, KeyState, KeyboardEvent};
|
||||||
use servo::msg::constellation_msg::TraversalDirection;
|
use servo::msg::constellation_msg::TraversalDirection;
|
||||||
|
@ -126,10 +126,14 @@ pub trait HostTrait {
|
||||||
fn on_shutdown_complete(&self);
|
fn on_shutdown_complete(&self);
|
||||||
/// A text input is focused.
|
/// A text input is focused.
|
||||||
fn on_ime_state_changed(&self, show: bool);
|
fn on_ime_state_changed(&self, show: bool);
|
||||||
/// Gets sytem clipboard contents
|
/// Gets sytem clipboard contents.
|
||||||
fn get_clipboard_contents(&self) -> Option<String>;
|
fn get_clipboard_contents(&self) -> Option<String>;
|
||||||
/// Sets system clipboard contents
|
/// Sets system clipboard contents.
|
||||||
fn set_clipboard_contents(&self, contents: String);
|
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 {
|
pub struct ServoGlue {
|
||||||
|
@ -466,6 +470,11 @@ impl ServoGlue {
|
||||||
self.process_event(WindowEvent::Keyboard(key_event))
|
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> {
|
fn process_event(&mut self, event: WindowEvent) -> Result<(), &'static str> {
|
||||||
self.events.push(event);
|
self.events.push(event);
|
||||||
if !self.batch_mode {
|
if !self.batch_mode {
|
||||||
|
@ -572,6 +581,21 @@ impl ServoGlue {
|
||||||
EmbedderMsg::HideIME => {
|
EmbedderMsg::HideIME => {
|
||||||
self.callbacks.host_callbacks.on_ime_state_changed(false);
|
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::Status(..) |
|
||||||
EmbedderMsg::SelectFiles(..) |
|
EmbedderMsg::SelectFiles(..) |
|
||||||
EmbedderMsg::MoveTo(..) |
|
EmbedderMsg::MoveTo(..) |
|
||||||
|
|
|
@ -216,6 +216,9 @@ pub struct CHostCallbacks {
|
||||||
pub on_ime_state_changed: extern "C" fn(show: bool),
|
pub on_ime_state_changed: extern "C" fn(show: bool),
|
||||||
pub get_clipboard_contents: extern "C" fn() -> *const c_char,
|
pub get_clipboard_contents: extern "C" fn() -> *const c_char,
|
||||||
pub set_clipboard_contents: extern "C" fn(contents: *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
|
/// Servo options
|
||||||
|
@ -708,4 +711,20 @@ impl HostTrait for HostCallbacks {
|
||||||
let contents = CString::new(contents).expect("Can't create string");
|
let contents = CString::new(contents).expect("Can't create string");
|
||||||
(self.0.set_clipboard_contents)(contents.as_ptr());
|
(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
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 {
|
pub struct WakeupCallback {
|
||||||
callback: GlobalRef,
|
callback: GlobalRef,
|
||||||
jvm: Arc<JavaVM>,
|
jvm: Arc<JavaVM>,
|
||||||
|
@ -508,6 +518,48 @@ impl HostTrait for HostCallbacks {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_clipboard_contents(&self, _contents: String) {}
|
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) {
|
fn initialize_android_glue(env: &JNIEnv, activity: JObject) {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.servo.MediaSession;
|
||||||
import org.mozilla.servoview.ServoView;
|
import org.mozilla.servoview.ServoView;
|
||||||
import org.mozilla.servoview.Servo;
|
import org.mozilla.servoview.Servo;
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ public class MainActivity extends Activity implements Servo.Client {
|
||||||
ProgressBar mProgressBar;
|
ProgressBar mProgressBar;
|
||||||
TextView mIdleText;
|
TextView mIdleText;
|
||||||
boolean mCanGoBack;
|
boolean mCanGoBack;
|
||||||
|
MediaSession mMediaSession;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
@ -85,6 +87,12 @@ public class MainActivity extends Activity implements Servo.Client {
|
||||||
setupUrlField();
|
setupUrlField();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
mMediaSession.hideMediaSessionControls();
|
||||||
|
}
|
||||||
|
|
||||||
private void setupUrlField() {
|
private void setupUrlField() {
|
||||||
mUrlField.setOnEditorActionListener((v, actionId, event) -> {
|
mUrlField.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
@ -203,6 +211,7 @@ public class MainActivity extends Activity implements Servo.Client {
|
||||||
mServoView.onPause();
|
mServoView.onPause();
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
mServoView.onResume();
|
mServoView.onResume();
|
||||||
|
@ -217,4 +226,33 @@ public class MainActivity extends Activity implements Servo.Client {
|
||||||
super.onBackPressed();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_icon.png
Executable file
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_icon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 213 B |
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_next.png
Executable file
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_next.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 203 B |
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_pause.png
Executable file
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_pause.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 114 B |
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_play.png
Executable file
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_play.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 227 B |
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_prev.png
Executable file
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_prev.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 205 B |
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_stop.png
Executable file
BIN
support/android/apk/servoapp/src/main/res/drawable/media_session_stop.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 106 B |
|
@ -1,3 +1,5 @@
|
||||||
<resources>
|
<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>
|
</resources>
|
||||||
|
|
|
@ -66,6 +66,8 @@ public class JNIServo {
|
||||||
|
|
||||||
public native void click(float x, float y);
|
public native void click(float x, float y);
|
||||||
|
|
||||||
|
public native void mediaSessionAction(int action);
|
||||||
|
|
||||||
public static class ServoOptions {
|
public static class ServoOptions {
|
||||||
public String args;
|
public String args;
|
||||||
public String url;
|
public String url;
|
||||||
|
@ -109,6 +111,10 @@ public class JNIServo {
|
||||||
void onHistoryChanged(boolean canGoBack, boolean canGoForward);
|
void onHistoryChanged(boolean canGoBack, boolean canGoForward);
|
||||||
|
|
||||||
void onShutdownComplete();
|
void onShutdownComplete();
|
||||||
|
|
||||||
|
void onMediaSessionMetadata(String title, String artist, String album);
|
||||||
|
|
||||||
|
void onMediaSessionPlaybackStateChange(int state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,10 @@ public class Servo {
|
||||||
mSuspended = suspended;
|
mSuspended = suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void mediaSessionAction(int action) {
|
||||||
|
mRunCallback.inGLThread(() -> mJNI.mediaSessionAction(action));
|
||||||
|
}
|
||||||
|
|
||||||
public interface Client {
|
public interface Client {
|
||||||
void onAlert(String message);
|
void onAlert(String message);
|
||||||
|
|
||||||
|
@ -184,6 +188,10 @@ public class Servo {
|
||||||
void onHistoryChanged(boolean canGoBack, boolean canGoForward);
|
void onHistoryChanged(boolean canGoBack, boolean canGoForward);
|
||||||
|
|
||||||
void onRedrawing(boolean redrawing);
|
void onRedrawing(boolean redrawing);
|
||||||
|
|
||||||
|
void onMediaSessionMetadata(String title, String artist, String album);
|
||||||
|
|
||||||
|
void onMediaSessionPlaybackStateChange(int state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface RunCallback {
|
public interface RunCallback {
|
||||||
|
@ -269,5 +277,13 @@ public class Servo {
|
||||||
public void onRedrawing(boolean redrawing) {
|
public void onRedrawing(boolean redrawing) {
|
||||||
mRunCallback.inUIThread(() -> mClient.onRedrawing(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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,10 @@ public class ServoView extends GLSurfaceView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void mediaSessionAction(int action) {
|
||||||
|
mServo.mediaSessionAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
public void flushGLBuffers() {
|
public void flushGLBuffers() {
|
||||||
requestRender();
|
requestRender();
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,8 @@ skip: true
|
||||||
skip: true
|
skip: true
|
||||||
[js]
|
[js]
|
||||||
skip: false
|
skip: false
|
||||||
|
[mediasession]
|
||||||
|
skip: false
|
||||||
[navigation-timing]
|
[navigation-timing]
|
||||||
skip: false
|
skip: false
|
||||||
[offscreen-canvas]
|
[offscreen-canvas]
|
||||||
|
|
16
tests/wpt/metadata/mediasession/idlharness.window.js.ini
Normal file
16
tests/wpt/metadata/mediasession/idlharness.window.js.ini
Normal file
|
@ -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
|
||||||
|
|
43
tests/wpt/metadata/mediasession/mediametadata.html.ini
Normal file
43
tests/wpt/metadata/mediasession/mediametadata.html.ini
Normal file
|
@ -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
|
||||||
|
|
19
tests/wpt/metadata/mediasession/positionstate.html.ini
Normal file
19
tests/wpt/metadata/mediasession/positionstate.html.ini
Normal file
|
@ -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
|
||||||
|
|
|
@ -19023,7 +19023,7 @@
|
||||||
"testharness"
|
"testharness"
|
||||||
],
|
],
|
||||||
"mozilla/interfaces.html": [
|
"mozilla/interfaces.html": [
|
||||||
"4006cae2d79ba4ca21c229084fcb528b8a4156f1",
|
"f1d58732adafef4afc9f9b7f16d6961e4b74a5e9",
|
||||||
"testharness"
|
"testharness"
|
||||||
],
|
],
|
||||||
"mozilla/interfaces.js": [
|
"mozilla/interfaces.js": [
|
||||||
|
|
|
@ -166,8 +166,10 @@ test_interfaces([
|
||||||
"MediaElementAudioSourceNode",
|
"MediaElementAudioSourceNode",
|
||||||
"MediaError",
|
"MediaError",
|
||||||
"MediaList",
|
"MediaList",
|
||||||
|
"MediaMetadata",
|
||||||
"MediaQueryList",
|
"MediaQueryList",
|
||||||
"MediaQueryListEvent",
|
"MediaQueryListEvent",
|
||||||
|
"MediaSession",
|
||||||
"MessageChannel",
|
"MessageChannel",
|
||||||
"MessageEvent",
|
"MessageEvent",
|
||||||
"MessagePort",
|
"MessagePort",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue