Bug 1623715 - [1.7] Add MediaSession API for (DOM) media session control delegation. r=snorp,alwu,geckoview-reviewers,agi

Differential Revision: https://phabricator.services.mozilla.com/D84189
This commit is contained in:
Eugen Sawin 2020-08-17 20:37:14 +00:00
Родитель fdca957200
Коммит fc486d897e
9 изменённых файлов: 1385 добавлений и 6 удалений

Просмотреть файл

@ -649,6 +649,12 @@ function startup() {
frameScript: "chrome://geckoview/content/GeckoViewAutofillChild.js",
},
},
{
name: "GeckoViewMediaControl",
onEnable: {
resource: "resource://gre/modules/GeckoViewMediaControl.jsm",
},
},
]);
// TODO: Bug 1569360 Allows actors to temporarely access ModuleManager until

Просмотреть файл

@ -883,14 +883,18 @@ public class GeckoSession implements Parcelable {
}
};
private final MediaSession.Handler mMediaSessionHandler =
new MediaSession.Handler(this);
/* package */ int handlersCount;
private final GeckoSessionHandler<?>[] mSessionHandlers = new GeckoSessionHandler<?>[] {
mContentHandler, mHistoryHandler, mMediaHandler, mNavigationHandler,
mPermissionHandler, mProcessHangHandler, mProgressHandler, mScrollHandler,
mSelectionActionDelegate, mContentBlockingHandler
};
private final GeckoSessionHandler<?>[] mSessionHandlers =
new GeckoSessionHandler<?>[] {
mContentHandler, mHistoryHandler, mMediaHandler,
mNavigationHandler, mPermissionHandler, mProcessHangHandler,
mProgressHandler, mScrollHandler, mSelectionActionDelegate,
mContentBlockingHandler, mMediaSessionHandler
};
private static class PermissionCallback implements
PermissionDelegate.Callback, PermissionDelegate.MediaCallback {
@ -1119,6 +1123,14 @@ public class GeckoSession implements Parcelable {
@WrapForJNI(dispatchTo = "proxy")
public native void attachAccessibility(SessionAccessibility.NativeProvider sessionAccessibility);
@WrapForJNI(dispatchTo = "proxy")
public native void attachMediaSessionController(
final MediaSession.Controller controller, final long id);
@WrapForJNI(dispatchTo = "proxy")
public native void detachMediaSessionController(
final MediaSession.Controller controller);
@WrapForJNI(calledFrom = "gecko")
private synchronized void onReady(final @Nullable NativeQueue queue) {
// onReady is called the first time the Gecko window is ready, with a null queue
@ -2630,6 +2642,81 @@ public class GeckoSession implements Parcelable {
return mMediaHandler.getDelegate();
}
/**
* Set the media session delegate.
* This will replace the current handler.
* @param delegate An implementation of {@link MediaSession.Delegate}.
*/
@AnyThread
public void setMediaSessionDelegate(
final @Nullable MediaSession.Delegate delegate) {
Log.d(LOGTAG, "setMediaSessionDelegate " + mWindow);
mMediaSessionHandler.setDelegate(delegate, this);
}
/**
* Get the media session delegate.
* @return The current media session delegate.
*/
@AnyThread
public @Nullable MediaSession.Delegate getMediaSessionDelegate() {
return mMediaSessionHandler.getDelegate();
}
@UiThread
/* package */ void attachMediaSessionController(
final MediaSession.Controller controller) {
ThreadUtils.assertOnUiThread();
if (DEBUG) {
Log.d(LOGTAG,
"attachMediaSessionController" +
" isOpen=" + isOpen() +
", isEnabled=" + mMediaSessionHandler.isEnabled());
}
if (!isOpen() || !mMediaSessionHandler.isEnabled()) {
return;
}
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
mWindow.attachMediaSessionController(controller, controller.getId());
} else {
GeckoThread.queueNativeCallUntil(
GeckoThread.State.PROFILE_READY,
mWindow, "attachMediaSessionController",
MediaSession.Controller.class,
controller,
controller.getId());
}
}
@UiThread
/* package */ void detachMediaSessionController(
final MediaSession.Controller controller) {
ThreadUtils.assertOnUiThread();
if (DEBUG) {
Log.d(LOGTAG,
"detachMediaSessionController" +
" isOpen=" + isOpen() +
", isEnabled=" + mMediaSessionHandler.isEnabled());
}
if (!isOpen() || !mMediaSessionHandler.isEnabled()) {
return;
}
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
mWindow.detachMediaSessionController(controller);
} else {
GeckoThread.queueNativeCallUntil(
GeckoThread.State.PROFILE_READY,
mWindow, "detachMediaSessionController",
MediaSession.Controller.class,
controller);
}
}
/**
* Get the current selection action delegate for this GeckoSession.

Просмотреть файл

@ -0,0 +1,746 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* 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 http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import android.support.annotation.AnyThread;
import android.support.annotation.LongDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.Log;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;
/**
* The MediaSession API provides media controls and events for a GeckoSession.
* This includes support for the DOM Media Session API and regular HTML media
* content.
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaSession">Media Session API</a>
*/
@UiThread
public class MediaSession {
private static final String LOGTAG = "MediaSession";
private static final boolean DEBUG = false;
private final GeckoSession mSession;
private Controller mController;
private static final String ATTACHED_EVENT =
"GeckoView:MediaSession:Attached";
private boolean mControllerAttached;
protected MediaSession(final GeckoSession session) {
mSession = session;
}
/* package */ final class Controller extends JNIObject {
private final long mId;
/* package */ Controller(final long id) {
mId = id;
}
public long getId() {
return mId;
}
@Override // JNIObject
public void disposeNative() {
// Dispose in native code.
throw new UnsupportedOperationException();
}
@WrapForJNI(calledFrom = "ui")
/* package */ void onAttached() {
MediaSession.this.onControllerAttached();
}
@WrapForJNI(dispatchTo = "gecko")
public native void pause();
@WrapForJNI(dispatchTo = "gecko")
public native void stop();
@WrapForJNI(dispatchTo = "gecko")
public native void play();
@WrapForJNI(dispatchTo = "gecko")
public native void skipAd();
@WrapForJNI(dispatchTo = "gecko")
public native void focus();
@WrapForJNI(dispatchTo = "gecko")
public native void seekTo(double time, boolean fast);
@WrapForJNI(dispatchTo = "gecko")
public native void seekForward(double offset);
@WrapForJNI(dispatchTo = "gecko")
public native void seekBackward(double offset);
@WrapForJNI(dispatchTo = "gecko")
public native void nextTrack();
@WrapForJNI(dispatchTo = "gecko")
public native void previousTrack();
@WrapForJNI(dispatchTo = "gecko")
public native void muteAudio(boolean mute);
}
/* package */ Controller getController() {
return mController;
}
/**
* Get whether the media session is active.
* Only active media sessions can be controlled.
* Inactive media session may receive state events since some state events
* may be dispatched before the media session becomes active.
*
* Changes in the active state are notified via {@link Delegate#onActivated}
* and {@link Delegate#onDeactivated} respectively.
*
* @see MediaSession.Delegate#onActivated
* @see MediaSession.Delegate#onDeactivated
*
* @return True if this media session is active, false otherwise.
*/
public boolean isActive() {
return mControllerAttached;
}
/* package */ void attachController(final long id) {
mController = new Controller(id);
mSession.attachMediaSessionController(mController);
}
void onControllerAttached() {
mControllerAttached = true;
// TODO: Remove temp workaround once we move to webidl (bug 1658937).
mSession.getEventDispatcher().dispatch(ATTACHED_EVENT, null);
}
/* package */ void detachController() {
if (mControllerAttached) {
return;
}
mSession.detachMediaSessionController(mController);
mControllerAttached = false;
mController = null;
}
/**
* Pause playback for the media session.
*/
public void pause() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "pause");
}
mController.pause();
}
/**
* Stop playback for the media session.
*/
public void stop() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "stop");
}
mController.stop();
}
/**
* Start playback for the media session.
*/
public void play() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "play");
}
mController.play();
}
/**
* Seek to a specific time.
* Prefer using fast seeking when calling this in a sequence.
* Don't use fast seeking for the last or only call in a sequence.
*
* @param time The time in seconds to move the playback time to.
* @param fast Whether fast seeking should be used.
*/
public void seekTo(final double time, final boolean fast) {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "seekTo: time=" + time + ", fast=" + fast);
}
mController.seekTo(time, fast);
}
/**
* Seek forward by a sensible number of seconds.
*/
public void seekForward() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "seekForward");
}
mController.seekForward(0.0);
}
/**
* Seek backward by a sensible number of seconds.
*/
public void seekBackward() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "seekBackward");
}
mController.seekBackward(0.0);
}
/**
* Select and play the next track.
* Move playback to the next item in the playlist when supported.
*/
public void nextTrack() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "nextTrack");
}
mController.nextTrack();
}
/**
* Select and play the previous track.
* Move playback to the previous item in the playlist when supported.
*/
public void previousTrack() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "previousTrack");
}
mController.previousTrack();
}
/**
* Skip the advertisement that is currently playing.
*/
public void skipAd() {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "skipAd");
}
mController.skipAd();
}
/**
* Set whether audio should be muted.
* Muting audio is supported by default and does not require the media
* session to be active.
*
* @param mute True if audio for this media session should be muted.
*/
public void muteAudio(final boolean mute) {
if (!mControllerAttached) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "muteAudio=" + mute);
}
mController.muteAudio(mute);
}
// TODO: Not sure if we want it.
// public void focus() {}
/**
* Implement this delegate to receive media session events.
*/
@UiThread
public interface Delegate {
/**
* Notify that the given media session has become active.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
*/
default void onActivated(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession) {}
/**
* Notify that the given media session has become inactive.
* Inactive media sessions can not be controlled.
*
* TODO: Add settings links to control behavior.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
*/
default void onDeactivated(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession) {}
/**
* Notify on updated metadata.
* Metadata may be provided by content via the DOM API or by GeckoView
* when not availble.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
* @param meta The updated metadata.
*/
default void onMetadata(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession,
@NonNull Metadata meta) {}
/**
* Notify on updated supported features.
* Unsupported actions will have no effect.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
* @param features A combination of {@link Feature}.
*/
default void onFeatures(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession,
@MSFeature long features) {}
/**
* Notify that playback has started for the given media session.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
*/
default void onPlay(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession) {}
/**
* Notify that playback has paused for the given media session.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
*/
default void onPause(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession) {}
/**
* Notify that playback has stopped for the given media session.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
*/
default void onStop(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession) {}
/**
* Notify on updated position state.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
* @param state An instance of {@link PositionState}.
*/
default void onPositionState(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession,
@NonNull PositionState state) {}
/**
* Notify on changed fullscreen state.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
* @param enabled True when this media session in in fullscreen mode.
*/
default void onFullscreen(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession,
boolean enabled) {}
/**
* Notify on changed picture-in-picture mode state.
*
* @param session The associated GeckoSession.
* @param mediaSession The media session for the given GeckoSession.
* @param enabled True when this media session in in picture-in-picture
* mode.
*/
default void onPictureInPicture(
@NonNull GeckoSession session,
@NonNull MediaSession mediaSession,
boolean enabled) {}
}
/**
* The representation of a media session's metadata.
*/
public static class Metadata {
/**
* The media title.
* May be backfilled based on the document's title.
* May be null or empty.
*/
public final @Nullable String title;
/**
* The media artist name.
* May be null or empty.
*/
public final @Nullable String artist;
/**
* The media album title.
* May be null or empty.
*/
public final @Nullable String album;
/**
* Metadata constructor.
*
* @param title The media title string.
* @param artist The media artist string.
* @param album The media album string.
*/
protected Metadata(
final @Nullable String title,
final @Nullable String artist,
final @Nullable String album) {
this.title = title;
this.artist = artist;
this.album = album;
}
@AnyThread
/* package */ static final class Builder {
private final GeckoBundle mBundle;
public Builder(final GeckoBundle bundle) {
mBundle = new GeckoBundle(bundle);
}
public Builder(final Metadata meta) {
mBundle = meta.toBundle();
}
@NonNull Builder title(final @Nullable String title) {
mBundle.putString("title", title);
return this;
}
@NonNull Builder artist(final @Nullable String artist) {
mBundle.putString("artist", artist);
return this;
}
@NonNull Builder album(final @Nullable String album) {
mBundle.putString("album", album);
return this;
}
}
/* package */ static @NonNull Metadata fromBundle(
final GeckoBundle bundle) {
return new Metadata(
bundle.getString("title"),
bundle.getString("artist"),
bundle.getString("album"));
}
/* package */ @NonNull GeckoBundle toBundle() {
final GeckoBundle bundle = new GeckoBundle(3);
bundle.putString("title", title);
bundle.putString("artist", artist);
bundle.putString("album", album);
return bundle;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder("Metadata {");
builder
.append(", title=").append(title)
.append(", artist=").append(artist)
.append(", album=").append(album)
.append("}");
return builder.toString();
}
}
/**
* Holds the details of the media session's playback state.
*/
public static class PositionState {
/**
* The duration of the media in seconds.
*/
public final double duration;
/**
* The last reported media playback position in seconds.
*/
public final double position;
/**
* The media playback rate coefficient.
* The rate is positive for forward and negative for backward playback.
*/
public final double playbackRate;
/**
* PositionState constructor.
*
* @param duration The media duration in seconds.
* @param position The current media playback position in seconds.
* @param playbackRate The playback rate coefficient.
*/
protected PositionState(
final double duration,
final double position,
final double playbackRate) {
this.duration = duration;
this.position = position;
this.playbackRate = playbackRate;
}
/* package */ static @NonNull PositionState fromBundle(
final GeckoBundle bundle) {
return new PositionState(
bundle.getDouble("duration"),
bundle.getDouble("position"),
bundle.getDouble("playbackRate"));
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder("PositionState {");
builder
.append("duration=").append(duration)
.append(", position=").append(position)
.append(", playbackRate=").append(playbackRate)
.append("}");
return builder.toString();
}
}
@Retention(RetentionPolicy.SOURCE)
@LongDef(flag = true,
value = {
Feature.NONE, Feature.PLAY, Feature.PAUSE, Feature.STOP,
Feature.SEEK_TO, Feature.SEEK_FORWARD, Feature.SEEK_BACKWARD,
Feature.SKIP_AD, Feature.NEXT_TRACK, Feature.PREVIOUS_TRACK,
//Feature.SET_VIDEO_SURFACE,
Feature.FOCUS })
/* package */ @interface MSFeature {}
/**
* Flags for supported media session features.
*/
public static class Feature {
public static final long NONE = 0;
/**
* Playback supported.
*/
public static final long PLAY = 1 << 0;
/**
* Pausing supported.
*/
public static final long PAUSE = 1 << 1;
/**
* Stopping supported.
*/
public static final long STOP = 1 << 2;
/**
* Absolute seeking supported.
*/
public static final long SEEK_TO = 1 << 3;
/**
* Relative seeking supported (forward).
*/
public static final long SEEK_FORWARD = 1 << 4;
/**
* Relative seeking supported (backward).
*/
public static final long SEEK_BACKWARD = 1 << 5;
/**
* Skipping advertisements supported.
*/
public static final long SKIP_AD = 1 << 6;
/**
* Next track selection supported.
*/
public static final long NEXT_TRACK = 1 << 7;
/**
* Previous track selection supported.
*/
public static final long PREVIOUS_TRACK = 1 << 8;
/**
* Focusing supported.
*/
public static final long FOCUS = 1 << 9;
// /**
// * Custom video surface supported.
// */
// public static final long SET_VIDEO_SURFACE = 1 << 10;
/* package */ static long fromBundle(final GeckoBundle bundle) {
// Sync with MediaController.webidl.
final long features =
NONE |
(bundle.getBoolean("play") ? PLAY : NONE) |
(bundle.getBoolean("pause") ? PAUSE : NONE) |
(bundle.getBoolean("stop") ? STOP : NONE) |
(bundle.getBoolean("seekto") ? SEEK_TO : NONE) |
(bundle.getBoolean("seekforward") ? SEEK_FORWARD : NONE) |
(bundle.getBoolean("seekbackward") ? SEEK_BACKWARD : NONE) |
(bundle.getBoolean("nexttrack") ? NEXT_TRACK : NONE) |
(bundle.getBoolean("previoustrack") ? PREVIOUS_TRACK : NONE) |
(bundle.getBoolean("skipad") ? SKIP_AD : NONE) |
(bundle.getBoolean("focus") ? FOCUS : NONE);
return features;
}
}
private static final String ACTIVATED_EVENT =
"GeckoView:MediaSession:Activated";
private static final String DEACTIVATED_EVENT =
"GeckoView:MediaSession:Deactivated";
private static final String METADATA_EVENT =
"GeckoView:MediaSession:Metadata";
private static final String POSITION_STATE_EVENT =
"GeckoView:MediaSession:PositionState";
private static final String FEATURES_EVENT =
"GeckoView:MediaSession:Features";
private static final String FULLSCREEN_EVENT =
"GeckoView:MediaSession:Fullscreen";
private static final String PICTURE_IN_PICTURE_EVENT =
"GeckoView:MediaSession:PictureInPicture";
private static final String PLAYBACK_NONE_EVENT =
"GeckoView:MediaSession:Playback:None";
private static final String PLAYBACK_PAUSED_EVENT =
"GeckoView:MediaSession:Playback:Paused";
private static final String PLAYBACK_PLAYING_EVENT =
"GeckoView:MediaSession:Playback:Playing";
/* package */ static class Handler
extends GeckoSessionHandler<MediaSession.Delegate> {
private final GeckoSession mSession;
private final MediaSession mMediaSession;
public Handler(final GeckoSession session) {
super(
"GeckoViewMediaControl",
session,
new String[]{
ATTACHED_EVENT,
ACTIVATED_EVENT,
DEACTIVATED_EVENT,
METADATA_EVENT,
FULLSCREEN_EVENT,
PICTURE_IN_PICTURE_EVENT,
POSITION_STATE_EVENT,
PLAYBACK_NONE_EVENT,
PLAYBACK_PAUSED_EVENT,
PLAYBACK_PLAYING_EVENT,
FEATURES_EVENT,
});
mSession = session;
mMediaSession = new MediaSession(session);
}
@Override
public void handleMessage(
final Delegate delegate,
final String event,
final GeckoBundle message,
final EventCallback callback) {
if (DEBUG) {
Log.d(LOGTAG, "handleMessage " + event);
}
if (ATTACHED_EVENT.equals(event)) {
delegate.onActivated(mSession, mMediaSession);
} else if (ACTIVATED_EVENT.equals(event)) {
mMediaSession.attachController(message.getLong("id"));
// TODO: We can call this direclty, once we move to webidl.
// delegate.onActivated(mSession, mMediaSession);
} else if (DEACTIVATED_EVENT.equals(event)) {
mMediaSession.detachController();
delegate.onDeactivated(mSession, mMediaSession);
} else if (METADATA_EVENT.equals(event)) {
final Metadata meta = Metadata.fromBundle(message);
delegate.onMetadata(mSession, mMediaSession, meta);
} else if (POSITION_STATE_EVENT.equals(event)) {
final PositionState state =
PositionState.fromBundle(message.getBundle("state"));
delegate.onPositionState(mSession, mMediaSession, state);
} else if (PLAYBACK_NONE_EVENT.equals(event)) {
delegate.onStop(mSession, mMediaSession);
} else if (PLAYBACK_PAUSED_EVENT.equals(event)) {
delegate.onPause(mSession, mMediaSession);
} else if (PLAYBACK_PLAYING_EVENT.equals(event)) {
delegate.onPlay(mSession, mMediaSession);
} else if (FEATURES_EVENT.equals(event)) {
final long features = Feature.fromBundle(
message.getBundle("features"));
delegate.onFeatures(mSession, mMediaSession, features);
} else if (FULLSCREEN_EVENT.equals(event)) {
final boolean enabled = message.getBoolean("enabled");
delegate.onFullscreen(mSession, mMediaSession, enabled);
} else if (PICTURE_IN_PICTURE_EVENT.equals(event)) {
final boolean enabled = message.getBoolean("enabled");
delegate.onPictureInPicture(mSession, mMediaSession, enabled);
}
}
}
}

Просмотреть файл

@ -0,0 +1,127 @@
/* 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 http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["GeckoViewMediaControl"];
const { GeckoViewModule } = ChromeUtils.import(
"resource://gre/modules/GeckoViewModule.jsm"
);
class GeckoViewMediaControl extends GeckoViewModule {
onInit() {
debug`onInit`;
}
onEnable() {
debug`onEnable`;
if (this.controller.isActive) {
this.handleActivated();
}
const options = {
mozSystemGroup: true,
capture: false,
};
this.controller.addEventListener("activated", this, options);
this.controller.addEventListener("deactivated", this, options);
this.controller.addEventListener("supportedkeyschange", this, options);
this.controller.addEventListener("positionstatechange", this, options);
// TODO: Move other events to webidl once supported.
}
onDisable() {
debug`onDisable`;
this.controller.removeEventListener("activated", this);
this.controller.removeEventListener("deactivated", this);
this.controller.removeEventListener("supportedkeyschange", this);
this.controller.removeEventListener("positionstatechange", this);
}
get controller() {
return this.browser.browsingContext.mediaController;
}
// eslint-disable-next-line complexity
handleEvent(aEvent) {
debug`handleEvent: ${aEvent.type}`;
switch (aEvent.type) {
case "activated":
this.handleActivated();
break;
case "deactivated":
this.handleDeactivated();
break;
case "supportedkeyschange":
this.handleSupportedKeysChanged();
break;
case "positionstatechange":
this.handlePositionStateChanged(aEvent);
break;
default:
warn`Unknown event type ${aEvent.type}`;
break;
}
}
handleActivated() {
debug`handleActivated`;
this.eventDispatcher.sendRequest({
type: "GeckoView:MediaSession:Activated",
id: this.controller.id,
});
}
handleDeactivated() {
debug`handleDeactivated`;
this.eventDispatcher.sendRequest({
type: "GeckoView:MediaSession:Deactivated",
id: this.controller.id,
});
}
handlePositionStateChanged(aEvent) {
debug`handlePositionStateChanged`;
this.eventDispatcher.sendRequest({
type: "GeckoView:MediaSession:PositionState",
id: this.controller.id,
state: {
duration: aEvent.duration,
playbackRate: aEvent.playbackRate,
position: aEvent.position,
}
});
}
handleSupportedKeysChanged() {
const supported = this.controller.supportedKeys;
debug`handleSupportedKeysChanged ${supported}`;
// Mapping it to a key-value store for compatibility with the JNI
// implementation for now.
const features = new Map();
supported.forEach(key => {
features[key] = true;
});
this.eventDispatcher.sendRequest({
type: "GeckoView:MediaSession:Features",
id: this.controller.id,
features,
});
}
}
const { debug, warn } = GeckoViewMediaControl.initLogging(
"GeckoViewMediaControl"
);

Просмотреть файл

@ -19,6 +19,7 @@ EXTRA_JS_MODULES += [
'GeckoViewContentBlocking.jsm',
'GeckoViewContentBlockingController.jsm',
'GeckoViewMedia.jsm',
'GeckoViewMediaControl.jsm',
'GeckoViewModule.jsm',
'GeckoViewNavigation.jsm',
'GeckoViewProcessHangMonitor.jsm',

Просмотреть файл

@ -8,7 +8,8 @@ namespace mozilla {
namespace widget {
mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() {
// TODO : will implement this in bug 1601510.
// GeckoView uses MediaController.webidl for media session events and control,
// see bug 1623715.
return nullptr;
}

Просмотреть файл

@ -62,6 +62,7 @@ classes_with_WrapForJNI = [
'HardwareCodecCapabilityUtils',
'ImageDecoder',
'MediaDrmProxy',
'MediaSession',
'PanZoomController',
'PrefsHelper',
'RuntimeTelemetry',

Просмотреть файл

@ -26,8 +26,11 @@
#include "mozilla/Preferences.h"
#include "mozilla/Unused.h"
#include "mozilla/a11y/SessionAccessibility.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/MediaControlService.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/DataSurfaceHelpers.h"
@ -89,6 +92,7 @@ using mozilla::dom::ContentParent;
#include "mozilla/java/GeckoResultWrappers.h"
#include "mozilla/java/GeckoSessionNatives.h"
#include "mozilla/java/GeckoSystemStateListenerWrappers.h"
#include "mozilla/java/MediaSessionNatives.h"
#include "mozilla/java/PanZoomControllerNatives.h"
#include "mozilla/java/SessionAccessibilityWrappers.h"
#include "ScreenHelperAndroid.h"
@ -363,8 +367,376 @@ class nsWindow::GeckoViewSupport final
int32_t aFlags, mozilla::jni::String::Param aTriggeringUri,
bool aHasUserGesture, bool aIsTopLevel) const
-> java::GeckoResult::LocalRef;
void AttachMediaSessionController(const GeckoSession::Window::LocalRef& inst,
jni::Object::Param aController,
const int64_t aId);
void DetachMediaSessionController(const GeckoSession::Window::LocalRef& inst,
jni::Object::Param aController);
};
class nsWindow::MediaSessionSupport final
: public mozilla::java::MediaSession::Controller::Natives<
MediaSessionSupport> {
using LockedWindowPtr = WindowPtr<MediaSessionSupport>::Locked;
using MediaKeysArray = nsTArray<MediaControlKey>;
typedef RefPtr<mozilla::dom::MediaController> ControllerPtr;
WindowPtr<MediaSessionSupport> mWindow;
mozilla::java::MediaSession::Controller::WeakRef mJavaController;
ControllerPtr mMediaController;
MediaEventListener mMetadataChangedListener;
MediaEventListener mPlaybackChangedListener;
MediaEventListener mFullscreenChangedListener;
public:
typedef java::MediaSession::Controller::Natives<MediaSessionSupport> Base;
using Base::AttachNative;
using Base::DisposeNative;
MediaSessionSupport(
NativePtr<MediaSessionSupport>* aPtr, nsWindow* aWindow,
const java::MediaSession::Controller::LocalRef& aController)
: mWindow(aPtr, aWindow),
mJavaController(aController),
mMediaController(nullptr) {
MOZ_ASSERT(mWindow);
}
bool Dispatch(const char16_t aType[],
java::GeckoBundle::Param aBundle = nullptr) {
widget::EventDispatcher* dispatcher = mWindow->GetEventDispatcher();
if (!dispatcher) {
return false;
}
dispatcher->Dispatch(aType, aBundle);
return true;
}
void PipChanged(const bool aEnabled) {
const size_t kBundleSize = 1;
AutoTArray<jni::String::LocalRef, kBundleSize> keys;
AutoTArray<jni::Object::LocalRef, kBundleSize> values;
keys.AppendElement(
jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("enabled")));
values.AppendElement(aEnabled ? java::sdk::Boolean::TRUE()
: java::sdk::Boolean::FALSE());
MOZ_ASSERT(kBundleSize == keys.Length());
MOZ_ASSERT(kBundleSize == values.Length());
auto bundleKeys = jni::ObjectArray::New<jni::String>(kBundleSize);
auto bundleValues = jni::ObjectArray::New<jni::Object>(kBundleSize);
for (size_t i = 0; i < kBundleSize; ++i) {
bundleKeys->SetElement(i, keys[i]);
bundleValues->SetElement(i, values[i]);
}
auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
const char16_t kPictureInPicture[] =
u"GeckoView:MediaSession:PictureInPicture";
Dispatch(kPictureInPicture, bundle);
}
void MetadataChanged(const dom::MediaMetadataBase& aMetadata) {
MOZ_ASSERT(NS_IsMainThread());
const size_t kBundleSize = 4;
AutoTArray<jni::String::LocalRef, kBundleSize> keys;
AutoTArray<jni::Object::LocalRef, kBundleSize> values;
keys.AppendElement(
jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("title")));
values.AppendElement(jni::StringParam(aMetadata.mTitle));
keys.AppendElement(
jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("artist")));
values.AppendElement(jni::StringParam(aMetadata.mArtist));
keys.AppendElement(
jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("album")));
values.AppendElement(jni::StringParam(aMetadata.mAlbum));
auto images =
jni::ObjectArray::New<java::GeckoBundle>(aMetadata.mArtwork.Length());
for (size_t i = 0; i < aMetadata.mArtwork.Length(); ++i) {
const auto& image = aMetadata.mArtwork[i];
const size_t kImageBundleSize = 3;
auto imageKeys = jni::ObjectArray::New<jni::String>(kImageBundleSize);
auto imageValues = jni::ObjectArray::New<jni::String>(kImageBundleSize);
imageKeys->SetElement(
0, jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("src")));
imageValues->SetElement(0, jni::StringParam(image.mSrc));
imageKeys->SetElement(
1, jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("type")));
imageValues->SetElement(1, jni::StringParam(image.mType));
imageKeys->SetElement(
2, jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("sizes")));
imageValues->SetElement(2, jni::StringParam(image.mSizes));
images->SetElement(i, java::GeckoBundle::New(imageKeys, imageValues));
}
keys.AppendElement(
jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("artwork")));
values.AppendElement(images);
MOZ_ASSERT(kBundleSize == keys.Length());
MOZ_ASSERT(kBundleSize == values.Length());
auto bundleKeys = jni::ObjectArray::New<jni::String>(kBundleSize);
auto bundleValues = jni::ObjectArray::New<jni::Object>(kBundleSize);
for (size_t i = 0; i < kBundleSize; ++i) {
bundleKeys->SetElement(i, keys[i]);
bundleValues->SetElement(i, values[i]);
}
auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
const char16_t kMetadata[] = u"GeckoView:MediaSession:Metadata";
Dispatch(kMetadata, bundle);
}
void PlaybackChanged(const MediaSessionPlaybackState& aState) {
MOZ_ASSERT(NS_IsMainThread());
const char16_t kPlaybackNone[] = u"GeckoView:MediaSession:Playback:None";
const char16_t kPlaybackPaused[] =
u"GeckoView:MediaSession:Playback:Paused";
const char16_t kPlaybackPlaying[] =
u"GeckoView:MediaSession:Playback:Playing";
switch (aState) {
case MediaSessionPlaybackState::None:
Dispatch(kPlaybackNone);
break;
case MediaSessionPlaybackState::Paused:
Dispatch(kPlaybackPaused);
break;
case MediaSessionPlaybackState::Playing:
Dispatch(kPlaybackPlaying);
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid MediaSessionPlaybackState");
break;
}
}
void FullscreenChanged(bool aIsEnabled) {
MOZ_ASSERT(NS_IsMainThread());
const size_t kBundleSize = 1;
AutoTArray<jni::String::LocalRef, kBundleSize> keys;
AutoTArray<jni::Object::LocalRef, kBundleSize> values;
keys.AppendElement(
jni::StringParam(NS_LITERAL_STRING_FROM_CSTRING("enabled")));
values.AppendElement(aIsEnabled ? java::sdk::Boolean::TRUE()
: java::sdk::Boolean::FALSE());
MOZ_ASSERT(kBundleSize == keys.Length());
MOZ_ASSERT(kBundleSize == values.Length());
auto bundleKeys = jni::ObjectArray::New<jni::String>(kBundleSize);
auto bundleValues = jni::ObjectArray::New<jni::Object>(kBundleSize);
for (size_t i = 0; i < kBundleSize; ++i) {
bundleKeys->SetElement(i, keys[i]);
bundleValues->SetElement(i, values[i]);
}
auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
const char16_t kFullscreen[] = u"GeckoView:MediaSession:Fullscreen";
Dispatch(kFullscreen, bundle);
}
void OnDetach(already_AddRefed<Runnable> aDisposer) {
MOZ_ASSERT(NS_IsMainThread());
SetNativeController(nullptr);
}
const java::MediaSession::Controller::Ref& GetJavaController() const {
return mJavaController;
}
void SetNativeController(mozilla::dom::MediaController* aController) {
MOZ_ASSERT(NS_IsMainThread());
if (mMediaController == aController) {
return;
}
MOZ_ASSERT(!mMediaController || !aController);
if (mMediaController) {
UnregisterControllerListeners();
}
mMediaController = aController;
if (mMediaController) {
MetadataChanged(mMediaController->GetCurrentMediaMetadata());
PlaybackChanged(mMediaController->GetState());
RegisterControllerListeners();
}
}
void RegisterControllerListeners() {
mMetadataChangedListener = mMediaController->MetadataChangedEvent().Connect(
AbstractThread::MainThread(), this,
&MediaSessionSupport::MetadataChanged);
mPlaybackChangedListener = mMediaController->PlaybackChangedEvent().Connect(
AbstractThread::MainThread(), this,
&MediaSessionSupport::PlaybackChanged);
mFullscreenChangedListener =
mMediaController->FullScreenChangedEvent().Connect(
AbstractThread::MainThread(), this,
&MediaSessionSupport::FullscreenChanged);
}
void UnregisterControllerListeners() {
mMetadataChangedListener.DisconnectIfExists();
mPlaybackChangedListener.DisconnectIfExists();
mFullscreenChangedListener.DisconnectIfExists();
}
bool IsActive() const {
MOZ_ASSERT(NS_IsMainThread());
return mMediaController && mMediaController->IsActive();
}
void Pause() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->Pause();
}
void Stop() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->Stop();
}
void Play() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->Play();
}
void Focus() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->Focus();
}
void NextTrack() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->NextTrack();
}
void PreviousTrack() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->PrevTrack();
}
void SeekTo(double aTime, bool aFast) {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->SeekTo(aTime, aFast);
}
void SeekForward(double aOffset) {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->SeekForward();
}
void SeekBackward(double aOffset) {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->SeekBackward();
}
void SkipAd() {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
mMediaController->SkipAd();
}
void MuteAudio(bool aMute) {
MOZ_ASSERT(NS_IsMainThread());
if (!IsActive()) {
return;
}
RefPtr<dom::BrowsingContext> bc =
dom::BrowsingContext::Get(mMediaController->Id());
if (!bc) {
return;
}
Unused << bc->SetMuted(aMute);
}
};
template <>
const char nsWindow::NativePtr<nsWindow::MediaSessionSupport>::sName[] =
"MediaSessionSupport";
/**
* PanZoomController handles its native calls on the UI thread, so make
* it separate from GeckoViewSupport.
@ -1495,11 +1867,46 @@ void nsWindow::GeckoViewSupport::AttachAccessibility(
sessionAccessibility);
}
void nsWindow::GeckoViewSupport::AttachMediaSessionController(
const GeckoSession::Window::LocalRef& inst, jni::Object::Param aController,
const int64_t aId) {
if (window.mMediaSessionSupport) {
window.mMediaSessionSupport.Detach(
window.mMediaSessionSupport->GetJavaController());
}
auto controller = java::MediaSession::Controller::LocalRef(
jni::GetGeckoThreadEnv(),
java::MediaSession::Controller::Ref::From(aController));
window.mMediaSessionSupport.Attach(controller, &window, controller);
RefPtr<BrowsingContext> bc = BrowsingContext::Get(aId);
RefPtr<dom::MediaController> nativeController =
bc->Canonical()->GetMediaController();
MOZ_ASSERT(nativeController && nativeController->Id() == aId);
window.mMediaSessionSupport->SetNativeController(nativeController);
DispatchToUiThread("GeckoViewSupport::AttachMediaSessionController",
[controller = java::MediaSession::Controller::GlobalRef(
controller)] { controller->OnAttached(); });
}
void nsWindow::GeckoViewSupport::DetachMediaSessionController(
const GeckoSession::Window::LocalRef& inst,
jni::Object::Param aController) {
if (window.mMediaSessionSupport) {
window.mMediaSessionSupport.Detach(
window.mMediaSessionSupport->GetJavaController());
}
}
void nsWindow::InitNatives() {
jni::InitConversionStatics();
nsWindow::GeckoViewSupport::Base::Init();
nsWindow::LayerViewSupport::Init();
nsWindow::NPZCSupport::Init();
nsWindow::MediaSessionSupport::Init();
GeckoEditableSupport::Init();
a11y::SessionAccessibility::Init();

Просмотреть файл

@ -192,6 +192,9 @@ class nsWindow final : public nsBaseWidget {
// Strong referenced by the Java instance.
NativePtr<mozilla::a11y::SessionAccessibility> mSessionAccessibility;
class MediaSessionSupport;
NativePtr<MediaSessionSupport> mMediaSessionSupport;
class GeckoViewSupport;
// Object that implements native GeckoView calls and associated states.
// nullptr for nsWindows that were not opened from GeckoView.