mirror of
https://github.com/servo/servo.git
synced 2025-07-23 07:13:52 +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 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
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 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;
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
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,
|
||||
};
|
||||
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() };
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::MediaSessionEvent(_) => {
|
||||
debug!("MediaSessionEvent received");
|
||||
// TODO(ferjm): MediaSession support for Glutin based browsers.
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(..) |
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -119,6 +119,8 @@ skip: true
|
|||
skip: true
|
||||
[js]
|
||||
skip: false
|
||||
[mediasession]
|
||||
skip: false
|
||||
[navigation-timing]
|
||||
skip: false
|
||||
[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"
|
||||
],
|
||||
"mozilla/interfaces.html": [
|
||||
"4006cae2d79ba4ca21c229084fcb528b8a4156f1",
|
||||
"f1d58732adafef4afc9f9b7f16d6961e4b74a5e9",
|
||||
"testharness"
|
||||
],
|
||||
"mozilla/interfaces.js": [
|
||||
|
|
|
@ -166,8 +166,10 @@ test_interfaces([
|
|||
"MediaElementAudioSourceNode",
|
||||
"MediaError",
|
||||
"MediaList",
|
||||
"MediaMetadata",
|
||||
"MediaQueryList",
|
||||
"MediaQueryListEvent",
|
||||
"MediaSession",
|
||||
"MessageChannel",
|
||||
"MessageEvent",
|
||||
"MessagePort",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue