/* -*- 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 android.util.Log; import androidx.annotation.AnyThread; import androidx.annotation.LongDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.GeckoBundle; import org.mozilla.gecko.util.ImageResource; /** * 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 Media Session * API */ @UiThread public class MediaSession { private static final String LOGTAG = "MediaSession"; private static final boolean DEBUG = false; private final GeckoSession mSession; private boolean mIsActive; protected MediaSession(final GeckoSession session) { mSession = session; } /** * Get whether the media session is active. Only active media sessions can be controlled. * *
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 mIsActive; } /* package */ void setActive(final boolean active) { mIsActive = active; } /** Pause playback for the media session. */ public void pause() { if (DEBUG) { Log.d(LOGTAG, "pause"); } mSession.getEventDispatcher().dispatch(PAUSE_EVENT, null); } /** Stop playback for the media session. */ public void stop() { if (DEBUG) { Log.d(LOGTAG, "stop"); } mSession.getEventDispatcher().dispatch(STOP_EVENT, null); } /** Start playback for the media session. */ public void play() { if (DEBUG) { Log.d(LOGTAG, "play"); } mSession.getEventDispatcher().dispatch(PLAY_EVENT, null); } /** * 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 (DEBUG) { Log.d(LOGTAG, "seekTo: time=" + time + ", fast=" + fast); } final GeckoBundle bundle = new GeckoBundle(2); bundle.putDouble("time", time); bundle.putBoolean("fast", fast); mSession.getEventDispatcher().dispatch(SEEK_TO_EVENT, bundle); } /** Seek forward by a sensible number of seconds. */ public void seekForward() { if (DEBUG) { Log.d(LOGTAG, "seekForward"); } final GeckoBundle bundle = new GeckoBundle(1); bundle.putDouble("offset", 0.0); mSession.getEventDispatcher().dispatch(SEEK_FORWARD_EVENT, bundle); } /** Seek backward by a sensible number of seconds. */ public void seekBackward() { if (DEBUG) { Log.d(LOGTAG, "seekBackward"); } final GeckoBundle bundle = new GeckoBundle(1); bundle.putDouble("offset", 0.0); mSession.getEventDispatcher().dispatch(SEEK_BACKWARD_EVENT, bundle); } /** * Select and play the next track. Move playback to the next item in the playlist when supported. */ public void nextTrack() { if (DEBUG) { Log.d(LOGTAG, "nextTrack"); } mSession.getEventDispatcher().dispatch(NEXT_TRACK_EVENT, null); } /** * Select and play the previous track. Move playback to the previous item in the playlist when * supported. */ public void previousTrack() { if (DEBUG) { Log.d(LOGTAG, "previousTrack"); } mSession.getEventDispatcher().dispatch(PREV_TRACK_EVENT, null); } /** Skip the advertisement that is currently playing. */ public void skipAd() { if (DEBUG) { Log.d(LOGTAG, "skipAd"); } mSession.getEventDispatcher().dispatch(SKIP_AD_EVENT, null); } /** * 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 (DEBUG) { Log.d(LOGTAG, "muteAudio=" + mute); } final GeckoBundle bundle = new GeckoBundle(1); bundle.putBoolean("mute", mute); mSession.getEventDispatcher().dispatch(MUTE_AUDIO_EVENT, bundle); } /** Implement this delegate to receive media session events. */ @UiThread public interface Delegate { /** * Notify that the given media session has become active. It is always the first event * dispatched for a new or previously deactivated media session. * * @param session The associated GeckoSession. * @param mediaSession The media session for the given GeckoSession. */ default void onActivated( @NonNull final GeckoSession session, @NonNull final 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 final GeckoSession session, @NonNull final 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 final GeckoSession session,
@NonNull final MediaSession mediaSession,
@NonNull final 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 final GeckoSession session,
@NonNull final MediaSession mediaSession,
@MSFeature final 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 final GeckoSession session, @NonNull final 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 final GeckoSession session, @NonNull final 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 final GeckoSession session, @NonNull final 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 final GeckoSession session,
@NonNull final MediaSession mediaSession,
@NonNull final 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.
* @param meta An instance of {@link ElementMetadata}, if enabled.
*/
default void onFullscreen(
@NonNull final GeckoSession session,
@NonNull final MediaSession mediaSession,
final boolean enabled,
@Nullable final ElementMetadata meta) {}
}
/** The representation of a media element's metadata. */
public static class ElementMetadata {
/** The media source URI. */
public final @Nullable String source;
/** The duration of the media in seconds. 0.0 if unknown. */
public final double duration;
/** The width of the video in device pixels. 0 if unknown. */
public final long width;
/** The height of the video in device pixels. 0 if unknown. */
public final long height;
/** The number of audio tracks contained in this element. */
public final int audioTrackCount;
/** The number of video tracks contained in this element. */
public final int videoTrackCount;
/**
* ElementMetadata constructor.
*
* @param source The media URI.
* @param duration The media duration in seconds.
* @param width The video width in device pixels.
* @param height The video height in device pixels.
* @param audioTrackCount The audio track count.
* @param videoTrackCount The video track count.
*/
public ElementMetadata(
@Nullable final String source,
final double duration,
final long width,
final long height,
final int audioTrackCount,
final int videoTrackCount) {
this.source = source;
this.duration = duration;
this.width = width;
this.height = height;
this.audioTrackCount = audioTrackCount;
this.videoTrackCount = videoTrackCount;
}
/* package */ static @NonNull ElementMetadata fromBundle(final GeckoBundle bundle) {
// Sync with MediaUtils.sys.mjs.
return new ElementMetadata(
bundle.getString("src"),
bundle.getDouble("duration", 0.0),
bundle.getLong("width", 0),
bundle.getLong("height", 0),
bundle.getInt("audioTrackCount", 0),
bundle.getInt("videoTrackCount", 0));
}
}
/** 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;
/** The media artwork image. May be null. */
public final @Nullable Image artwork;
/**
* Metadata constructor.
*
* @param title The media title string.
* @param artist The media artist string.
* @param album The media album string.
* @param artwork The media artwork {@link Image}.
*/
protected Metadata(
final @Nullable String title,
final @Nullable String artist,
final @Nullable String album,
final @Nullable Image artwork) {
this.title = title;
this.artist = artist;
this.album = album;
this.artwork = artwork;
}
@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) {
final GeckoBundle[] artworkBundles = bundle.getBundleArray("artwork");
final ImageResource.Collection.Builder artworkBuilder =
new ImageResource.Collection.Builder();
for (final GeckoBundle artworkBundle : artworkBundles) {
artworkBuilder.add(ImageResource.fromBundle(artworkBundle));
}
return new Metadata(
bundle.getString("title"),
bundle.getString("artist"),
bundle.getString("album"),
new Image(artworkBuilder.build()));
}
/* 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(", artwork=")
.append(artwork)
.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
})
public @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.
return 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);
}
}
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 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";
private static final String PLAY_EVENT = "GeckoView:MediaSession:Play";
private static final String PAUSE_EVENT = "GeckoView:MediaSession:Pause";
private static final String STOP_EVENT = "GeckoView:MediaSession:Stop";
private static final String NEXT_TRACK_EVENT = "GeckoView:MediaSession:NextTrack";
private static final String PREV_TRACK_EVENT = "GeckoView:MediaSession:PrevTrack";
private static final String SEEK_FORWARD_EVENT = "GeckoView:MediaSession:SeekForward";
private static final String SEEK_BACKWARD_EVENT = "GeckoView:MediaSession:SeekBackward";
private static final String SKIP_AD_EVENT = "GeckoView:MediaSession:SkipAd";
private static final String SEEK_TO_EVENT = "GeckoView:MediaSession:SeekTo";
private static final String MUTE_AUDIO_EVENT = "GeckoView:MediaSession:MuteAudio";
/* package */ static class Handler extends GeckoSessionHandler