Commit 0dcdbf0a by kimvde Committed by Ian Baker

Add Player onAvailableCommandsChanged callback

PiperOrigin-RevId: 361122259
parent 3de81c6d
......@@ -105,6 +105,7 @@ public final class CastPlayer extends BasePlayer {
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
private Commands availableCommands;
@Player.State private int playbackState;
private int currentWindowIndex;
private long lastReportedPositionMs;
......@@ -147,6 +148,7 @@ public final class CastPlayer extends BasePlayer {
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
availableCommands = Commands.EMPTY;
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
......@@ -370,6 +372,11 @@ public final class CastPlayer extends BasePlayer {
}
@Override
public boolean isCommandAvailable(@Command int command) {
return availableCommands.contains(command);
}
@Override
public void prepare() {
// Do nothing.
}
......@@ -452,6 +459,7 @@ public final class CastPlayer extends BasePlayer {
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
updateAvailableCommandsAndNotifyIfChanged();
} else if (pendingSeekCount == 0) {
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
......@@ -645,6 +653,7 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
}
updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents();
}
......@@ -693,6 +702,7 @@ public final class CastPlayer extends BasePlayer {
timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
});
updateAvailableCommandsAndNotifyIfChanged();
}
}
......@@ -762,6 +772,16 @@ public final class CastPlayer extends BasePlayer {
return false;
}
private void updateAvailableCommandsAndNotifyIfChanged() {
Commands previousAvailableCommands = availableCommands;
availableCommands = getAvailableCommands();
if (!availableCommands.equals(previousAvailableCommands)) {
listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(availableCommands));
}
}
@Nullable
private PendingResult<MediaChannelResult> setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems,
......@@ -819,6 +839,7 @@ public final class CastPlayer extends BasePlayer {
this.repeatMode.value = repeatMode;
listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
updateAvailableCommandsAndNotifyIfChanged();
}
}
......@@ -1003,6 +1024,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onQueueStatusUpdated() {
updateTimelineAndNotifyIfChanged();
listeners.flushEvents();
}
@Override
......
......@@ -72,14 +72,6 @@ public abstract class BasePlayer implements Player {
}
@Override
public boolean isCommandAvailable(@Command int command) {
if (command == COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) {
return hasNext();
}
throw new IllegalArgumentException();
}
@Override
public final void play() {
setPlayWhenReady(true);
}
......@@ -262,4 +254,8 @@ public abstract class BasePlayer implements Player {
@RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
}
protected Commands getAvailableCommands() {
return new Commands.Builder().addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNext()).build();
}
}
......@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.content.Context;
import android.os.Looper;
import android.util.SparseBooleanArray;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
......@@ -482,6 +485,17 @@ public interface Player {
default void onLoadingChanged(boolean isLoading) {}
/**
* Called when the value returned from {@link #isCommandAvailable(int)} changes for at least one
* {@link Command}.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param availableCommands The available {@link Commands}.
*/
default void onAvailableCommandsChanged(Commands availableCommands) {}
/**
* @deprecated Use {@link #onPlaybackStateChanged(int)} and {@link
* #onPlayWhenReadyChanged(boolean, int)} instead.
*/
......@@ -693,6 +707,105 @@ public interface Player {
}
/**
* A set of {@link Command commands}.
*
* <p>Instances are immutable.
*/
final class Commands {
/** A builder for {@link Commands} instances. */
public static final class Builder {
private final SparseBooleanArray commandsArray;
private boolean buildCalled;
/** Creates a builder. */
public Builder() {
commandsArray = new SparseBooleanArray();
}
/** Creates a builder with the values of the provided {@link Commands}. */
private Builder(Commands commands) {
this.commandsArray = commands.commandsArray.clone();
}
/**
* Adds a {@link Command}.
*
* @param command A {@link Command}.
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
public Builder add(@Command int command) {
checkState(!buildCalled);
commandsArray.append(command, /* value= */ true);
return this;
}
/**
* Adds a {@link Command} if the provided condition is true. Does nothing otherwise.
*
* @param command A {@link Command}.
* @param condition A condition.
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
public Builder addIf(@Command int command, boolean condition) {
checkState(!buildCalled);
if (condition) {
commandsArray.append(command, /* value= */ true);
}
return this;
}
/** Builds a {@link Commands} instance. */
public Commands build() {
checkState(!buildCalled);
buildCalled = true;
return new Commands(commandsArray);
}
}
/** An empty set of commands. */
public static final Commands EMPTY = new Commands.Builder().build();
// A SparseBooleanArray is used instead of a Set to avoid auto-boxing the Command values.
private final SparseBooleanArray commandsArray;
private Commands(SparseBooleanArray commandsArray) {
this.commandsArray = commandsArray;
}
/** Returns a {@link Commands.Builder} initialized with the values of this instance. */
public Builder buildUpon() {
return new Builder(this);
}
/** Returns whether the set of commands contains the specified {@link Command}. */
public boolean contains(@Command int command) {
return commandsArray.get(command);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Commands)) {
return false;
}
Commands commands = (Commands) obj;
return this.commandsArray.equals(commands.commandsArray);
}
@Override
public int hashCode() {
return commandsArray.hashCode();
}
}
/**
* Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}.
*/
......@@ -881,7 +994,8 @@ public interface Player {
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
EVENT_PLAYER_ERROR,
EVENT_POSITION_DISCONTINUITY,
EVENT_PLAYBACK_PARAMETERS_CHANGED
EVENT_PLAYBACK_PARAMETERS_CHANGED,
EVENT_AVAILABLE_COMMANDS_CHANGED
})
@interface EventFlags {}
/** {@link #getCurrentTimeline()} changed. */
......@@ -912,6 +1026,8 @@ public interface Player {
int EVENT_POSITION_DISCONTINUITY = 12;
/** {@link #getPlaybackParameters()} changed. */
int EVENT_PLAYBACK_PARAMETERS_CHANGED = 13;
/** {@link #isCommandAvailable(int)} changed for at least one {@link Command}. */
int EVENT_AVAILABLE_COMMANDS_CHANGED = 14;
/**
* Commands that can be executed on a {@code Player}. One of {@link
......@@ -1105,6 +1221,7 @@ public interface Player {
*
* @param command A {@link Command}.
* @return Whether the {@link Command} is available.
* @see EventListener#onAvailableCommandsChanged(Commands)
*/
boolean isCommandAvailable(@Command int command);
......
......@@ -91,6 +91,7 @@ import java.util.List;
private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder;
private boolean pauseAtEndOfMediaItems;
private Commands availableCommands;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
......@@ -174,6 +175,7 @@ import java.util.List;
new ExoTrackSelection[renderers.length],
/* info= */ null);
period = new Timeline.Period();
availableCommands = Commands.EMPTY;
maskingWindowIndex = C.INDEX_UNSET;
playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null);
playbackInfoUpdateListener =
......@@ -284,6 +286,11 @@ import java.util.List;
}
@Override
public boolean isCommandAvailable(@Command int command) {
return availableCommands.contains(command);
}
@Override
@State
public int getPlaybackState() {
return playbackInfo.playbackState;
......@@ -573,8 +580,10 @@ import java.util.List;
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
internalPlayer.setRepeatMode(repeatMode);
listeners.sendEvent(
listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
updateAvailableCommands();
listeners.flushEvents();
}
}
......@@ -588,9 +597,11 @@ import java.util.List;
if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled;
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
listeners.sendEvent(
listeners.queueEvent(
Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
updateAvailableCommands();
listeners.flushEvents();
}
}
......@@ -1110,6 +1121,7 @@ import java.util.List;
listener ->
listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload));
}
updateAvailableCommands();
listeners.flushEvents();
}
......@@ -1159,6 +1171,16 @@ import java.util.List;
return new Pair<>(/* isTransitioning */ false, /* mediaItemTransitionReason */ C.INDEX_UNSET);
}
private void updateAvailableCommands() {
Commands previousAvailableCommands = availableCommands;
availableCommands = getAvailableCommands();
if (!availableCommands.equals(previousAvailableCommands)) {
listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(availableCommands));
}
}
private void setMediaSourcesInternal(
List<MediaSource> mediaSources,
int startWindowIndex,
......
......@@ -1258,6 +1258,12 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public boolean isCommandAvailable(@Command int command) {
verifyApplicationThread();
return player.isCommandAvailable(command);
}
@Override
public void prepare() {
verifyApplicationThread();
boolean playWhenReady = getPlayWhenReady();
......
......@@ -8079,6 +8079,143 @@ public final class ExoPlayerTest {
}
@Test
public void seekTo_otherWindow_notifiesAvailableCommandsChanged() {
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(any());
player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(Player.Commands.EMPTY);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@Test
public void automaticWindowTransition_notifiesAvailableCommandsChanged() throws Exception {
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
verify(mockListener).onAvailableCommandsChanged(Player.Commands.EMPTY);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void addMediaItems_whenLastPlaying_notifiesAvailableCommandsChanged() throws Exception {
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
verify(mockListener, never()).onAvailableCommandsChanged(any());
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void removeMediaItems_followingCurrent_notifiesAvailableCommandsChanged()
throws Exception {
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 2);
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 1);
verify(mockListener).onAvailableCommandsChanged(Player.Commands.EMPTY);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void setRepeatMode_all_notifiesAvailableCommandsChanged() throws Exception {
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
verify(mockListener, never()).onAvailableCommandsChanged(any());
player.setRepeatMode(Player.REPEAT_MODE_ALL);
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void setRepeatMode_one_doesNotNotifyAvailableCommandsChanged() throws Exception {
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
player.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockListener, never()).onAvailableCommandsChanged(any());
}
@Test
public void setShuffleModeEnabled_notifiesAvailableCommandsChanged() throws Exception {
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
Player.EventListener mockListener = mock(Player.EventListener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
MediaSource mediaSource =
new ConcatenatingMediaSource(
false,
new FakeShuffleOrder(/* length= */ 2),
new FakeMediaSource(),
new FakeMediaSource());
player.addMediaSource(mediaSource);
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
player.setShuffleModeEnabled(true);
verify(mockListener).onAvailableCommandsChanged(Player.Commands.EMPTY);
}
@Test
public void
mediaSourceMaybeThrowSourceInfoRefreshError_isNotThrownUntilPlaybackReachedFailingItem()
throws Exception {
......
......@@ -238,6 +238,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
}
@Override
public boolean isCommandAvailable(int command) {
throw new UnsupportedOperationException();
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
throw new UnsupportedOperationException();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment