mirror of
https://github.com/samsonjs/media.git
synced 2026-03-29 10:05:48 +00:00
3129 lines
114 KiB
Java
3129 lines
114 KiB
Java
/*
|
|
* Copyright 2019 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package androidx.media3.session;
|
|
|
|
import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME;
|
|
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
|
|
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
|
|
import static androidx.media3.common.Player.COMMAND_PREPARE;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
|
|
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
|
|
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
|
|
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
|
|
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
|
|
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
|
|
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
|
|
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
|
|
import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
|
|
import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE;
|
|
import static androidx.media3.common.Player.COMMAND_SET_VOLUME;
|
|
import static androidx.media3.common.Player.COMMAND_STOP;
|
|
import static androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL;
|
|
import static androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE;
|
|
import static androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK;
|
|
import static androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_IS_LOADING_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION;
|
|
import static androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_PLAYER_ERROR;
|
|
import static androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY;
|
|
import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_TRACKS_CHANGED;
|
|
import static androidx.media3.common.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED;
|
|
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
|
|
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT;
|
|
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK;
|
|
import static androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
|
import static androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
|
|
import static androidx.media3.common.Player.STATE_BUFFERING;
|
|
import static androidx.media3.common.Player.STATE_ENDED;
|
|
import static androidx.media3.common.Player.STATE_IDLE;
|
|
import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
|
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
|
import static androidx.media3.common.util.Assertions.checkIndex;
|
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
|
import static androidx.media3.common.util.Assertions.checkState;
|
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
|
import static androidx.media3.common.util.Util.usToMs;
|
|
import static androidx.media3.session.MediaUtils.calculateBufferedPercentage;
|
|
import static androidx.media3.session.MediaUtils.intersect;
|
|
import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM;
|
|
import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING;
|
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED;
|
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
|
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
|
|
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
|
|
import static androidx.media3.session.SessionToken.TYPE_SESSION;
|
|
import static java.lang.Math.max;
|
|
import static java.lang.Math.min;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.ServiceConnection;
|
|
import android.graphics.Rect;
|
|
import android.graphics.SurfaceTexture;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.Looper;
|
|
import android.os.Message;
|
|
import android.os.Process;
|
|
import android.os.RemoteException;
|
|
import android.os.SystemClock;
|
|
import android.support.v4.media.MediaBrowserCompat;
|
|
import android.view.Surface;
|
|
import android.view.SurfaceHolder;
|
|
import android.view.SurfaceView;
|
|
import android.view.TextureView;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.media3.common.AdPlaybackState;
|
|
import androidx.media3.common.AudioAttributes;
|
|
import androidx.media3.common.BundleListRetriever;
|
|
import androidx.media3.common.C;
|
|
import androidx.media3.common.DeviceInfo;
|
|
import androidx.media3.common.IllegalSeekPositionException;
|
|
import androidx.media3.common.MediaItem;
|
|
import androidx.media3.common.MediaMetadata;
|
|
import androidx.media3.common.PlaybackException;
|
|
import androidx.media3.common.PlaybackParameters;
|
|
import androidx.media3.common.Player;
|
|
import androidx.media3.common.Player.Commands;
|
|
import androidx.media3.common.Player.DiscontinuityReason;
|
|
import androidx.media3.common.Player.Events;
|
|
import androidx.media3.common.Player.Listener;
|
|
import androidx.media3.common.Player.MediaItemTransitionReason;
|
|
import androidx.media3.common.Player.PlayWhenReadyChangeReason;
|
|
import androidx.media3.common.Player.PositionInfo;
|
|
import androidx.media3.common.Player.RepeatMode;
|
|
import androidx.media3.common.Player.TimelineChangeReason;
|
|
import androidx.media3.common.Rating;
|
|
import androidx.media3.common.Timeline;
|
|
import androidx.media3.common.Timeline.Period;
|
|
import androidx.media3.common.Timeline.RemotableTimeline;
|
|
import androidx.media3.common.Timeline.Window;
|
|
import androidx.media3.common.TrackSelectionParameters;
|
|
import androidx.media3.common.Tracks;
|
|
import androidx.media3.common.VideoSize;
|
|
import androidx.media3.common.text.CueGroup;
|
|
import androidx.media3.common.util.BundleableUtil;
|
|
import androidx.media3.common.util.Clock;
|
|
import androidx.media3.common.util.ListenerSet;
|
|
import androidx.media3.common.util.Log;
|
|
import androidx.media3.common.util.Util;
|
|
import androidx.media3.session.MediaController.MediaControllerImpl;
|
|
import androidx.media3.session.SessionCommand.CommandCode;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.util.concurrent.Futures;
|
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
import com.google.common.util.concurrent.MoreExecutors;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.concurrent.CancellationException;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.TimeoutException;
|
|
import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
|
|
@SuppressWarnings("FutureReturnValueIgnored") // TODO(b/138091975): Not to ignore if feasible
|
|
/* package */ class MediaControllerImplBase implements MediaControllerImpl {
|
|
|
|
public static final String TAG = "MCImplBase";
|
|
|
|
private static final long RELEASE_TIMEOUT_MS = 30_000;
|
|
|
|
private final MediaController instance;
|
|
protected final SequencedFutureManager sequencedFutureManager;
|
|
protected final MediaControllerStub controllerStub;
|
|
|
|
private final Context context;
|
|
private final SessionToken token;
|
|
private final Bundle connectionHints;
|
|
private final IBinder.DeathRecipient deathRecipient;
|
|
private final SurfaceCallback surfaceCallback;
|
|
private final ListenerSet<Listener> listeners;
|
|
private final FlushCommandQueueHandler flushCommandQueueHandler;
|
|
|
|
@Nullable private SessionToken connectedToken;
|
|
@Nullable private SessionServiceConnection serviceConnection;
|
|
private boolean released;
|
|
private PlayerInfo playerInfo;
|
|
@Nullable private PendingIntent sessionActivity;
|
|
private SessionCommands sessionCommands;
|
|
private Commands playerCommandsFromSession;
|
|
private Commands playerCommandsFromPlayer;
|
|
private Commands intersectedPlayerCommands;
|
|
@Nullable private Surface videoSurface;
|
|
@Nullable private SurfaceHolder videoSurfaceHolder;
|
|
@Nullable private TextureView videoTextureView;
|
|
private int surfaceWidth;
|
|
private int surfaceHeight;
|
|
@Nullable private IMediaSession iSession;
|
|
private long lastReturnedContentPositionMs;
|
|
private long lastSetPlayWhenReadyCalledTimeMs;
|
|
|
|
public MediaControllerImplBase(
|
|
Context context,
|
|
@UnderInitialization MediaController instance,
|
|
SessionToken token,
|
|
Bundle connectionHints,
|
|
Looper applicationLooper) {
|
|
// Initialize default values.
|
|
playerInfo = PlayerInfo.DEFAULT;
|
|
sessionCommands = SessionCommands.EMPTY;
|
|
playerCommandsFromSession = Commands.EMPTY;
|
|
playerCommandsFromPlayer = Commands.EMPTY;
|
|
intersectedPlayerCommands = Commands.EMPTY;
|
|
listeners =
|
|
new ListenerSet<>(
|
|
applicationLooper,
|
|
Clock.DEFAULT,
|
|
(listener, flags) -> listener.onEvents(getInstance(), new Events(flags)));
|
|
|
|
// Initialize members
|
|
this.instance = instance;
|
|
checkNotNull(context, "context must not be null");
|
|
checkNotNull(token, "token must not be null");
|
|
this.context = context;
|
|
sequencedFutureManager = new SequencedFutureManager();
|
|
controllerStub = new MediaControllerStub(this);
|
|
this.token = token;
|
|
this.connectionHints = connectionHints;
|
|
deathRecipient =
|
|
() ->
|
|
MediaControllerImplBase.this
|
|
.getInstance()
|
|
.runOnApplicationLooper(MediaControllerImplBase.this.getInstance()::release);
|
|
surfaceCallback = new SurfaceCallback();
|
|
|
|
serviceConnection =
|
|
(this.token.getType() == TYPE_SESSION)
|
|
? null
|
|
: new SessionServiceConnection(connectionHints);
|
|
flushCommandQueueHandler = new FlushCommandQueueHandler(applicationLooper);
|
|
lastReturnedContentPositionMs = C.TIME_UNSET;
|
|
lastSetPlayWhenReadyCalledTimeMs = C.TIME_UNSET;
|
|
}
|
|
|
|
/* package*/ MediaController getInstance() {
|
|
return instance;
|
|
}
|
|
|
|
@Override
|
|
public void connect(@UnderInitialization MediaControllerImplBase this) {
|
|
boolean connectionRequested;
|
|
if (this.token.getType() == TYPE_SESSION) {
|
|
// Session
|
|
serviceConnection = null;
|
|
connectionRequested = requestConnectToSession(connectionHints);
|
|
} else {
|
|
serviceConnection = new SessionServiceConnection(connectionHints);
|
|
connectionRequested = requestConnectToService();
|
|
}
|
|
if (!connectionRequested) {
|
|
getInstance().runOnApplicationLooper(getInstance()::release);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addListener(Listener listener) {
|
|
listeners.add(listener);
|
|
}
|
|
|
|
@Override
|
|
public void removeListener(Listener listener) {
|
|
listeners.remove(listener);
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
if (!isPlayerCommandAvailable(COMMAND_STOP)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_STOP,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.stop(controllerStub, seq);
|
|
}
|
|
});
|
|
|
|
playerInfo =
|
|
playerInfo.copyWithSessionPositionInfo(
|
|
new SessionPositionInfo(
|
|
playerInfo.sessionPositionInfo.positionInfo,
|
|
playerInfo.sessionPositionInfo.isPlayingAd,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
playerInfo.sessionPositionInfo.durationMs,
|
|
/* bufferedPositionMs= */ playerInfo.sessionPositionInfo.positionInfo.positionMs,
|
|
/* bufferedPercentage= */ calculateBufferedPercentage(
|
|
playerInfo.sessionPositionInfo.positionInfo.positionMs,
|
|
playerInfo.sessionPositionInfo.durationMs),
|
|
/* totalBufferedDurationMs= */ 0,
|
|
playerInfo.sessionPositionInfo.currentLiveOffsetMs,
|
|
playerInfo.sessionPositionInfo.contentDurationMs,
|
|
/* contentBufferedPositionMs= */ playerInfo
|
|
.sessionPositionInfo
|
|
.positionInfo
|
|
.positionMs));
|
|
|
|
if (playerInfo.playbackState != STATE_IDLE) {
|
|
playerInfo =
|
|
playerInfo.copyWithPlaybackState(STATE_IDLE, /* playerError= */ playerInfo.playerError);
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_STATE_CHANGED, listener -> listener.onPlaybackStateChanged(STATE_IDLE));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void release() {
|
|
@Nullable IMediaSession iSession = this.iSession;
|
|
if (released) {
|
|
return;
|
|
}
|
|
released = true;
|
|
connectedToken = null;
|
|
flushCommandQueueHandler.release();
|
|
this.iSession = null;
|
|
if (iSession != null) {
|
|
int seq = sequencedFutureManager.obtainNextSequenceNumber();
|
|
try {
|
|
iSession.asBinder().unlinkToDeath(deathRecipient, 0);
|
|
iSession.release(controllerStub, seq);
|
|
} catch (RemoteException e) {
|
|
// No-op.
|
|
}
|
|
}
|
|
listeners.release();
|
|
sequencedFutureManager.lazyRelease(
|
|
RELEASE_TIMEOUT_MS,
|
|
() -> {
|
|
if (serviceConnection != null) {
|
|
context.unbindService(serviceConnection);
|
|
serviceConnection = null;
|
|
}
|
|
controllerStub.destroy();
|
|
});
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public SessionToken getConnectedToken() {
|
|
return connectedToken;
|
|
}
|
|
|
|
@Override
|
|
public boolean isConnected() {
|
|
return iSession != null;
|
|
}
|
|
|
|
/* package */ boolean isReleased() {
|
|
return released;
|
|
}
|
|
|
|
/* @FunctionalInterface */
|
|
private interface RemoteSessionTask {
|
|
void run(IMediaSession iSession, int seq) throws RemoteException;
|
|
}
|
|
|
|
private ListenableFuture<SessionResult> dispatchRemoteSessionTaskWithPlayerCommand(
|
|
@Player.Command int command, RemoteSessionTask task) {
|
|
if (command != COMMAND_SET_VIDEO_SURFACE) {
|
|
flushCommandQueueHandler.sendFlushCommandQueueMessage();
|
|
}
|
|
return dispatchRemoteSessionTask(iSession, task);
|
|
}
|
|
|
|
private ListenableFuture<SessionResult> dispatchRemoteSessionTaskWithSessionCommand(
|
|
@CommandCode int commandCode, RemoteSessionTask task) {
|
|
return dispatchRemoteSessionTaskWithSessionCommandInternal(
|
|
commandCode, /* sessionCommand= */ null, task);
|
|
}
|
|
|
|
private ListenableFuture<SessionResult> dispatchRemoteSessionTaskWithSessionCommand(
|
|
SessionCommand sessionCommand, RemoteSessionTask task) {
|
|
return dispatchRemoteSessionTaskWithSessionCommandInternal(
|
|
COMMAND_CODE_CUSTOM, sessionCommand, task);
|
|
}
|
|
|
|
private ListenableFuture<SessionResult> dispatchRemoteSessionTaskWithSessionCommandInternal(
|
|
@CommandCode int commandCode, SessionCommand sessionCommand, RemoteSessionTask task) {
|
|
return dispatchRemoteSessionTask(
|
|
sessionCommand != null
|
|
? getSessionInterfaceWithSessionCommandIfAble(sessionCommand)
|
|
: getSessionInterfaceWithSessionCommandIfAble(commandCode),
|
|
task);
|
|
}
|
|
|
|
private ListenableFuture<SessionResult> dispatchRemoteSessionTask(
|
|
IMediaSession iSession, RemoteSessionTask task) {
|
|
if (iSession != null) {
|
|
SequencedFutureManager.SequencedFuture<SessionResult> result =
|
|
sequencedFutureManager.createSequencedFuture(new SessionResult(RESULT_INFO_SKIPPED));
|
|
try {
|
|
task.run(iSession, result.getSequenceNumber());
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Cannot connect to the service or the session is gone", e);
|
|
result.set(new SessionResult(RESULT_ERROR_SESSION_DISCONNECTED));
|
|
}
|
|
return result;
|
|
} else {
|
|
// Don't create Future with SequencedFutureManager.
|
|
// Otherwise session would receive discontinued sequence number, and it would make
|
|
// future work item 'keeping call sequence when session execute commands' impossible.
|
|
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_PERMISSION_DENIED));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void play() {
|
|
if (!isPlayerCommandAvailable(COMMAND_PLAY_PAUSE)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_PLAY_PAUSE,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.play(controllerStub, seq);
|
|
}
|
|
});
|
|
|
|
setPlayWhenReady(
|
|
/* playWhenReady= */ true,
|
|
PLAYBACK_SUPPRESSION_REASON_NONE,
|
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
|
}
|
|
|
|
@Override
|
|
public void pause() {
|
|
if (!isPlayerCommandAvailable(COMMAND_PLAY_PAUSE)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_PLAY_PAUSE,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.pause(controllerStub, seq);
|
|
}
|
|
});
|
|
|
|
setPlayWhenReady(
|
|
/* playWhenReady= */ false,
|
|
PLAYBACK_SUPPRESSION_REASON_NONE,
|
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
|
}
|
|
|
|
@Override
|
|
public void prepare() {
|
|
if (!isPlayerCommandAvailable(COMMAND_PREPARE)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_PREPARE,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.prepare(controllerStub, seq);
|
|
}
|
|
});
|
|
|
|
if (playerInfo.playbackState == Player.STATE_IDLE) {
|
|
PlayerInfo playerInfo =
|
|
this.playerInfo.copyWithPlaybackState(
|
|
this.playerInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING,
|
|
/* playerError= */ null);
|
|
|
|
updatePlayerInfo(
|
|
playerInfo,
|
|
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
/* positionDiscontinuity= */ false,
|
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
|
/* mediaItemTransition= */ false,
|
|
/* ignored */ MEDIA_ITEM_TRANSITION_REASON_REPEAT);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void seekToDefaultPosition() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_DEFAULT_POSITION,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.seekToDefaultPosition(controllerStub, seq);
|
|
}
|
|
});
|
|
|
|
seekToInternal(getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
}
|
|
|
|
@Override
|
|
public void seekToDefaultPosition(int mediaItemIndex) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_MEDIA_ITEM,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.seekToDefaultPositionWithMediaItemIndex(controllerStub, seq, mediaItemIndex);
|
|
}
|
|
});
|
|
|
|
seekToInternal(mediaItemIndex, /* positionMs= */ C.TIME_UNSET);
|
|
}
|
|
|
|
@Override
|
|
public void seekTo(long positionMs) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.seekTo(controllerStub, seq, positionMs);
|
|
}
|
|
});
|
|
|
|
seekToInternal(getCurrentMediaItemIndex(), positionMs);
|
|
}
|
|
|
|
@Override
|
|
public void seekTo(int mediaItemIndex, long positionMs) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_MEDIA_ITEM,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.seekToWithMediaItemIndex(controllerStub, seq, mediaItemIndex, positionMs);
|
|
}
|
|
});
|
|
|
|
seekToInternal(mediaItemIndex, positionMs);
|
|
}
|
|
|
|
@Override
|
|
public long getSeekBackIncrement() {
|
|
return playerInfo.seekBackIncrementMs;
|
|
}
|
|
|
|
@Override
|
|
public void seekBack() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_BACK)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_BACK, (iSession, seq) -> iSession.seekBack(controllerStub, seq));
|
|
|
|
seekToInternalByOffset(-getSeekBackIncrement());
|
|
}
|
|
|
|
@Override
|
|
public long getSeekForwardIncrement() {
|
|
return playerInfo.seekForwardIncrementMs;
|
|
}
|
|
|
|
@Override
|
|
public void seekForward() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_FORWARD)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_FORWARD, (iSession, seq) -> iSession.seekForward(controllerStub, seq));
|
|
|
|
seekToInternalByOffset(getSeekForwardIncrement());
|
|
}
|
|
|
|
@Override
|
|
public PendingIntent getSessionActivity() {
|
|
return sessionActivity;
|
|
}
|
|
|
|
@Override
|
|
public void setPlayWhenReady(boolean playWhenReady) {
|
|
if (!isPlayerCommandAvailable(COMMAND_PLAY_PAUSE)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_PLAY_PAUSE,
|
|
(iSession, seq) -> iSession.setPlayWhenReady(controllerStub, seq, playWhenReady));
|
|
|
|
setPlayWhenReady(
|
|
playWhenReady,
|
|
PLAYBACK_SUPPRESSION_REASON_NONE,
|
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
|
}
|
|
|
|
@Override
|
|
public boolean getPlayWhenReady() {
|
|
return playerInfo.playWhenReady;
|
|
}
|
|
|
|
@Override
|
|
@Player.PlaybackSuppressionReason
|
|
public int getPlaybackSuppressionReason() {
|
|
return playerInfo.playbackSuppressionReason;
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public PlaybackException getPlayerError() {
|
|
return playerInfo.playerError;
|
|
}
|
|
|
|
@Override
|
|
@Player.State
|
|
public int getPlaybackState() {
|
|
return playerInfo.playbackState;
|
|
}
|
|
|
|
@Override
|
|
public boolean isPlaying() {
|
|
return playerInfo.isPlaying;
|
|
}
|
|
|
|
@Override
|
|
public boolean isLoading() {
|
|
return playerInfo.isLoading;
|
|
}
|
|
|
|
@Override
|
|
public long getDuration() {
|
|
return playerInfo.sessionPositionInfo.durationMs;
|
|
}
|
|
|
|
@Override
|
|
public long getCurrentPosition() {
|
|
if (!playerInfo.isPlaying) {
|
|
return playerInfo.sessionPositionInfo.positionInfo.positionMs;
|
|
}
|
|
long elapsedTimeMs =
|
|
(getInstance().getTimeDiffMs() != C.TIME_UNSET)
|
|
? getInstance().getTimeDiffMs()
|
|
: SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs;
|
|
long estimatedPositionMs =
|
|
playerInfo.sessionPositionInfo.positionInfo.positionMs
|
|
+ (long) (elapsedTimeMs * playerInfo.playbackParameters.speed);
|
|
return playerInfo.sessionPositionInfo.durationMs == C.TIME_UNSET
|
|
? estimatedPositionMs
|
|
: Math.min(estimatedPositionMs, playerInfo.sessionPositionInfo.durationMs);
|
|
}
|
|
|
|
@Override
|
|
public long getBufferedPosition() {
|
|
return playerInfo.sessionPositionInfo.bufferedPositionMs;
|
|
}
|
|
|
|
@Override
|
|
public int getBufferedPercentage() {
|
|
return playerInfo.sessionPositionInfo.bufferedPercentage;
|
|
}
|
|
|
|
@Override
|
|
public long getTotalBufferedDuration() {
|
|
return playerInfo.sessionPositionInfo.totalBufferedDurationMs;
|
|
}
|
|
|
|
@Override
|
|
public long getCurrentLiveOffset() {
|
|
return playerInfo.sessionPositionInfo.currentLiveOffsetMs;
|
|
}
|
|
|
|
@Override
|
|
public long getContentDuration() {
|
|
return playerInfo.sessionPositionInfo.contentDurationMs;
|
|
}
|
|
|
|
@Override
|
|
public long getContentPosition() {
|
|
boolean receivedUpdatedPositionInfo =
|
|
lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs;
|
|
if (!playerInfo.isPlaying || playerInfo.sessionPositionInfo.isPlayingAd) {
|
|
if (receivedUpdatedPositionInfo || lastReturnedContentPositionMs == C.TIME_UNSET) {
|
|
lastReturnedContentPositionMs =
|
|
playerInfo.sessionPositionInfo.positionInfo.contentPositionMs;
|
|
}
|
|
return lastReturnedContentPositionMs;
|
|
}
|
|
|
|
if (!receivedUpdatedPositionInfo && lastReturnedContentPositionMs != C.TIME_UNSET) {
|
|
// We need an updated content position to make a new position estimation.
|
|
return lastReturnedContentPositionMs;
|
|
}
|
|
|
|
long elapsedTimeMs =
|
|
(getInstance().getTimeDiffMs() != C.TIME_UNSET)
|
|
? getInstance().getTimeDiffMs()
|
|
: SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs;
|
|
long estimatedPositionMs =
|
|
playerInfo.sessionPositionInfo.positionInfo.contentPositionMs
|
|
+ (long) (elapsedTimeMs * playerInfo.playbackParameters.speed);
|
|
if (playerInfo.sessionPositionInfo.contentDurationMs != C.TIME_UNSET) {
|
|
estimatedPositionMs =
|
|
Math.min(estimatedPositionMs, playerInfo.sessionPositionInfo.contentDurationMs);
|
|
}
|
|
lastReturnedContentPositionMs = estimatedPositionMs;
|
|
return lastReturnedContentPositionMs;
|
|
}
|
|
|
|
@Override
|
|
public long getContentBufferedPosition() {
|
|
return playerInfo.sessionPositionInfo.contentBufferedPositionMs;
|
|
}
|
|
|
|
@Override
|
|
public boolean isPlayingAd() {
|
|
return playerInfo.sessionPositionInfo.isPlayingAd;
|
|
}
|
|
|
|
@Override
|
|
public int getCurrentAdGroupIndex() {
|
|
return playerInfo.sessionPositionInfo.positionInfo.adGroupIndex;
|
|
}
|
|
|
|
@Override
|
|
public int getCurrentAdIndexInAdGroup() {
|
|
return playerInfo.sessionPositionInfo.positionInfo.adIndexInAdGroup;
|
|
}
|
|
|
|
@Override
|
|
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_SPEED_AND_PITCH,
|
|
(iSession, seq) ->
|
|
iSession.setPlaybackParameters(controllerStub, seq, playbackParameters.toBundle()));
|
|
|
|
if (!playerInfo.playbackParameters.equals(playbackParameters)) {
|
|
playerInfo = playerInfo.copyWithPlaybackParameters(playbackParameters);
|
|
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_PARAMETERS_CHANGED,
|
|
listener -> listener.onPlaybackParametersChanged(playbackParameters));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public PlaybackParameters getPlaybackParameters() {
|
|
return playerInfo.playbackParameters;
|
|
}
|
|
|
|
@Override
|
|
public void setPlaybackSpeed(float speed) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_SPEED_AND_PITCH,
|
|
(iSession, seq) -> iSession.setPlaybackSpeed(controllerStub, seq, speed));
|
|
|
|
if (playerInfo.playbackParameters.speed != speed) {
|
|
PlaybackParameters newPlaybackParameters = playerInfo.playbackParameters.withSpeed(speed);
|
|
playerInfo = playerInfo.copyWithPlaybackParameters(newPlaybackParameters);
|
|
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_PARAMETERS_CHANGED,
|
|
listener -> listener.onPlaybackParametersChanged(newPlaybackParameters));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public AudioAttributes getAudioAttributes() {
|
|
return playerInfo.audioAttributes;
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<SessionResult> setRating(String mediaId, Rating rating) {
|
|
return dispatchRemoteSessionTaskWithSessionCommand(
|
|
COMMAND_CODE_SESSION_SET_RATING,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.setRatingWithMediaId(controllerStub, seq, mediaId, rating.toBundle());
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<SessionResult> setRating(Rating rating) {
|
|
return dispatchRemoteSessionTaskWithSessionCommand(
|
|
COMMAND_CODE_SESSION_SET_RATING,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.setRating(controllerStub, seq, rating.toBundle());
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<SessionResult> sendCustomCommand(SessionCommand command, Bundle args) {
|
|
return dispatchRemoteSessionTaskWithSessionCommand(
|
|
command,
|
|
(iSession, seq) -> iSession.onCustomCommand(controllerStub, seq, command.toBundle(), args));
|
|
}
|
|
|
|
@Override
|
|
public Timeline getCurrentTimeline() {
|
|
return playerInfo.timeline;
|
|
}
|
|
|
|
@Override
|
|
public void setMediaItem(MediaItem mediaItem) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_MEDIA_ITEM,
|
|
(iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle()));
|
|
|
|
setMediaItemsInternal(
|
|
Collections.singletonList(mediaItem),
|
|
/* startIndex= */ C.INDEX_UNSET,
|
|
/* startPositionMs= */ C.TIME_UNSET,
|
|
/* resetToDefaultPosition= */ true);
|
|
}
|
|
|
|
@Override
|
|
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_MEDIA_ITEM,
|
|
(iSession, seq) ->
|
|
iSession.setMediaItemWithStartPosition(
|
|
controllerStub, seq, mediaItem.toBundle(), startPositionMs));
|
|
|
|
setMediaItemsInternal(
|
|
Collections.singletonList(mediaItem),
|
|
/* startIndex= */ C.INDEX_UNSET,
|
|
/* startPositionMs= */ startPositionMs,
|
|
/* resetToDefaultPosition= */ false);
|
|
}
|
|
|
|
@Override
|
|
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_MEDIA_ITEM,
|
|
(iSession, seq) ->
|
|
iSession.setMediaItemWithResetPosition(
|
|
controllerStub, seq, mediaItem.toBundle(), resetPosition));
|
|
|
|
setMediaItemsInternal(
|
|
Collections.singletonList(mediaItem),
|
|
/* startIndex= */ C.INDEX_UNSET,
|
|
/* startPositionMs= */ C.TIME_UNSET,
|
|
/* resetToDefaultPosition= */ resetPosition);
|
|
}
|
|
|
|
@Override
|
|
public void setMediaItems(List<MediaItem> mediaItems) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.setMediaItems(
|
|
controllerStub,
|
|
seq,
|
|
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
|
|
|
|
setMediaItemsInternal(
|
|
mediaItems,
|
|
/* startIndex= */ C.INDEX_UNSET,
|
|
/* startPositionMs= */ C.TIME_UNSET,
|
|
/* resetToDefaultPosition= */ true);
|
|
}
|
|
|
|
@Override
|
|
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.setMediaItemsWithResetPosition(
|
|
controllerStub,
|
|
seq,
|
|
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)),
|
|
resetPosition));
|
|
|
|
setMediaItemsInternal(
|
|
mediaItems,
|
|
/* startIndex= */ C.INDEX_UNSET,
|
|
/* startPositionMs= */ C.TIME_UNSET,
|
|
/* resetToDefaultPosition= */ resetPosition);
|
|
}
|
|
|
|
@Override
|
|
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.setMediaItemsWithStartIndex(
|
|
controllerStub,
|
|
seq,
|
|
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)),
|
|
startIndex,
|
|
startPositionMs));
|
|
|
|
setMediaItemsInternal(
|
|
mediaItems, startIndex, startPositionMs, /* resetToDefaultPosition= */ false);
|
|
}
|
|
|
|
@Override
|
|
public void setPlaylistMetadata(MediaMetadata playlistMetadata) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_MEDIA_ITEMS_METADATA,
|
|
(iSession, seq) ->
|
|
iSession.setPlaylistMetadata(controllerStub, seq, playlistMetadata.toBundle()));
|
|
|
|
if (!playerInfo.playlistMetadata.equals(playlistMetadata)) {
|
|
playerInfo = playerInfo.copyWithPlaylistMetadata(playlistMetadata);
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onPlaylistMetadataChanged(playlistMetadata));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public MediaMetadata getPlaylistMetadata() {
|
|
return playerInfo.playlistMetadata;
|
|
}
|
|
|
|
@Override
|
|
public void addMediaItem(MediaItem mediaItem) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) -> iSession.addMediaItem(controllerStub, seq, mediaItem.toBundle()));
|
|
|
|
addMediaItemsInternal(
|
|
getCurrentTimeline().getWindowCount(), Collections.singletonList(mediaItem));
|
|
}
|
|
|
|
@Override
|
|
public void addMediaItem(int index, MediaItem mediaItem) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.addMediaItemWithIndex(controllerStub, seq, index, mediaItem.toBundle()));
|
|
|
|
addMediaItemsInternal(index, Collections.singletonList(mediaItem));
|
|
}
|
|
|
|
@Override
|
|
public void addMediaItems(List<MediaItem> mediaItems) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.addMediaItems(
|
|
controllerStub,
|
|
seq,
|
|
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
|
|
|
|
addMediaItemsInternal(getCurrentTimeline().getWindowCount(), mediaItems);
|
|
}
|
|
|
|
@Override
|
|
public void addMediaItems(int index, List<MediaItem> mediaItems) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.addMediaItemsWithIndex(
|
|
controllerStub,
|
|
seq,
|
|
index,
|
|
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
|
|
|
|
addMediaItemsInternal(index, mediaItems);
|
|
}
|
|
|
|
private void addMediaItemsInternal(int index, List<MediaItem> mediaItems) {
|
|
if (mediaItems.isEmpty()) {
|
|
return;
|
|
}
|
|
// Add media items to the end of the timeline if the index exceeds the window count.
|
|
index = min(index, playerInfo.timeline.getWindowCount());
|
|
|
|
Timeline oldTimeline = playerInfo.timeline;
|
|
List<Window> newWindows = new ArrayList<>();
|
|
List<Period> newPeriods = new ArrayList<>();
|
|
for (int i = 0; i < oldTimeline.getWindowCount(); i++) {
|
|
newWindows.add(oldTimeline.getWindow(i, new Window()));
|
|
}
|
|
for (int i = 0; i < mediaItems.size(); i++) {
|
|
newWindows.add(i + index, createNewWindow(mediaItems.get(i)));
|
|
}
|
|
rebuildPeriods(oldTimeline, newWindows, newPeriods);
|
|
Timeline newTimeline = createMaskingTimeline(newWindows, newPeriods);
|
|
|
|
int newMediaItemIndex;
|
|
int newPeriodIndex;
|
|
if (playerInfo.timeline.isEmpty()) {
|
|
newMediaItemIndex = 0;
|
|
newPeriodIndex = 0;
|
|
} else {
|
|
newMediaItemIndex =
|
|
(playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= index)
|
|
? playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex + mediaItems.size()
|
|
: playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex;
|
|
newPeriodIndex =
|
|
(playerInfo.sessionPositionInfo.positionInfo.periodIndex >= index)
|
|
? playerInfo.sessionPositionInfo.positionInfo.periodIndex + mediaItems.size()
|
|
: playerInfo.sessionPositionInfo.positionInfo.periodIndex;
|
|
}
|
|
PlayerInfo newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
newTimeline,
|
|
newMediaItemIndex,
|
|
newPeriodIndex,
|
|
DISCONTINUITY_REASON_INTERNAL);
|
|
updatePlayerInfo(
|
|
newPlayerInfo,
|
|
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
/* positionDiscontinuity= */ false,
|
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
|
/* mediaItemTransition= */ oldTimeline.isEmpty(),
|
|
MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
|
|
}
|
|
|
|
@Override
|
|
public void removeMediaItem(int index) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) -> iSession.removeMediaItem(controllerStub, seq, index));
|
|
|
|
removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1);
|
|
}
|
|
|
|
@Override
|
|
public void removeMediaItems(int fromIndex, int toIndex) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) -> iSession.removeMediaItems(controllerStub, seq, fromIndex, toIndex));
|
|
|
|
removeMediaItemsInternal(fromIndex, toIndex);
|
|
}
|
|
|
|
@Override
|
|
public void clearMediaItems() {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) -> iSession.clearMediaItems(controllerStub, seq));
|
|
|
|
removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ Integer.MAX_VALUE);
|
|
}
|
|
|
|
private void removeMediaItemsInternal(int fromIndex, int toIndex) {
|
|
int clippedToIndex = min(toIndex, playerInfo.timeline.getWindowCount());
|
|
|
|
checkArgument(
|
|
fromIndex >= 0
|
|
&& clippedToIndex >= fromIndex
|
|
&& clippedToIndex <= playerInfo.timeline.getWindowCount());
|
|
|
|
Timeline oldTimeline = playerInfo.timeline;
|
|
|
|
List<Window> newWindows = new ArrayList<>();
|
|
List<Period> newPeriods = new ArrayList<>();
|
|
for (int i = 0; i < oldTimeline.getWindowCount(); i++) {
|
|
if (i < fromIndex || i >= clippedToIndex) {
|
|
newWindows.add(oldTimeline.getWindow(i, new Window()));
|
|
}
|
|
}
|
|
rebuildPeriods(oldTimeline, newWindows, newPeriods);
|
|
Timeline newTimeline = createMaskingTimeline(newWindows, newPeriods);
|
|
|
|
int oldMediaItemIndex = getCurrentMediaItemIndex();
|
|
int newMediaItemIndex = oldMediaItemIndex;
|
|
int oldPeriodIndex = playerInfo.sessionPositionInfo.positionInfo.periodIndex;
|
|
int newPeriodIndex = oldPeriodIndex;
|
|
boolean currentItemRemoved =
|
|
getCurrentMediaItemIndex() >= fromIndex && getCurrentMediaItemIndex() < clippedToIndex;
|
|
Window window = new Window();
|
|
if (oldTimeline.isEmpty()) {
|
|
// No masking required. Just forwarding command to session.
|
|
} else {
|
|
if (newTimeline.isEmpty()) {
|
|
newMediaItemIndex = C.INDEX_UNSET;
|
|
newPeriodIndex = 0;
|
|
} else {
|
|
if (currentItemRemoved) {
|
|
int oldNextMediaItemIndex =
|
|
resolveSubsequentMediaItemIndex(
|
|
getRepeatMode(),
|
|
getShuffleModeEnabled(),
|
|
oldMediaItemIndex,
|
|
oldTimeline,
|
|
fromIndex,
|
|
toIndex);
|
|
if (oldNextMediaItemIndex == C.INDEX_UNSET) {
|
|
newMediaItemIndex = newTimeline.getFirstWindowIndex(getShuffleModeEnabled());
|
|
} else if (oldNextMediaItemIndex >= clippedToIndex) {
|
|
newMediaItemIndex = oldNextMediaItemIndex - (clippedToIndex - fromIndex);
|
|
} else {
|
|
newMediaItemIndex = oldNextMediaItemIndex;
|
|
}
|
|
newPeriodIndex = newTimeline.getWindow(newMediaItemIndex, window).firstPeriodIndex;
|
|
} else if (oldMediaItemIndex >= clippedToIndex) {
|
|
newMediaItemIndex -= (clippedToIndex - fromIndex);
|
|
newPeriodIndex =
|
|
getNewPeriodIndexWithoutRemovedPeriods(
|
|
oldTimeline, oldPeriodIndex, fromIndex, clippedToIndex);
|
|
}
|
|
}
|
|
|
|
PlayerInfo newPlayerInfo;
|
|
if (currentItemRemoved) {
|
|
PositionInfo newPositionInfo;
|
|
if (newMediaItemIndex == C.INDEX_UNSET) {
|
|
newPositionInfo = SessionPositionInfo.DEFAULT_POSITION_INFO;
|
|
newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
newTimeline,
|
|
newPositionInfo,
|
|
SessionPositionInfo.DEFAULT,
|
|
DISCONTINUITY_REASON_REMOVE);
|
|
} else {
|
|
Window newWindow = newTimeline.getWindow(newMediaItemIndex, new Window());
|
|
long defaultPositionMs = newWindow.getDefaultPositionMs();
|
|
long durationMs = newWindow.getDurationMs();
|
|
newPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
newMediaItemIndex,
|
|
newWindow.mediaItem,
|
|
/* periodUid= */ null,
|
|
newPeriodIndex,
|
|
/* positionMs= */ defaultPositionMs,
|
|
/* contentPositionMs= */ defaultPositionMs,
|
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
|
/* adIndexInAdGroup= */ C.INDEX_UNSET);
|
|
newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
newTimeline,
|
|
newPositionInfo,
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
/* isPlayingAd= */ false,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
/* durationMs= */ durationMs,
|
|
/* bufferedPositionMs= */ defaultPositionMs,
|
|
/* bufferedPercentage= */ calculateBufferedPercentage(
|
|
defaultPositionMs, durationMs),
|
|
/* totalBufferedDurationMs= */ 0,
|
|
/* currentLiveOffsetMs= */ C.TIME_UNSET,
|
|
/* contentDurationMs= */ durationMs,
|
|
/* contentBufferedPositionMs= */ defaultPositionMs),
|
|
DISCONTINUITY_REASON_REMOVE);
|
|
}
|
|
} else {
|
|
newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
newTimeline,
|
|
newMediaItemIndex,
|
|
newPeriodIndex,
|
|
DISCONTINUITY_REASON_REMOVE);
|
|
}
|
|
|
|
// Player transitions to STATE_ENDED if the current index is part of the removed tail.
|
|
final boolean transitionsToEnded =
|
|
newPlayerInfo.playbackState != STATE_IDLE
|
|
&& newPlayerInfo.playbackState != STATE_ENDED
|
|
&& fromIndex < clippedToIndex
|
|
&& clippedToIndex == oldTimeline.getWindowCount()
|
|
&& getCurrentMediaItemIndex() >= fromIndex;
|
|
if (transitionsToEnded) {
|
|
newPlayerInfo = newPlayerInfo.copyWithPlaybackState(STATE_ENDED, /* playerError= */ null);
|
|
}
|
|
|
|
updatePlayerInfo(
|
|
newPlayerInfo,
|
|
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
/* positionDiscontinuity= */ currentItemRemoved,
|
|
DISCONTINUITY_REASON_REMOVE,
|
|
/* mediaItemTransition*/ playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex
|
|
>= fromIndex
|
|
&& playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < clippedToIndex,
|
|
MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void moveMediaItem(int currentIndex, int newIndex) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
checkArgument(
|
|
currentIndex >= 0 && currentIndex < playerInfo.timeline.getWindowCount() && newIndex >= 0);
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) -> iSession.moveMediaItem(controllerStub, seq, currentIndex, newIndex));
|
|
|
|
int clippedNewIndex = min(newIndex, playerInfo.timeline.getWindowCount() - 1);
|
|
|
|
moveMediaItemsInternal(
|
|
/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, clippedNewIndex);
|
|
}
|
|
|
|
@Override
|
|
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
|
|
if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
return;
|
|
}
|
|
|
|
checkArgument(
|
|
fromIndex >= 0
|
|
&& fromIndex <= toIndex
|
|
&& toIndex <= playerInfo.timeline.getWindowCount()
|
|
&& newIndex >= 0);
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
(iSession, seq) ->
|
|
iSession.moveMediaItems(controllerStub, seq, fromIndex, toIndex, newIndex));
|
|
|
|
int clippedNewIndex =
|
|
min(newIndex, playerInfo.timeline.getWindowCount() - (toIndex - fromIndex));
|
|
|
|
moveMediaItemsInternal(fromIndex, toIndex, clippedNewIndex);
|
|
}
|
|
|
|
@Override
|
|
public int getCurrentPeriodIndex() {
|
|
return playerInfo.sessionPositionInfo.positionInfo.periodIndex;
|
|
}
|
|
|
|
@Override
|
|
public int getCurrentMediaItemIndex() {
|
|
return playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex == C.INDEX_UNSET
|
|
? 0
|
|
: playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex;
|
|
}
|
|
|
|
// TODO(b/184479406): Get the index directly from Player rather than Timeline.
|
|
@Override
|
|
public int getPreviousMediaItemIndex() {
|
|
return playerInfo.timeline.isEmpty()
|
|
? C.INDEX_UNSET
|
|
: playerInfo.timeline.getPreviousWindowIndex(
|
|
getCurrentMediaItemIndex(),
|
|
convertRepeatModeForNavigation(playerInfo.repeatMode),
|
|
playerInfo.shuffleModeEnabled);
|
|
}
|
|
|
|
// TODO(b/184479406): Get the index directly from Player rather than Timeline.
|
|
@Override
|
|
public int getNextMediaItemIndex() {
|
|
return playerInfo.timeline.isEmpty()
|
|
? C.INDEX_UNSET
|
|
: playerInfo.timeline.getNextWindowIndex(
|
|
getCurrentMediaItemIndex(),
|
|
convertRepeatModeForNavigation(playerInfo.repeatMode),
|
|
playerInfo.shuffleModeEnabled);
|
|
}
|
|
|
|
@Override
|
|
public boolean hasPreviousMediaItem() {
|
|
return getPreviousMediaItemIndex() != C.INDEX_UNSET;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasNextMediaItem() {
|
|
return getNextMediaItemIndex() != C.INDEX_UNSET;
|
|
}
|
|
|
|
@Override
|
|
public void seekToPreviousMediaItem() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
|
|
(iSession, seq) -> iSession.seekToPreviousMediaItem(controllerStub, seq));
|
|
|
|
if (getPreviousMediaItemIndex() != C.INDEX_UNSET) {
|
|
seekToInternal(getPreviousMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void seekToNextMediaItem() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
|
|
(iSession, seq) -> iSession.seekToNextMediaItem(controllerStub, seq));
|
|
|
|
if (getNextMediaItemIndex() != C.INDEX_UNSET) {
|
|
seekToInternal(getNextMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void seekToPrevious() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_PREVIOUS, (iSession, seq) -> iSession.seekToPrevious(controllerStub, seq));
|
|
|
|
Timeline timeline = getCurrentTimeline();
|
|
if (timeline.isEmpty() || isPlayingAd()) {
|
|
return;
|
|
}
|
|
boolean hasPreviousMediaItem = hasPreviousMediaItem();
|
|
Window window = timeline.getWindow(getCurrentMediaItemIndex(), new Window());
|
|
if (window.isDynamic && window.isLive()) {
|
|
if (hasPreviousMediaItem) {
|
|
seekToInternal(getPreviousMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
}
|
|
} else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) {
|
|
seekToInternal(getPreviousMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
} else {
|
|
seekToInternal(getCurrentMediaItemIndex(), /* positionMs= */ 0);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long getMaxSeekToPreviousPosition() {
|
|
return playerInfo.maxSeekToPreviousPositionMs;
|
|
}
|
|
|
|
@Override
|
|
public void seekToNext() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SEEK_TO_NEXT)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SEEK_TO_NEXT, (iSession, seq) -> iSession.seekToNext(controllerStub, seq));
|
|
|
|
Timeline timeline = getCurrentTimeline();
|
|
if (timeline.isEmpty() || isPlayingAd()) {
|
|
return;
|
|
}
|
|
if (hasNextMediaItem()) {
|
|
seekToInternal(getNextMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
} else {
|
|
Window window = timeline.getWindow(getCurrentMediaItemIndex(), new Window());
|
|
if (window.isDynamic && window.isLive()) {
|
|
seekToInternal(getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getRepeatMode() {
|
|
return playerInfo.repeatMode;
|
|
}
|
|
|
|
@Override
|
|
public void setRepeatMode(@RepeatMode int repeatMode) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_REPEAT_MODE,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.setRepeatMode(controllerStub, seq, repeatMode);
|
|
}
|
|
});
|
|
|
|
if (playerInfo.repeatMode != repeatMode) {
|
|
playerInfo = playerInfo.copyWithRepeatMode(repeatMode);
|
|
|
|
listeners.queueEvent(
|
|
EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean getShuffleModeEnabled() {
|
|
return playerInfo.shuffleModeEnabled;
|
|
}
|
|
|
|
@Override
|
|
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_SHUFFLE_MODE,
|
|
new RemoteSessionTask() {
|
|
@Override
|
|
public void run(IMediaSession iSession, int seq) throws RemoteException {
|
|
iSession.setShuffleModeEnabled(controllerStub, seq, shuffleModeEnabled);
|
|
}
|
|
});
|
|
|
|
if (playerInfo.shuffleModeEnabled != shuffleModeEnabled) {
|
|
playerInfo = playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled);
|
|
|
|
listeners.queueEvent(
|
|
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
|
|
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public CueGroup getCurrentCues() {
|
|
return playerInfo.cueGroup;
|
|
}
|
|
|
|
@Override
|
|
public float getVolume() {
|
|
return playerInfo.volume;
|
|
}
|
|
|
|
@Override
|
|
public void setVolume(float volume) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VOLUME)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_VOLUME, (iSession, seq) -> iSession.setVolume(controllerStub, seq, volume));
|
|
|
|
if (playerInfo.volume != volume) {
|
|
playerInfo = playerInfo.copyWithVolume(volume);
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onVolumeChanged(volume));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public DeviceInfo getDeviceInfo() {
|
|
return playerInfo.deviceInfo;
|
|
}
|
|
|
|
@Override
|
|
public int getDeviceVolume() {
|
|
return playerInfo.deviceVolume;
|
|
}
|
|
|
|
@Override
|
|
public boolean isDeviceMuted() {
|
|
return playerInfo.deviceMuted;
|
|
}
|
|
|
|
@Override
|
|
public void setDeviceVolume(int volume) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_DEVICE_VOLUME)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_DEVICE_VOLUME,
|
|
(iSession, seq) -> iSession.setDeviceVolume(controllerStub, seq, volume));
|
|
|
|
if (playerInfo.deviceVolume != volume) {
|
|
playerInfo = playerInfo.copyWithDeviceVolume(volume, playerInfo.deviceMuted);
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onDeviceVolumeChanged(volume, playerInfo.deviceMuted));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void increaseDeviceVolume() {
|
|
if (!isPlayerCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_ADJUST_DEVICE_VOLUME,
|
|
(iSession, seq) -> iSession.increaseDeviceVolume(controllerStub, seq));
|
|
|
|
int newDeviceVolume = playerInfo.deviceVolume + 1;
|
|
if (newDeviceVolume <= getDeviceInfo().maxVolume) {
|
|
playerInfo = playerInfo.copyWithDeviceVolume(newDeviceVolume, playerInfo.deviceMuted);
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onDeviceVolumeChanged(newDeviceVolume, playerInfo.deviceMuted));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void decreaseDeviceVolume() {
|
|
if (!isPlayerCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_ADJUST_DEVICE_VOLUME,
|
|
(iSession, seq) -> iSession.decreaseDeviceVolume(controllerStub, seq));
|
|
|
|
int newDeviceVolume = playerInfo.deviceVolume - 1;
|
|
if (newDeviceVolume >= getDeviceInfo().minVolume) {
|
|
playerInfo = playerInfo.copyWithDeviceVolume(newDeviceVolume, playerInfo.deviceMuted);
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onDeviceVolumeChanged(newDeviceVolume, playerInfo.deviceMuted));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setDeviceMuted(boolean muted) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_DEVICE_VOLUME)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_DEVICE_VOLUME,
|
|
(iSession, seq) -> iSession.setDeviceMuted(controllerStub, seq, muted));
|
|
|
|
if (playerInfo.deviceMuted != muted) {
|
|
playerInfo = playerInfo.copyWithDeviceVolume(playerInfo.deviceVolume, muted);
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onDeviceVolumeChanged(playerInfo.deviceVolume, muted));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public VideoSize getVideoSize() {
|
|
return playerInfo.videoSize;
|
|
}
|
|
|
|
@Override
|
|
public void clearVideoSurface() {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
clearSurfacesAndCallbacks();
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null);
|
|
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
|
|
}
|
|
|
|
@Override
|
|
public void clearVideoSurface(@Nullable Surface surface) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
if (surface == null || videoSurface != surface) {
|
|
return;
|
|
}
|
|
clearVideoSurface();
|
|
}
|
|
|
|
@Override
|
|
public void setVideoSurface(@Nullable Surface surface) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
clearSurfacesAndCallbacks();
|
|
videoSurface = surface;
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(surface);
|
|
int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET;
|
|
maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize);
|
|
}
|
|
|
|
@Override
|
|
public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
if (surfaceHolder == null) {
|
|
clearVideoSurface();
|
|
return;
|
|
}
|
|
|
|
if (videoSurfaceHolder == surfaceHolder) {
|
|
return;
|
|
}
|
|
clearSurfacesAndCallbacks();
|
|
videoSurfaceHolder = surfaceHolder;
|
|
videoSurfaceHolder.addCallback(surfaceCallback);
|
|
|
|
@Nullable Surface surface = surfaceHolder.getSurface();
|
|
if (surface != null && surface.isValid()) {
|
|
videoSurface = surface;
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(surface);
|
|
Rect surfaceSize = surfaceHolder.getSurfaceFrame();
|
|
maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height());
|
|
} else {
|
|
videoSurface = null;
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null);
|
|
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
if (surfaceHolder == null || videoSurfaceHolder != surfaceHolder) {
|
|
return;
|
|
}
|
|
clearVideoSurface();
|
|
}
|
|
|
|
@Override
|
|
public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
@Nullable SurfaceHolder surfaceHolder = surfaceView == null ? null : surfaceView.getHolder();
|
|
setVideoSurfaceHolder(surfaceHolder);
|
|
}
|
|
|
|
@Override
|
|
public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
@Nullable SurfaceHolder surfaceHolder = surfaceView == null ? null : surfaceView.getHolder();
|
|
clearVideoSurfaceHolder(surfaceHolder);
|
|
}
|
|
|
|
@Override
|
|
public void setVideoTextureView(@Nullable TextureView textureView) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
if (textureView == null) {
|
|
clearVideoSurface();
|
|
return;
|
|
}
|
|
|
|
if (videoTextureView == textureView) {
|
|
return;
|
|
}
|
|
|
|
clearSurfacesAndCallbacks();
|
|
videoTextureView = textureView;
|
|
videoTextureView.setSurfaceTextureListener(surfaceCallback);
|
|
|
|
@Nullable SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
|
|
if (surfaceTexture == null) {
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null);
|
|
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
|
|
} else {
|
|
videoSurface = new Surface(surfaceTexture);
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface);
|
|
maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void clearVideoTextureView(@Nullable TextureView textureView) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
|
|
return;
|
|
}
|
|
|
|
if (textureView == null || videoTextureView != textureView) {
|
|
return;
|
|
}
|
|
clearVideoSurface();
|
|
}
|
|
|
|
@Override
|
|
public MediaMetadata getMediaMetadata() {
|
|
return playerInfo.mediaMetadata;
|
|
}
|
|
|
|
@Override
|
|
public Commands getAvailableCommands() {
|
|
return intersectedPlayerCommands;
|
|
}
|
|
|
|
@Override
|
|
public Tracks getCurrentTracks() {
|
|
return playerInfo.currentTracks;
|
|
}
|
|
|
|
@Override
|
|
public TrackSelectionParameters getTrackSelectionParameters() {
|
|
return playerInfo.trackSelectionParameters;
|
|
}
|
|
|
|
@Override
|
|
public void setTrackSelectionParameters(TrackSelectionParameters parameters) {
|
|
if (!isPlayerCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
|
|
return;
|
|
}
|
|
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
|
|
(iSession, seq) ->
|
|
iSession.setTrackSelectionParameters(controllerStub, seq, parameters.toBundle()));
|
|
|
|
if (parameters != playerInfo.trackSelectionParameters) {
|
|
playerInfo = playerInfo.copyWithTrackSelectionParameters(parameters);
|
|
|
|
listeners.queueEvent(
|
|
EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
|
|
listener -> listener.onTrackSelectionParametersChanged(parameters));
|
|
listeners.flushEvents();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public SessionCommands getAvailableSessionCommands() {
|
|
return sessionCommands;
|
|
}
|
|
|
|
@Override
|
|
public Context getContext() {
|
|
return context;
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public MediaBrowserCompat getBrowserCompat() {
|
|
return null;
|
|
}
|
|
|
|
private Timeline createMaskingTimeline(List<Window> windows, List<Period> periods) {
|
|
return new RemotableTimeline(
|
|
new ImmutableList.Builder<Window>().addAll(windows).build(),
|
|
new ImmutableList.Builder<Period>().addAll(periods).build(),
|
|
MediaUtils.generateUnshuffledIndices(windows.size()));
|
|
}
|
|
|
|
private void setMediaItemsInternal(
|
|
List<MediaItem> mediaItems,
|
|
int startIndex,
|
|
long startPositionMs,
|
|
boolean resetToDefaultPosition) {
|
|
List<Window> windows = new ArrayList<>();
|
|
List<Period> periods = new ArrayList<>();
|
|
for (int i = 0; i < mediaItems.size(); i++) {
|
|
windows.add(MediaUtils.convertToWindow(mediaItems.get(i), i));
|
|
periods.add(MediaUtils.convertToPeriod(i));
|
|
}
|
|
|
|
Timeline newTimeline = createMaskingTimeline(windows, periods);
|
|
if (!newTimeline.isEmpty() && startIndex >= newTimeline.getWindowCount()) {
|
|
throw new IllegalSeekPositionException(newTimeline, startIndex, startPositionMs);
|
|
}
|
|
|
|
boolean correctedStartIndex = false;
|
|
if (resetToDefaultPosition) {
|
|
startIndex = newTimeline.getFirstWindowIndex(playerInfo.shuffleModeEnabled);
|
|
startPositionMs = C.TIME_UNSET;
|
|
} else if (startIndex == C.INDEX_UNSET) {
|
|
startIndex = playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex;
|
|
startPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs;
|
|
if (startIndex >= newTimeline.getWindowCount()) {
|
|
correctedStartIndex = true;
|
|
startIndex = newTimeline.getFirstWindowIndex(playerInfo.shuffleModeEnabled);
|
|
startPositionMs = C.TIME_UNSET;
|
|
}
|
|
}
|
|
PositionInfo newPositionInfo;
|
|
SessionPositionInfo newSessionPositionInfo;
|
|
@Nullable PeriodInfo periodInfo = getPeriodInfo(newTimeline, startIndex, startPositionMs);
|
|
if (periodInfo == null) {
|
|
// Timeline is empty.
|
|
newPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
startIndex,
|
|
/* mediaItem= */ null,
|
|
/* periodUid= */ null,
|
|
/* periodIndex= */ 0,
|
|
/* positionMs= */ startPositionMs == C.TIME_UNSET ? 0 : startPositionMs,
|
|
/* contentPositionMs= */ startPositionMs == C.TIME_UNSET ? 0 : startPositionMs,
|
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
|
/* adIndexInAdGroup= */ C.INDEX_UNSET);
|
|
newSessionPositionInfo =
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
/* isPlayingAd= */ false,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
/* durationMs= */ C.TIME_UNSET,
|
|
/* bufferedPositionMs= */ startPositionMs == C.TIME_UNSET ? 0 : startPositionMs,
|
|
/* bufferedPercentage= */ 0,
|
|
/* totalBufferedDurationMs= */ 0,
|
|
/* currentLiveOffsetMs= */ C.TIME_UNSET,
|
|
/* contentDurationMs= */ C.TIME_UNSET,
|
|
/* contentBufferedPositionMs= */ startPositionMs == C.TIME_UNSET
|
|
? 0
|
|
: startPositionMs);
|
|
} else {
|
|
newPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
startIndex,
|
|
mediaItems.get(startIndex),
|
|
/* periodUid= */ null,
|
|
periodInfo.index,
|
|
/* positionMs= */ usToMs(periodInfo.periodPositionUs),
|
|
/* contentPositionMs= */ usToMs(periodInfo.periodPositionUs),
|
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
|
/* adIndexInAdGroup= */ C.INDEX_UNSET);
|
|
newSessionPositionInfo =
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
/* isPlayingAd= */ false,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
/* durationMs= */ C.TIME_UNSET,
|
|
/* bufferedPositionMs= */ usToMs(periodInfo.periodPositionUs),
|
|
/* bufferedPercentage= */ 0,
|
|
/* totalBufferedDurationMs= */ 0,
|
|
/* currentLiveOffsetMs= */ C.TIME_UNSET,
|
|
/* contentDurationMs= */ C.TIME_UNSET,
|
|
/* contentBufferedPositionMs= */ usToMs(periodInfo.periodPositionUs));
|
|
}
|
|
PlayerInfo newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
newTimeline,
|
|
newPositionInfo,
|
|
newSessionPositionInfo,
|
|
DISCONTINUITY_REASON_REMOVE);
|
|
|
|
// Mask the playback state.
|
|
int maskingPlaybackState = newPlayerInfo.playbackState;
|
|
if (startIndex != C.INDEX_UNSET && newPlayerInfo.playbackState != STATE_IDLE) {
|
|
if (newTimeline.isEmpty() || correctedStartIndex) {
|
|
// Setting an empty timeline or invalid seek transitions to ended.
|
|
maskingPlaybackState = STATE_ENDED;
|
|
} else {
|
|
maskingPlaybackState = STATE_BUFFERING;
|
|
}
|
|
}
|
|
newPlayerInfo =
|
|
newPlayerInfo.copyWithPlaybackState(maskingPlaybackState, playerInfo.playerError);
|
|
|
|
updatePlayerInfo(
|
|
newPlayerInfo,
|
|
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
/* positionDiscontinuity= */ !playerInfo.timeline.isEmpty(),
|
|
DISCONTINUITY_REASON_REMOVE,
|
|
/* mediaItemTransition= */ !playerInfo.timeline.isEmpty()
|
|
|| !newPlayerInfo.timeline.isEmpty(),
|
|
MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
|
|
}
|
|
|
|
private void moveMediaItemsInternal(int fromIndex, int toIndex, int newIndex) {
|
|
if (fromIndex == 0 && toIndex == playerInfo.timeline.getWindowCount()) {
|
|
return;
|
|
}
|
|
|
|
Timeline oldTimeline = playerInfo.timeline;
|
|
|
|
List<Window> newWindows = new ArrayList<>();
|
|
List<Period> newPeriods = new ArrayList<>();
|
|
|
|
for (int i = 0; i < oldTimeline.getWindowCount(); i++) {
|
|
newWindows.add(oldTimeline.getWindow(i, new Window()));
|
|
}
|
|
Util.moveItems(newWindows, fromIndex, toIndex, newIndex);
|
|
rebuildPeriods(oldTimeline, newWindows, newPeriods);
|
|
Timeline newTimeline = createMaskingTimeline(newWindows, newPeriods);
|
|
|
|
if (!newTimeline.isEmpty()) {
|
|
int oldWindowIndex = getCurrentMediaItemIndex();
|
|
int newWindowIndex = oldWindowIndex;
|
|
if (oldWindowIndex >= fromIndex && oldWindowIndex < toIndex) {
|
|
// if old window index was part of items that should be moved.
|
|
newWindowIndex = (oldWindowIndex - fromIndex) + newIndex;
|
|
} else {
|
|
if (toIndex <= oldWindowIndex && newIndex > oldWindowIndex) {
|
|
// if items were moved from before the old window index to after the old window index.
|
|
newWindowIndex = oldWindowIndex - (toIndex - fromIndex);
|
|
} else if (toIndex > oldWindowIndex && newIndex <= oldWindowIndex) {
|
|
// if items were moved from after the old window index to before the old window index.
|
|
newWindowIndex = oldWindowIndex + (toIndex - fromIndex);
|
|
}
|
|
}
|
|
Window window = new Window();
|
|
int oldPeriodIndex = playerInfo.sessionPositionInfo.positionInfo.periodIndex;
|
|
int deltaFromFirstPeriodIndex =
|
|
oldPeriodIndex - oldTimeline.getWindow(oldWindowIndex, window).firstPeriodIndex;
|
|
int newPeriodIndex =
|
|
newTimeline.getWindow(newWindowIndex, window).firstPeriodIndex
|
|
+ deltaFromFirstPeriodIndex;
|
|
PlayerInfo newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
newTimeline,
|
|
newWindowIndex,
|
|
newPeriodIndex,
|
|
DISCONTINUITY_REASON_INTERNAL);
|
|
|
|
updatePlayerInfo(
|
|
newPlayerInfo,
|
|
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
/* positionDiscontinuity= */ false,
|
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
|
/* mediaItemTransition= */ false,
|
|
/* ignored */ MEDIA_ITEM_TRANSITION_REASON_REPEAT);
|
|
}
|
|
}
|
|
|
|
private void seekToInternalByOffset(long offsetMs) {
|
|
long positionMs = getCurrentPosition() + offsetMs;
|
|
long durationMs = getDuration();
|
|
if (durationMs != C.TIME_UNSET) {
|
|
positionMs = min(positionMs, durationMs);
|
|
}
|
|
positionMs = max(positionMs, 0);
|
|
seekToInternal(getCurrentMediaItemIndex(), positionMs);
|
|
}
|
|
|
|
private void seekToInternal(int windowIndex, long positionMs) {
|
|
Timeline timeline = playerInfo.timeline;
|
|
if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
|
|
throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
|
|
}
|
|
|
|
if (isPlayingAd()) {
|
|
return;
|
|
}
|
|
|
|
@Player.State
|
|
int newPlaybackState =
|
|
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING;
|
|
PlayerInfo newPlayerInfo =
|
|
playerInfo.copyWithPlaybackState(newPlaybackState, playerInfo.playerError);
|
|
@Nullable PeriodInfo periodInfo = getPeriodInfo(timeline, windowIndex, positionMs);
|
|
if (periodInfo == null) {
|
|
// Timeline is empty.
|
|
PositionInfo newPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
windowIndex,
|
|
/* mediaItem= */ null,
|
|
/* periodUid= */ null,
|
|
/* periodIndex= */ 0,
|
|
/* positionMs= */ positionMs == C.TIME_UNSET ? 0 : positionMs,
|
|
/* contentPositionMs= */ positionMs == C.TIME_UNSET ? 0 : positionMs,
|
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
|
/* adIndexInAdGroup= */ C.INDEX_UNSET);
|
|
newPlayerInfo =
|
|
maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
playerInfo.timeline,
|
|
newPositionInfo,
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
playerInfo.sessionPositionInfo.isPlayingAd,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
playerInfo.sessionPositionInfo.durationMs,
|
|
/* bufferedPositionMs= */ positionMs == C.TIME_UNSET ? 0 : positionMs,
|
|
/* bufferedPercentage= */ 0,
|
|
/* totalBufferedDurationMs= */ 0,
|
|
playerInfo.sessionPositionInfo.currentLiveOffsetMs,
|
|
playerInfo.sessionPositionInfo.contentDurationMs,
|
|
/* contentBufferedPositionMs= */ positionMs == C.TIME_UNSET ? 0 : positionMs),
|
|
DISCONTINUITY_REASON_SEEK);
|
|
} else {
|
|
newPlayerInfo = maskPositionInfo(newPlayerInfo, timeline, periodInfo);
|
|
}
|
|
boolean mediaItemTransition =
|
|
!playerInfo.timeline.isEmpty()
|
|
&& newPlayerInfo.sessionPositionInfo.positionInfo.mediaItemIndex
|
|
!= playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex;
|
|
boolean positionDiscontinuity =
|
|
mediaItemTransition
|
|
|| newPlayerInfo.sessionPositionInfo.positionInfo.positionMs
|
|
!= playerInfo.sessionPositionInfo.positionInfo.positionMs;
|
|
if (!positionDiscontinuity) {
|
|
return;
|
|
}
|
|
updatePlayerInfo(
|
|
newPlayerInfo,
|
|
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
positionDiscontinuity,
|
|
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
|
|
mediaItemTransition,
|
|
MEDIA_ITEM_TRANSITION_REASON_SEEK);
|
|
}
|
|
|
|
private void setPlayWhenReady(
|
|
boolean playWhenReady,
|
|
@Player.PlaybackSuppressionReason int playbackSuppressionReason,
|
|
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
|
|
if (playerInfo.playWhenReady == playWhenReady
|
|
&& playerInfo.playbackSuppressionReason == playbackSuppressionReason) {
|
|
return;
|
|
}
|
|
|
|
// Stop estimating content position until a new positionInfo arrives from the player
|
|
lastSetPlayWhenReadyCalledTimeMs = SystemClock.elapsedRealtime();
|
|
PlayerInfo playerInfo =
|
|
this.playerInfo.copyWithPlayWhenReady(
|
|
playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
|
|
updatePlayerInfo(
|
|
playerInfo,
|
|
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
|
playWhenReadyChangeReason,
|
|
/* positionDiscontinuity= */ false,
|
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
|
/* mediaItemTransition= */ false,
|
|
/* ignored */ MEDIA_ITEM_TRANSITION_REASON_REPEAT);
|
|
}
|
|
|
|
private void updatePlayerInfo(
|
|
PlayerInfo newPlayerInfo,
|
|
@TimelineChangeReason int timelineChangeReason,
|
|
@PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
|
boolean positionDiscontinuity,
|
|
@DiscontinuityReason int positionDiscontinuityReason,
|
|
boolean mediaItemTransition,
|
|
@MediaItemTransitionReason int mediaItemTransitionReason) {
|
|
// Assign player info immediately such that all getters return the right values, but keep
|
|
// snapshot of previous and new states so that listener invocations are triggered correctly.
|
|
PlayerInfo oldPlayerInfo = this.playerInfo;
|
|
this.playerInfo = newPlayerInfo;
|
|
|
|
if (mediaItemTransition) {
|
|
listeners.queueEvent(
|
|
EVENT_MEDIA_ITEM_TRANSITION,
|
|
listener ->
|
|
listener.onMediaItemTransition(
|
|
newPlayerInfo.getCurrentMediaItem(), mediaItemTransitionReason));
|
|
}
|
|
if (positionDiscontinuity) {
|
|
listeners.queueEvent(
|
|
EVENT_POSITION_DISCONTINUITY,
|
|
listener ->
|
|
listener.onPositionDiscontinuity(
|
|
newPlayerInfo.oldPositionInfo,
|
|
newPlayerInfo.newPositionInfo,
|
|
positionDiscontinuityReason));
|
|
}
|
|
if (!oldPlayerInfo.timeline.equals(newPlayerInfo.timeline)) {
|
|
listeners.queueEvent(
|
|
Player.EVENT_TIMELINE_CHANGED,
|
|
listener -> listener.onTimelineChanged(newPlayerInfo.timeline, timelineChangeReason));
|
|
}
|
|
if (oldPlayerInfo.playbackState != newPlayerInfo.playbackState) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_STATE_CHANGED,
|
|
listener -> listener.onPlaybackStateChanged(newPlayerInfo.playbackState));
|
|
}
|
|
if (oldPlayerInfo.playWhenReady != newPlayerInfo.playWhenReady) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAY_WHEN_READY_CHANGED,
|
|
listener ->
|
|
listener.onPlayWhenReadyChanged(
|
|
newPlayerInfo.playWhenReady, playWhenReadyChangeReason));
|
|
}
|
|
if (oldPlayerInfo.playbackSuppressionReason != newPlayerInfo.playbackSuppressionReason) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
|
|
listener ->
|
|
listener.onPlaybackSuppressionReasonChanged(newPlayerInfo.playbackSuppressionReason));
|
|
}
|
|
if (oldPlayerInfo.isPlaying != newPlayerInfo.isPlaying) {
|
|
listeners.queueEvent(
|
|
EVENT_IS_PLAYING_CHANGED,
|
|
listener -> listener.onIsPlayingChanged(newPlayerInfo.isPlaying));
|
|
}
|
|
listeners.flushEvents();
|
|
}
|
|
|
|
private boolean requestConnectToService() {
|
|
int flags =
|
|
Util.SDK_INT >= 29
|
|
? Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES
|
|
: Context.BIND_AUTO_CREATE;
|
|
|
|
// Service. Needs to get fresh binder whenever connection is needed.
|
|
Intent intent = new Intent(MediaSessionService.SERVICE_INTERFACE);
|
|
intent.setClassName(token.getPackageName(), token.getServiceName());
|
|
|
|
// Use bindService() instead of startForegroundService() to start session service for three
|
|
// reasons.
|
|
// 1. Prevent session service owner's stopSelf() from destroying service.
|
|
// With the startForegroundService(), service's call of stopSelf() will trigger immediate
|
|
// onDestroy() calls on the main thread even when onConnect() is running in another
|
|
// thread.
|
|
// 2. Minimize APIs for developers to take care about.
|
|
// With bindService(), developers only need to take care about Service.onBind()
|
|
// but Service.onStartCommand() should be also taken care about with the
|
|
// startForegroundService().
|
|
// 3. Future support for UI-less playback
|
|
// If a service wants to keep running, it should be either foreground service or
|
|
// bound service. But there had been request for the feature for system apps
|
|
// and using bindService() will be better fit with it.
|
|
boolean result = context.bindService(intent, serviceConnection, flags);
|
|
if (!result) {
|
|
Log.w(TAG, "bind to " + token + " failed");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean requestConnectToSession(Bundle connectionHints) {
|
|
IMediaSession iSession =
|
|
IMediaSession.Stub.asInterface((IBinder) checkStateNotNull(token.getBinder()));
|
|
int seq = sequencedFutureManager.obtainNextSequenceNumber();
|
|
ConnectionRequest request =
|
|
new ConnectionRequest(context.getPackageName(), Process.myPid(), connectionHints);
|
|
try {
|
|
iSession.connect(controllerStub, seq, request.toBundle());
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Failed to call connection request.", e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(@Nullable Surface surface) {
|
|
Future<SessionResult> future =
|
|
dispatchRemoteSessionTaskWithPlayerCommand(
|
|
COMMAND_SET_VIDEO_SURFACE,
|
|
(iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, surface));
|
|
|
|
try {
|
|
MediaUtils.getFutureResult(future, /* timeoutMs= */ 3_000);
|
|
} catch (ExecutionException e) {
|
|
// Never happens because future.setException will not be called.
|
|
throw new IllegalStateException(e);
|
|
} catch (TimeoutException e) {
|
|
Log.w(TAG, "set/clearVideoSurface takes too long on the session side.", e);
|
|
// TODO(b/188888693): Let developers know the failure in their code.
|
|
}
|
|
}
|
|
|
|
private void clearSurfacesAndCallbacks() {
|
|
if (videoTextureView != null) {
|
|
videoTextureView.setSurfaceTextureListener(null);
|
|
videoTextureView = null;
|
|
}
|
|
if (videoSurfaceHolder != null) {
|
|
videoSurfaceHolder.removeCallback(surfaceCallback);
|
|
videoSurfaceHolder = null;
|
|
}
|
|
if (videoSurface != null) {
|
|
videoSurface = null;
|
|
}
|
|
}
|
|
|
|
private void maybeNotifySurfaceSizeChanged(int width, int height) {
|
|
if (surfaceWidth != width || surfaceHeight != height) {
|
|
surfaceWidth = width;
|
|
surfaceHeight = height;
|
|
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.sendEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSurfaceSizeChanged(width, height));
|
|
}
|
|
}
|
|
|
|
/** Returns session interface if the controller can send the predefined command. */
|
|
@Nullable
|
|
IMediaSession getSessionInterfaceWithSessionCommandIfAble(@CommandCode int commandCode) {
|
|
checkArgument(commandCode != COMMAND_CODE_CUSTOM);
|
|
if (!sessionCommands.contains(commandCode)) {
|
|
Log.w(TAG, "Controller isn't allowed to call command, commandCode=" + commandCode);
|
|
return null;
|
|
}
|
|
return iSession;
|
|
}
|
|
|
|
/** Returns session interface if the controller can send the custom command. */
|
|
@Nullable
|
|
IMediaSession getSessionInterfaceWithSessionCommandIfAble(SessionCommand command) {
|
|
checkArgument(command.commandCode == COMMAND_CODE_CUSTOM);
|
|
if (!sessionCommands.contains(command)) {
|
|
Log.w(TAG, "Controller isn't allowed to call custom session command:" + command.customAction);
|
|
return null;
|
|
}
|
|
return iSession;
|
|
}
|
|
|
|
void notifyPeriodicSessionPositionInfoChanged(SessionPositionInfo sessionPositionInfo) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
updateSessionPositionInfoIfNeeded(sessionPositionInfo);
|
|
}
|
|
|
|
<T extends @NonNull Object> void setFutureResult(int seq, T futureResult) {
|
|
sequencedFutureManager.setFutureResult(seq, futureResult);
|
|
}
|
|
|
|
void onConnected(ConnectionState result) {
|
|
if (iSession != null) {
|
|
Log.e(
|
|
TAG,
|
|
"Cannot be notified about the connection result many times."
|
|
+ " Probably a bug or malicious app.");
|
|
getInstance().release();
|
|
return;
|
|
}
|
|
iSession = result.sessionBinder;
|
|
sessionActivity = result.sessionActivity;
|
|
sessionCommands = result.sessionCommands;
|
|
playerCommandsFromSession = result.playerCommandsFromSession;
|
|
playerCommandsFromPlayer = result.playerCommandsFromPlayer;
|
|
intersectedPlayerCommands = intersect(playerCommandsFromSession, playerCommandsFromPlayer);
|
|
playerInfo = result.playerInfo;
|
|
try {
|
|
// Implementation for the local binder is no-op,
|
|
// so can be used without worrying about deadlock.
|
|
result.sessionBinder.asBinder().linkToDeath(deathRecipient, 0);
|
|
} catch (RemoteException e) {
|
|
getInstance().release();
|
|
return;
|
|
}
|
|
connectedToken =
|
|
new SessionToken(
|
|
token.getUid(),
|
|
TYPE_SESSION,
|
|
result.libraryVersion,
|
|
result.sessionInterfaceVersion,
|
|
token.getPackageName(),
|
|
result.sessionBinder,
|
|
result.tokenExtras);
|
|
getInstance().notifyAccepted();
|
|
}
|
|
|
|
private void sendControllerResult(int seq, SessionResult result) {
|
|
IMediaSession iSession = this.iSession;
|
|
if (iSession == null) {
|
|
return;
|
|
}
|
|
try {
|
|
iSession.onControllerResult(controllerStub, seq, result.toBundle());
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Error in sending");
|
|
}
|
|
}
|
|
|
|
private void sendControllerResultWhenReady(int seq, ListenableFuture<SessionResult> future) {
|
|
future.addListener(
|
|
() -> {
|
|
SessionResult result;
|
|
try {
|
|
result = checkNotNull(future.get(), "SessionResult must not be null");
|
|
} catch (CancellationException unused) {
|
|
result = new SessionResult(RESULT_INFO_SKIPPED);
|
|
} catch (ExecutionException | InterruptedException unused) {
|
|
result = new SessionResult(RESULT_ERROR_UNKNOWN);
|
|
}
|
|
sendControllerResult(seq, result);
|
|
},
|
|
MoreExecutors.directExecutor());
|
|
}
|
|
|
|
void onCustomCommand(int seq, SessionCommand command, Bundle args) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
getInstance()
|
|
.notifyControllerListener(
|
|
listener -> {
|
|
ListenableFuture<SessionResult> future =
|
|
checkNotNull(
|
|
listener.onCustomCommand(getInstance(), command, args),
|
|
"ControllerCallback#onCustomCommand() must not return null");
|
|
sendControllerResultWhenReady(seq, future);
|
|
});
|
|
}
|
|
|
|
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
|
|
void onPlayerInfoChanged(
|
|
PlayerInfo newPlayerInfo,
|
|
@TimelineChangeReason int timelineChangedReason,
|
|
boolean isTimelineExcluded) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
PlayerInfo oldPlayerInfo = playerInfo;
|
|
playerInfo = newPlayerInfo;
|
|
if (isTimelineExcluded) {
|
|
playerInfo = playerInfo.copyWithTimeline(oldPlayerInfo.timeline);
|
|
}
|
|
PlaybackException oldPlayerError = oldPlayerInfo.playerError;
|
|
PlaybackException playerError = playerInfo.playerError;
|
|
boolean errorsMatch =
|
|
oldPlayerError == playerError
|
|
|| (oldPlayerError != null && oldPlayerError.errorInfoEquals(playerError));
|
|
if (!errorsMatch) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYER_ERROR, listener -> listener.onPlayerErrorChanged(playerInfo.playerError));
|
|
if (playerInfo.playerError != null) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYER_ERROR, listener -> listener.onPlayerError(playerInfo.playerError));
|
|
}
|
|
}
|
|
MediaItem oldCurrentMediaItem = oldPlayerInfo.getCurrentMediaItem();
|
|
MediaItem currentMediaItem = playerInfo.getCurrentMediaItem();
|
|
if (!Util.areEqual(oldCurrentMediaItem, currentMediaItem)) {
|
|
listeners.queueEvent(
|
|
EVENT_MEDIA_ITEM_TRANSITION,
|
|
listener ->
|
|
listener.onMediaItemTransition(
|
|
currentMediaItem, playerInfo.mediaItemTransitionReason));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.currentTracks, playerInfo.currentTracks)) {
|
|
listeners.queueEvent(
|
|
EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(playerInfo.currentTracks));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.playbackParameters, playerInfo.playbackParameters)) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_PARAMETERS_CHANGED,
|
|
listener -> listener.onPlaybackParametersChanged(playerInfo.playbackParameters));
|
|
}
|
|
if (oldPlayerInfo.repeatMode != playerInfo.repeatMode) {
|
|
listeners.queueEvent(
|
|
EVENT_REPEAT_MODE_CHANGED,
|
|
listener -> listener.onRepeatModeChanged(playerInfo.repeatMode));
|
|
}
|
|
if (oldPlayerInfo.shuffleModeEnabled != playerInfo.shuffleModeEnabled) {
|
|
listeners.queueEvent(
|
|
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
|
|
listener -> listener.onShuffleModeEnabledChanged(playerInfo.shuffleModeEnabled));
|
|
}
|
|
if (!isTimelineExcluded && !Util.areEqual(oldPlayerInfo.timeline, playerInfo.timeline)) {
|
|
listeners.queueEvent(
|
|
EVENT_TIMELINE_CHANGED,
|
|
listener -> listener.onTimelineChanged(playerInfo.timeline, timelineChangedReason));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.playlistMetadata, playerInfo.playlistMetadata)) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onPlaylistMetadataChanged(playerInfo.playlistMetadata));
|
|
}
|
|
if (oldPlayerInfo.volume != playerInfo.volume) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onVolumeChanged(playerInfo.volume));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.audioAttributes, playerInfo.audioAttributes)) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onAudioAttributesChanged(playerInfo.audioAttributes));
|
|
}
|
|
if (!oldPlayerInfo.cueGroup.cues.equals(playerInfo.cueGroup.cues)) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onCues(playerInfo.cueGroup.cues));
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onCues(playerInfo.cueGroup));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.deviceInfo, playerInfo.deviceInfo)) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onDeviceInfoChanged(playerInfo.deviceInfo));
|
|
}
|
|
if (oldPlayerInfo.deviceVolume != playerInfo.deviceVolume
|
|
|| oldPlayerInfo.deviceMuted != playerInfo.deviceMuted) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener ->
|
|
listener.onDeviceVolumeChanged(playerInfo.deviceVolume, playerInfo.deviceMuted));
|
|
}
|
|
if (oldPlayerInfo.playWhenReady != playerInfo.playWhenReady) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAY_WHEN_READY_CHANGED,
|
|
listener ->
|
|
listener.onPlayWhenReadyChanged(
|
|
playerInfo.playWhenReady, playerInfo.playWhenReadyChangedReason));
|
|
}
|
|
if (oldPlayerInfo.playbackSuppressionReason != playerInfo.playbackSuppressionReason) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
|
|
listener ->
|
|
listener.onPlaybackSuppressionReasonChanged(playerInfo.playbackSuppressionReason));
|
|
}
|
|
if (oldPlayerInfo.playbackState != playerInfo.playbackState) {
|
|
listeners.queueEvent(
|
|
EVENT_PLAYBACK_STATE_CHANGED,
|
|
listener -> listener.onPlaybackStateChanged(playerInfo.playbackState));
|
|
}
|
|
if (oldPlayerInfo.isPlaying != playerInfo.isPlaying) {
|
|
listeners.queueEvent(
|
|
EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(playerInfo.isPlaying));
|
|
}
|
|
if (oldPlayerInfo.isLoading != playerInfo.isLoading) {
|
|
listeners.queueEvent(
|
|
EVENT_IS_LOADING_CHANGED, listener -> listener.onIsLoadingChanged(playerInfo.isLoading));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.videoSize, playerInfo.videoSize)) {
|
|
// TODO(b/187152483): Set proper event code when available.
|
|
listeners.queueEvent(
|
|
/* eventFlag= */ C.INDEX_UNSET,
|
|
listener -> listener.onVideoSizeChanged(playerInfo.videoSize));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.oldPositionInfo, playerInfo.oldPositionInfo)
|
|
|| !Util.areEqual(oldPlayerInfo.newPositionInfo, playerInfo.newPositionInfo)) {
|
|
listeners.queueEvent(
|
|
EVENT_POSITION_DISCONTINUITY,
|
|
listener ->
|
|
listener.onPositionDiscontinuity(
|
|
playerInfo.oldPositionInfo,
|
|
playerInfo.newPositionInfo,
|
|
playerInfo.discontinuityReason));
|
|
}
|
|
if (!Util.areEqual(oldPlayerInfo.mediaMetadata, playerInfo.mediaMetadata)) {
|
|
listeners.queueEvent(
|
|
Player.EVENT_MEDIA_METADATA_CHANGED,
|
|
listener -> listener.onMediaMetadataChanged(playerInfo.mediaMetadata));
|
|
}
|
|
if (oldPlayerInfo.seekBackIncrementMs != playerInfo.seekBackIncrementMs) {
|
|
listeners.queueEvent(
|
|
EVENT_SEEK_BACK_INCREMENT_CHANGED,
|
|
listener -> listener.onSeekBackIncrementChanged(playerInfo.seekBackIncrementMs));
|
|
}
|
|
if (oldPlayerInfo.seekForwardIncrementMs != playerInfo.seekForwardIncrementMs) {
|
|
listeners.queueEvent(
|
|
EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
|
|
listener -> listener.onSeekForwardIncrementChanged(playerInfo.seekForwardIncrementMs));
|
|
}
|
|
if (oldPlayerInfo.maxSeekToPreviousPositionMs != newPlayerInfo.maxSeekToPreviousPositionMs) {
|
|
listeners.queueEvent(
|
|
EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED,
|
|
listener ->
|
|
listener.onMaxSeekToPreviousPositionChanged(
|
|
newPlayerInfo.maxSeekToPreviousPositionMs));
|
|
}
|
|
if (!Util.areEqual(
|
|
oldPlayerInfo.trackSelectionParameters, newPlayerInfo.trackSelectionParameters)) {
|
|
listeners.queueEvent(
|
|
EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
|
|
listener ->
|
|
listener.onTrackSelectionParametersChanged(newPlayerInfo.trackSelectionParameters));
|
|
}
|
|
listeners.flushEvents();
|
|
}
|
|
|
|
void onAvailableCommandsChangedFromSession(
|
|
SessionCommands sessionCommands, Commands playerCommands) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
boolean playerCommandsChanged = !Util.areEqual(playerCommandsFromSession, playerCommands);
|
|
boolean sessionCommandsChanged = !Util.areEqual(this.sessionCommands, sessionCommands);
|
|
if (!playerCommandsChanged && !sessionCommandsChanged) {
|
|
return;
|
|
}
|
|
boolean intersectedPlayerCommandsChanged = false;
|
|
if (playerCommandsChanged) {
|
|
playerCommandsFromSession = playerCommands;
|
|
Commands prevIntersectedPlayerCommands = intersectedPlayerCommands;
|
|
intersectedPlayerCommands = intersect(playerCommandsFromSession, playerCommandsFromPlayer);
|
|
intersectedPlayerCommandsChanged =
|
|
!Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands);
|
|
}
|
|
if (sessionCommandsChanged) {
|
|
this.sessionCommands = sessionCommands;
|
|
}
|
|
if (intersectedPlayerCommandsChanged) {
|
|
listeners.sendEvent(
|
|
EVENT_AVAILABLE_COMMANDS_CHANGED,
|
|
listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands));
|
|
}
|
|
if (sessionCommandsChanged) {
|
|
getInstance()
|
|
.notifyControllerListener(
|
|
listener ->
|
|
listener.onAvailableSessionCommandsChanged(getInstance(), sessionCommands));
|
|
}
|
|
}
|
|
|
|
void onAvailableCommandsChangedFromPlayer(Commands commandsFromPlayer) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
if (Util.areEqual(playerCommandsFromPlayer, commandsFromPlayer)) {
|
|
return;
|
|
}
|
|
playerCommandsFromPlayer = commandsFromPlayer;
|
|
Commands prevIntersectedPlayerCommands = intersectedPlayerCommands;
|
|
intersectedPlayerCommands = intersect(playerCommandsFromSession, playerCommandsFromPlayer);
|
|
boolean intersectedPlayerCommandsChanged =
|
|
!Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands);
|
|
if (intersectedPlayerCommandsChanged) {
|
|
listeners.sendEvent(
|
|
EVENT_AVAILABLE_COMMANDS_CHANGED,
|
|
listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands));
|
|
}
|
|
}
|
|
|
|
void onSetCustomLayout(int seq, List<CommandButton> layout) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
List<CommandButton> validatedCustomLayout = new ArrayList<>();
|
|
for (int i = 0; i < layout.size(); i++) {
|
|
CommandButton button = layout.get(i);
|
|
if (intersectedPlayerCommands.contains(button.playerCommand)
|
|
|| (button.sessionCommand != null && sessionCommands.contains(button.sessionCommand))
|
|
|| (button.playerCommand != Player.COMMAND_INVALID
|
|
&& sessionCommands.contains(button.playerCommand))) {
|
|
validatedCustomLayout.add(button);
|
|
}
|
|
}
|
|
getInstance()
|
|
.notifyControllerListener(
|
|
listener -> {
|
|
ListenableFuture<SessionResult> future =
|
|
checkNotNull(
|
|
listener.onSetCustomLayout(getInstance(), validatedCustomLayout),
|
|
"MediaController.Listener#onSetCustomLayout() must not return null");
|
|
sendControllerResultWhenReady(seq, future);
|
|
});
|
|
}
|
|
|
|
public void onExtrasChanged(Bundle extras) {
|
|
if (!isConnected()) {
|
|
return;
|
|
}
|
|
getInstance()
|
|
.notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras));
|
|
}
|
|
|
|
public void onRenderedFirstFrame() {
|
|
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onRenderedFirstFrame);
|
|
}
|
|
|
|
private void updateSessionPositionInfoIfNeeded(SessionPositionInfo sessionPositionInfo) {
|
|
if (playerInfo.sessionPositionInfo.eventTimeMs < sessionPositionInfo.eventTimeMs) {
|
|
playerInfo = playerInfo.copyWithSessionPositionInfo(sessionPositionInfo);
|
|
}
|
|
}
|
|
|
|
@RepeatMode
|
|
private static int convertRepeatModeForNavigation(@RepeatMode int repeatMode) {
|
|
return repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_OFF : repeatMode;
|
|
}
|
|
|
|
private boolean isPlayerCommandAvailable(@Player.Command int command) {
|
|
if (!intersectedPlayerCommands.contains(command)) {
|
|
Log.w(TAG, "Controller isn't allowed to call command= " + command);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private PlayerInfo maskPositionInfo(
|
|
PlayerInfo playerInfo, Timeline timeline, PeriodInfo periodInfo) {
|
|
int oldPeriodIndex = playerInfo.sessionPositionInfo.positionInfo.periodIndex;
|
|
int newPeriodIndex = periodInfo.index;
|
|
Period oldPeriod = new Period();
|
|
timeline.getPeriod(oldPeriodIndex, oldPeriod);
|
|
Period newPeriod = new Period();
|
|
timeline.getPeriod(newPeriodIndex, newPeriod);
|
|
boolean playingPeriodChanged = oldPeriodIndex != newPeriodIndex;
|
|
long newPositionUs = periodInfo.periodPositionUs;
|
|
long oldPositionUs = Util.msToUs(getCurrentPosition()) - oldPeriod.getPositionInWindowUs();
|
|
|
|
if (!playingPeriodChanged && newPositionUs == oldPositionUs) {
|
|
// Period position remains unchanged.
|
|
return playerInfo;
|
|
}
|
|
|
|
checkState(!isAd(newPeriod, newPositionUs));
|
|
|
|
PositionInfo oldPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
oldPeriod.windowIndex,
|
|
playerInfo.sessionPositionInfo.positionInfo.mediaItem,
|
|
/* periodUid= */ null,
|
|
oldPeriodIndex,
|
|
/* positionMs= */ usToMs(oldPeriod.positionInWindowUs + oldPositionUs),
|
|
/* contentPositionMs= */ usToMs(oldPeriod.positionInWindowUs + oldPositionUs),
|
|
playerInfo.sessionPositionInfo.positionInfo.adGroupIndex,
|
|
playerInfo.sessionPositionInfo.positionInfo.adIndexInAdGroup);
|
|
|
|
timeline.getPeriod(newPeriodIndex, newPeriod);
|
|
Window newWindow = new Window();
|
|
timeline.getWindow(newPeriod.windowIndex, newWindow);
|
|
PositionInfo newPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
newPeriod.windowIndex,
|
|
newWindow.mediaItem,
|
|
/* periodUid= */ null,
|
|
newPeriodIndex,
|
|
/* positionMs= */ usToMs(newPeriod.positionInWindowUs + newPositionUs),
|
|
/* contentPositionMs= */ usToMs(newPeriod.positionInWindowUs + newPositionUs),
|
|
playerInfo.sessionPositionInfo.positionInfo.adGroupIndex,
|
|
playerInfo.sessionPositionInfo.positionInfo.adIndexInAdGroup);
|
|
playerInfo =
|
|
playerInfo.copyWithPositionInfos(
|
|
oldPositionInfo, newPositionInfo, DISCONTINUITY_REASON_SEEK);
|
|
|
|
if (playingPeriodChanged || newPositionUs < oldPositionUs) {
|
|
// The playing period changes or a backwards seek within the playing period occurs.
|
|
playerInfo =
|
|
playerInfo.copyWithSessionPositionInfo(
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
/* isPlayingAd= */ false,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
newWindow.getDurationMs(),
|
|
/* bufferedPositionMs= */ usToMs(newPeriod.positionInWindowUs + newPositionUs),
|
|
/* bufferedPercentage= */ calculateBufferedPercentage(
|
|
/* bufferedPositionMs= */ usToMs(
|
|
newPeriod.positionInWindowUs + newPositionUs),
|
|
newWindow.getDurationMs()),
|
|
/* totalBufferedDurationMs= */ 0,
|
|
/* currentLiveOffsetMs= */ C.TIME_UNSET,
|
|
/* contentDurationMs= */ C.TIME_UNSET,
|
|
/* contentBufferedPositionMs= */ usToMs(
|
|
newPeriod.positionInWindowUs + newPositionUs)));
|
|
} else {
|
|
// A forward seek within the playing period (timeline did not change).
|
|
long maskedTotalBufferedDurationUs =
|
|
max(
|
|
0,
|
|
Util.msToUs(playerInfo.sessionPositionInfo.totalBufferedDurationMs)
|
|
- (newPositionUs - oldPositionUs));
|
|
long maskedBufferedPositionUs = newPositionUs + maskedTotalBufferedDurationUs;
|
|
|
|
playerInfo =
|
|
playerInfo.copyWithSessionPositionInfo(
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
/* isPlayingAd= */ false,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
newWindow.getDurationMs(),
|
|
/* bufferedPositionMs= */ usToMs(maskedBufferedPositionUs),
|
|
/* bufferedPercentage= */ calculateBufferedPercentage(
|
|
usToMs(maskedBufferedPositionUs), newWindow.getDurationMs()),
|
|
/* totalBufferedDurationMs= */ usToMs(maskedTotalBufferedDurationUs),
|
|
/* currentLiveOffsetMs= */ C.TIME_UNSET,
|
|
/* contentDurationMs= */ C.TIME_UNSET,
|
|
/* contentBufferedPositionMs= */ usToMs(maskedBufferedPositionUs)));
|
|
}
|
|
return playerInfo;
|
|
}
|
|
|
|
@Nullable
|
|
private PeriodInfo getPeriodInfo(Timeline timeline, int windowIndex, long windowPositionMs) {
|
|
if (timeline.isEmpty()) {
|
|
return null;
|
|
}
|
|
Window window = new Window();
|
|
Period period = new Period();
|
|
if (windowIndex == C.INDEX_UNSET || windowIndex >= timeline.getWindowCount()) {
|
|
// Use default position of timeline if window index still unset or if a previous initial seek
|
|
// now turns out to be invalid.
|
|
windowIndex = timeline.getFirstWindowIndex(getShuffleModeEnabled());
|
|
windowPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs();
|
|
}
|
|
return getPeriodInfo(timeline, window, period, windowIndex, Util.msToUs(windowPositionMs));
|
|
}
|
|
|
|
private boolean isAd(Period period, long periodPosition) {
|
|
return period.getAdGroupIndexForPositionUs(periodPosition) != C.INDEX_UNSET;
|
|
}
|
|
|
|
@Nullable
|
|
private PeriodInfo getPeriodInfo(
|
|
Timeline timeline, Window window, Period period, int windowIndex, long windowPositionUs) {
|
|
checkIndex(windowIndex, 0, timeline.getWindowCount());
|
|
timeline.getWindow(windowIndex, window);
|
|
if (windowPositionUs == C.TIME_UNSET) {
|
|
windowPositionUs = window.getDefaultPositionUs();
|
|
if (windowPositionUs == C.TIME_UNSET) {
|
|
return null;
|
|
}
|
|
}
|
|
int periodIndex = window.firstPeriodIndex;
|
|
timeline.getPeriod(periodIndex, period);
|
|
while (periodIndex < window.lastPeriodIndex
|
|
&& period.positionInWindowUs != windowPositionUs
|
|
&& timeline.getPeriod(periodIndex + 1, period).positionInWindowUs <= windowPositionUs) {
|
|
periodIndex++;
|
|
}
|
|
timeline.getPeriod(periodIndex, period);
|
|
long periodPositionUs = windowPositionUs - period.positionInWindowUs;
|
|
return new PeriodInfo(periodIndex, periodPositionUs);
|
|
}
|
|
|
|
private PlayerInfo maskTimelineAndPositionInfo(
|
|
PlayerInfo playerInfo,
|
|
Timeline timeline,
|
|
int newMediaItemIndex,
|
|
int newPeriodIndex,
|
|
int discontinuityReason) {
|
|
PositionInfo newPositionInfo =
|
|
new PositionInfo(
|
|
/* windowUid= */ null,
|
|
newMediaItemIndex,
|
|
timeline.getWindow(newMediaItemIndex, new Window()).mediaItem,
|
|
/* periodUid= */ null,
|
|
newPeriodIndex,
|
|
playerInfo.sessionPositionInfo.positionInfo.positionMs,
|
|
playerInfo.sessionPositionInfo.positionInfo.contentPositionMs,
|
|
playerInfo.sessionPositionInfo.positionInfo.adGroupIndex,
|
|
playerInfo.sessionPositionInfo.positionInfo.adIndexInAdGroup);
|
|
return maskTimelineAndPositionInfo(
|
|
playerInfo,
|
|
timeline,
|
|
newPositionInfo,
|
|
new SessionPositionInfo(
|
|
newPositionInfo,
|
|
playerInfo.sessionPositionInfo.isPlayingAd,
|
|
/* eventTimeMs= */ SystemClock.elapsedRealtime(),
|
|
playerInfo.sessionPositionInfo.durationMs,
|
|
playerInfo.sessionPositionInfo.bufferedPositionMs,
|
|
playerInfo.sessionPositionInfo.bufferedPercentage,
|
|
playerInfo.sessionPositionInfo.totalBufferedDurationMs,
|
|
playerInfo.sessionPositionInfo.currentLiveOffsetMs,
|
|
playerInfo.sessionPositionInfo.contentDurationMs,
|
|
playerInfo.sessionPositionInfo.contentBufferedPositionMs),
|
|
discontinuityReason);
|
|
}
|
|
|
|
private PlayerInfo maskTimelineAndPositionInfo(
|
|
PlayerInfo playerInfo,
|
|
Timeline timeline,
|
|
PositionInfo newPositionInfo,
|
|
SessionPositionInfo newSessionPositionInfo,
|
|
int discontinuityReason) {
|
|
playerInfo =
|
|
new PlayerInfo.Builder(playerInfo)
|
|
.setTimeline(timeline)
|
|
.setOldPositionInfo(playerInfo.sessionPositionInfo.positionInfo)
|
|
.setNewPositionInfo(newPositionInfo)
|
|
.setSessionPositionInfo(newSessionPositionInfo)
|
|
.setDiscontinuityReason(discontinuityReason)
|
|
.build();
|
|
return playerInfo;
|
|
}
|
|
|
|
private Period getPeriodWithNewWindowIndex(Timeline timeline, int periodIndex, int windowIndex) {
|
|
Period period = new Period();
|
|
timeline.getPeriod(periodIndex, period);
|
|
period.windowIndex = windowIndex;
|
|
return period;
|
|
}
|
|
|
|
private int getNewPeriodIndexWithoutRemovedPeriods(
|
|
Timeline timeline, int oldPeriodIndex, int fromIndex, int toIndex) {
|
|
if (oldPeriodIndex == C.INDEX_UNSET) {
|
|
return oldPeriodIndex;
|
|
}
|
|
int newPeriodIndex = oldPeriodIndex;
|
|
for (int i = fromIndex; i < toIndex; i++) {
|
|
Window window = new Window();
|
|
timeline.getWindow(i, window);
|
|
int size = window.lastPeriodIndex - window.firstPeriodIndex + 1;
|
|
newPeriodIndex -= size;
|
|
}
|
|
return newPeriodIndex;
|
|
}
|
|
|
|
private static Window createNewWindow(MediaItem mediaItem) {
|
|
return new Window()
|
|
.set(
|
|
/* uid= */ 0,
|
|
mediaItem,
|
|
/* manifest= */ null,
|
|
/* presentationStartTimeMs= */ 0,
|
|
/* windowStartTimeMs= */ 0,
|
|
/* elapsedRealtimeEpochOffsetMs= */ 0,
|
|
/* isSeekable= */ true,
|
|
/* isDynamic= */ false,
|
|
/* liveConfiguration= */ null,
|
|
/* defaultPositionUs= */ 0,
|
|
/* durationUs= */ C.TIME_UNSET,
|
|
/* firstPeriodIndex= */ C.INDEX_UNSET,
|
|
/* lastPeriodIndex= */ C.INDEX_UNSET,
|
|
/* positionInFirstPeriodUs= */ 0);
|
|
}
|
|
|
|
private static Period createNewPeriod(int windowIndex) {
|
|
return new Period()
|
|
.set(
|
|
/* id= */ null,
|
|
/* uid= */ null,
|
|
windowIndex,
|
|
/* durationUs= */ C.TIME_UNSET,
|
|
/* positionInWindowUs= */ 0,
|
|
/* adPlaybackState= */ AdPlaybackState.NONE,
|
|
/* isPlaceholder= */ true);
|
|
}
|
|
|
|
private void rebuildPeriods(
|
|
Timeline oldTimeline, List<Window> newWindows, List<Period> newPeriods) {
|
|
for (int i = 0; i < newWindows.size(); i++) {
|
|
Window window = newWindows.get(i);
|
|
int firstPeriodIndex = window.firstPeriodIndex;
|
|
int lastPeriodIndex = window.lastPeriodIndex;
|
|
if (firstPeriodIndex == C.INDEX_UNSET || lastPeriodIndex == C.INDEX_UNSET) {
|
|
window.firstPeriodIndex = newPeriods.size();
|
|
window.lastPeriodIndex = newPeriods.size();
|
|
newPeriods.add(createNewPeriod(i));
|
|
} else {
|
|
window.firstPeriodIndex = newPeriods.size();
|
|
window.lastPeriodIndex = newPeriods.size() + (lastPeriodIndex - firstPeriodIndex);
|
|
for (int j = firstPeriodIndex; j <= lastPeriodIndex; j++) {
|
|
newPeriods.add(
|
|
getPeriodWithNewWindowIndex(oldTimeline, /* periodIndex= */ j, /* windowIndex= */ i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static int resolveSubsequentMediaItemIndex(
|
|
@RepeatMode int repeatMode,
|
|
boolean shuffleModeEnabled,
|
|
int oldMediaItemIndex,
|
|
Timeline oldTimeline,
|
|
int fromIndex,
|
|
int toIndex) {
|
|
int newMediaItemIndex = C.INDEX_UNSET;
|
|
int maxIterations = oldTimeline.getWindowCount();
|
|
for (int i = 0; i < maxIterations; i++) {
|
|
oldMediaItemIndex =
|
|
oldTimeline.getNextWindowIndex(oldMediaItemIndex, repeatMode, shuffleModeEnabled);
|
|
if (oldMediaItemIndex == C.INDEX_UNSET) {
|
|
// We've reached the end of the old timeline.
|
|
break;
|
|
}
|
|
if (oldMediaItemIndex < fromIndex || oldMediaItemIndex >= toIndex) {
|
|
newMediaItemIndex = oldMediaItemIndex;
|
|
break;
|
|
}
|
|
}
|
|
return newMediaItemIndex;
|
|
}
|
|
|
|
// This will be called on the main thread.
|
|
private class SessionServiceConnection implements ServiceConnection {
|
|
|
|
private final Bundle connectionHints;
|
|
|
|
public SessionServiceConnection(Bundle connectionHints) {
|
|
this.connectionHints = connectionHints;
|
|
}
|
|
|
|
@Override
|
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
boolean connectionRequested = false;
|
|
try {
|
|
// Note that it's always main-thread.
|
|
if (!token.getPackageName().equals(name.getPackageName())) {
|
|
Log.e(
|
|
TAG,
|
|
"Expected connection to "
|
|
+ token.getPackageName()
|
|
+ " but is"
|
|
+ " connected to "
|
|
+ name);
|
|
return;
|
|
}
|
|
IMediaSessionService iService = IMediaSessionService.Stub.asInterface(service);
|
|
if (iService == null) {
|
|
Log.e(TAG, "Service interface is missing.");
|
|
return;
|
|
}
|
|
ConnectionRequest request =
|
|
new ConnectionRequest(getContext().getPackageName(), Process.myPid(), connectionHints);
|
|
iService.connect(controllerStub, request.toBundle());
|
|
connectionRequested = true;
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Service " + name + " has died prematurely");
|
|
} finally {
|
|
if (!connectionRequested) {
|
|
getInstance().runOnApplicationLooper(getInstance()::release);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onServiceDisconnected(ComponentName name) {
|
|
// Temporal lose of the binding because of the service crash. System will automatically
|
|
// rebind, but we'd better to release() here. Otherwise ControllerCallback#onConnected()
|
|
// would be called multiple times, and the controller would be connected to the
|
|
// different session everytime.
|
|
getInstance().runOnApplicationLooper(getInstance()::release);
|
|
}
|
|
|
|
@Override
|
|
public void onBindingDied(ComponentName name) {
|
|
// Permanent lose of the binding because of the service package update or removed.
|
|
// This SessionServiceRecord will be removed accordingly, but forget session binder here
|
|
// for sure.
|
|
getInstance().runOnApplicationLooper(getInstance()::release);
|
|
}
|
|
}
|
|
|
|
private class SurfaceCallback
|
|
implements SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
|
|
|
|
// SurfaceHolder.Callback implementation
|
|
|
|
@Override
|
|
public void surfaceCreated(SurfaceHolder holder) {
|
|
if (videoSurfaceHolder != holder) {
|
|
return;
|
|
}
|
|
videoSurface = holder.getSurface();
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface);
|
|
Rect surfaceSize = holder.getSurfaceFrame();
|
|
maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height());
|
|
}
|
|
|
|
@Override
|
|
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
|
if (videoSurfaceHolder != holder) {
|
|
return;
|
|
}
|
|
maybeNotifySurfaceSizeChanged(width, height);
|
|
}
|
|
|
|
@Override
|
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
|
if (videoSurfaceHolder != holder) {
|
|
return;
|
|
}
|
|
videoSurface = null;
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null);
|
|
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
|
|
}
|
|
|
|
// TextureView.SurfaceTextureListener implementation
|
|
|
|
@Override
|
|
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
|
if (videoTextureView == null || videoTextureView.getSurfaceTexture() != surfaceTexture) {
|
|
return;
|
|
}
|
|
videoSurface = new Surface(surfaceTexture);
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface);
|
|
maybeNotifySurfaceSizeChanged(width, height);
|
|
}
|
|
|
|
@Override
|
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
|
|
if (videoTextureView == null || videoTextureView.getSurfaceTexture() != surfaceTexture) {
|
|
return;
|
|
}
|
|
maybeNotifySurfaceSizeChanged(width, height);
|
|
}
|
|
|
|
@Override
|
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
|
|
if (videoTextureView == null || videoTextureView.getSurfaceTexture() != surfaceTexture) {
|
|
return true;
|
|
}
|
|
videoSurface = null;
|
|
dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null);
|
|
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
|
|
// Do nothing.
|
|
}
|
|
}
|
|
|
|
private class FlushCommandQueueHandler {
|
|
|
|
private static final int MSG_FLUSH_COMMAND_QUEUE = 1;
|
|
|
|
private final Handler handler;
|
|
|
|
public FlushCommandQueueHandler(Looper looper) {
|
|
handler = new Handler(looper, /* callback= */ this::handleMessage);
|
|
}
|
|
|
|
public void sendFlushCommandQueueMessage() {
|
|
if (iSession != null && !handler.hasMessages(MSG_FLUSH_COMMAND_QUEUE)) {
|
|
// Send message to notify the end of the transaction. It will be handled when the current
|
|
// looper iteration is over.
|
|
handler.sendEmptyMessage(MSG_FLUSH_COMMAND_QUEUE);
|
|
}
|
|
}
|
|
|
|
public void release() {
|
|
if (handler.hasMessages(MSG_FLUSH_COMMAND_QUEUE)) {
|
|
flushCommandQueue();
|
|
}
|
|
handler.removeCallbacksAndMessages(/* token= */ null);
|
|
}
|
|
|
|
private boolean handleMessage(Message msg) {
|
|
if (msg.what == MSG_FLUSH_COMMAND_QUEUE) {
|
|
flushCommandQueue();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void flushCommandQueue() {
|
|
try {
|
|
iSession.flushCommandQueue(controllerStub);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Error in sending flushCommandQueue");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final class PeriodInfo {
|
|
private final int index;
|
|
private final long periodPositionUs;
|
|
|
|
public PeriodInfo(int index, long periodPositionUs) {
|
|
this.index = index;
|
|
this.periodPositionUs = periodPositionUs;
|
|
}
|
|
}
|
|
}
|