зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
fdca957200
Коммит
fc486d897e
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче