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:
bors-servo 2019-11-20 12:31:29 -05:00 committed by GitHub
commit f6348b8b54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1112 additions and 21 deletions

View file

@ -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"),
}
}
}

View file

@ -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");
}
}
}

View file

@ -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 its 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),
}

View file

@ -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]

View file

@ -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]

View 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();
}
}

View 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,
}
}
}

View file

@ -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;

View file

@ -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)
})
}
}

View 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 = [];
};

View 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);
};

View file

@ -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() };

View file

@ -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"),
}
}
}

View file

@ -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)
}

View file

@ -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
);
}
},
}
}

View file

@ -449,6 +449,10 @@ where
error!("Failed to store profile: {}", e);
}
},
EmbedderMsg::MediaSessionEvent(_) => {
debug!("MediaSessionEvent received");
// TODO(ferjm): MediaSession support for Glutin based browsers.
},
}
}
}

View file

@ -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(..) |

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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));
}
}
}

View file

@ -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

View file

@ -119,6 +119,8 @@ skip: true
skip: true
[js]
skip: false
[mediasession]
skip: false
[navigation-timing]
skip: false
[offscreen-canvas]

View 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

View 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

View 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

View file

@ -19023,7 +19023,7 @@
"testharness"
],
"mozilla/interfaces.html": [
"4006cae2d79ba4ca21c229084fcb528b8a4156f1",
"f1d58732adafef4afc9f9b7f16d6961e4b74a5e9",
"testharness"
],
"mozilla/interfaces.js": [

View file

@ -166,8 +166,10 @@ test_interfaces([
"MediaElementAudioSourceNode",
"MediaError",
"MediaList",
"MediaMetadata",
"MediaQueryList",
"MediaQueryListEvent",
"MediaSession",
"MessageChannel",
"MessageEvent",
"MessagePort",