Add Player onAvailableCommandsChanged callback

PiperOrigin-RevId: 361122259
This commit is contained in:
kimvde 2021-03-05 13:04:57 +00:00 committed by Ian Baker
parent 3de81c6d31
commit 0dcdbf0adf
8 changed files with 519 additions and 34 deletions

View File

@ -105,6 +105,7 @@ public final class CastPlayer extends BasePlayer {
private CastTimeline currentTimeline; private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups; private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection; private TrackSelectionArray currentTrackSelection;
private Commands availableCommands;
@Player.State private int playbackState; @Player.State private int playbackState;
private int currentWindowIndex; private int currentWindowIndex;
private long lastReportedPositionMs; private long lastReportedPositionMs;
@ -147,6 +148,7 @@ public final class CastPlayer extends BasePlayer {
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY; currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
availableCommands = Commands.EMPTY;
pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
@ -369,6 +371,11 @@ public final class CastPlayer extends BasePlayer {
removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount()); removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount());
} }
@Override
public boolean isCommandAvailable(@Command int command) {
return availableCommands.contains(command);
}
@Override @Override
public void prepare() { public void prepare() {
// Do nothing. // Do nothing.
@ -452,6 +459,7 @@ public final class CastPlayer extends BasePlayer {
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY, Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
updateAvailableCommandsAndNotifyIfChanged();
} else if (pendingSeekCount == 0) { } else if (pendingSeekCount == 0) {
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed); listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
} }
@ -645,6 +653,7 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_TRACKS_CHANGED, Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)); listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
} }
updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents(); listeners.flushEvents();
} }
@ -693,6 +702,7 @@ public final class CastPlayer extends BasePlayer {
timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
listener.onTimelineChanged(timeline, 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; 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 @Nullable
private PendingResult<MediaChannelResult> setMediaItemsInternal( private PendingResult<MediaChannelResult> setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems, MediaQueueItem[] mediaQueueItems,
@ -819,6 +839,7 @@ public final class CastPlayer extends BasePlayer {
this.repeatMode.value = repeatMode; this.repeatMode.value = repeatMode;
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode)); Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
updateAvailableCommandsAndNotifyIfChanged();
} }
} }
@ -1003,6 +1024,7 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void onQueueStatusUpdated() { public void onQueueStatusUpdated() {
updateTimelineAndNotifyIfChanged(); updateTimelineAndNotifyIfChanged();
listeners.flushEvents();
} }
@Override @Override

View File

@ -15,11 +15,14 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -42,6 +45,7 @@ import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -293,7 +297,7 @@ public class CastPlayerTest {
public void addMediaItems_insertAtIndex_callsRemoteMediaClient() { public void addMediaItems_insertAtIndex_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
String uri = "http://www.google.com/video3"; String uri = "http://www.google.com/video3";
MediaItem anotherMediaItem = MediaItem anotherMediaItem =
new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build(); new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build();
@ -316,7 +320,7 @@ public class CastPlayerTest {
public void moveMediaItem_callsRemoteMediaClient() { public void moveMediaItem_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2); castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2);
@ -328,7 +332,7 @@ public class CastPlayerTest {
public void moveMediaItem_toBegin_callsRemoteMediaClient() { public void moveMediaItem_toBegin_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0); castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0);
@ -340,7 +344,7 @@ public class CastPlayerTest {
public void moveMediaItem_toEnd_callsRemoteMediaClient() { public void moveMediaItem_toEnd_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 4); castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 4);
@ -355,7 +359,7 @@ public class CastPlayerTest {
public void moveMediaItems_callsRemoteMediaClient() { public void moveMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 3, /* newIndex= */ 1); castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 3, /* newIndex= */ 1);
@ -368,7 +372,7 @@ public class CastPlayerTest {
public void moveMediaItems_toBeginning_callsRemoteMediaClient() { public void moveMediaItems_toBeginning_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4, /* newIndex= */ 0); castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4, /* newIndex= */ 0);
@ -381,7 +385,7 @@ public class CastPlayerTest {
public void moveMediaItems_toEnd_callsRemoteMediaClient() { public void moveMediaItems_toEnd_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 3); castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 3);
@ -396,7 +400,7 @@ public class CastPlayerTest {
public void moveMediaItems_noItems_doesNotCallRemoteMediaClient() { public void moveMediaItems_noItems_doesNotCallRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 1, /* newIndex= */ 0); castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 1, /* newIndex= */ 0);
@ -407,7 +411,7 @@ public class CastPlayerTest {
public void moveMediaItems_noMove_doesNotCallRemoteMediaClient() { public void moveMediaItems_noMove_doesNotCallRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 1); castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 1);
@ -418,7 +422,7 @@ public class CastPlayerTest {
public void removeMediaItems_callsRemoteMediaClient() { public void removeMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4); castPlayer.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4);
@ -429,7 +433,7 @@ public class CastPlayerTest {
public void clearMediaItems_callsRemoteMediaClient() { public void clearMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.clearMediaItems(); castPlayer.clearMediaItems();
@ -444,7 +448,7 @@ public class CastPlayerTest {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds); List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
fillTimeline(mediaItems, mediaQueueItemIds); addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
Timeline currentTimeline = castPlayer.getCurrentTimeline(); Timeline currentTimeline = castPlayer.getCurrentTimeline();
for (int i = 0; i < mediaItems.size(); i++) { for (int i = 0; i < mediaItems.size(); i++) {
@ -453,6 +457,128 @@ public class CastPlayerTest {
} }
} }
@Test
public void seekTo_otherWindow_notifiesAvailableCommandsChanged() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
.thenReturn(mockPendingResult);
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
int[] mediaQueueItemIds = new int[] {1, 2, 3};
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(Player.Commands.EMPTY);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@Test
public void addMediaItems_whenLastPlaying_notifiesAvailableCommandsChanged() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
int[] mediaQueueItemIds = new int[] {1};
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
verify(mockListener, never()).onAvailableCommandsChanged(any());
int[] mediaQueueItemIdsToAdd = new int[] {2};
List<MediaItem> mediaItemsToAdd = createMediaItems(mediaQueueItemIdsToAdd);
addMediaItemsAndUpdateTimeline(
/* existingMediaItems= */ mediaItems,
/* existingMediaQueueItemIds= */ mediaQueueItemIds,
mediaItemsToAdd,
mediaQueueItemIdsToAdd);
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
mediaQueueItemIdsToAdd = new int[] {3};
mediaItemsToAdd = createMediaItems(mediaQueueItemIdsToAdd);
addMediaItemsAndUpdateTimeline(
/* existingMediaItems= */ mediaItems,
/* existingMediaQueueItemIds= */ mediaQueueItemIds,
mediaItemsToAdd,
mediaQueueItemIdsToAdd);
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void removeMediaItems_followingCurrent_notifiesAvailableCommandsChanged() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
MediaItem mediaItem3 = createMediaItem(/* mediaQueueItemId= */ 3);
int[] mediaQueueItemIds = new int[] {1, 2, 3};
List<MediaItem> mediaItems = ImmutableList.of(mediaItem1, mediaItem2, mediaItem3);
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
removeMediaItemsAndUpdateTimeline(
/* existingMediaItems= */ mediaItems,
/* existingMediaQueueItemIds= */ mediaQueueItemIds,
/* fromIndex= */ 2,
/* toIndex= */ 3);
verify(mockListener).onAvailableCommandsChanged(any());
removeMediaItemsAndUpdateTimeline(
/* existingMediaItems= */ ImmutableList.of(mediaItem1, mediaItem2),
/* existingMediaQueueItemIds= */ new int[] {1, 2},
/* fromIndex= */ 1,
/* toIndex= */ 2);
verify(mockListener).onAvailableCommandsChanged(Player.Commands.EMPTY);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void setRepeatMode_all_notifiesAvailableCommandsChanged() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), eq(null)))
.thenReturn(mockPendingResult);
Player.Commands commandsWithHasNext =
new Player.Commands.Builder().add(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
int[] mediaQueueItemIds = new int[] {1};
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
verify(mockListener, never()).onAvailableCommandsChanged(any());
castPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
verify(mockListener).onAvailableCommandsChanged(commandsWithHasNext);
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void setRepeatMode_one_doesNotNotifyAvailableCommandsChanged() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), eq(null)))
.thenReturn(mockPendingResult);
int[] mediaQueueItemIds = new int[] {1};
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockListener, never()).onAvailableCommandsChanged(any());
}
private int[] createMediaQueueItemIds(int numberOfIds) { private int[] createMediaQueueItemIds(int numberOfIds) {
int[] mediaQueueItemIds = new int[numberOfIds]; int[] mediaQueueItemIds = new int[numberOfIds];
for (int i = 0; i < numberOfIds; i++) { for (int i = 0; i < numberOfIds; i++) {
@ -462,22 +588,77 @@ public class CastPlayerTest {
} }
private List<MediaItem> createMediaItems(int[] mediaQueueItemIds) { private List<MediaItem> createMediaItems(int[] mediaQueueItemIds) {
MediaItem.Builder builder = new MediaItem.Builder();
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (int mediaQueueItemId : mediaQueueItemIds) { for (int mediaQueueItemId : mediaQueueItemIds) {
MediaItem mediaItem = mediaItems.add(createMediaItem(mediaQueueItemId));
builder
.setUri("http://www.google.com/video" + mediaQueueItemId)
.setMimeType(MimeTypes.APPLICATION_MPD)
.setTag(mediaQueueItemId)
.build();
mediaItems.add(mediaItem);
} }
return mediaItems; return mediaItems;
} }
private void fillTimeline(List<MediaItem> mediaItems, int[] mediaQueueItemIds) { private MediaItem createMediaItem(int mediaQueueItemId) {
Assertions.checkState(mediaItems.size() == mediaQueueItemIds.length); return new MediaItem.Builder()
.setUri("http://www.google.com/video" + mediaQueueItemId)
.setMimeType(MimeTypes.APPLICATION_MPD)
.setTag(mediaQueueItemId)
.build();
}
private void addMediaItemsAndUpdateTimeline(List<MediaItem> mediaItems, int[] mediaQueueItemIds) {
addMediaItemsAndUpdateTimeline(
/* existingMediaItems= */ ImmutableList.of(),
/* existingMediaQueueItemIds= */ new int[0],
/* mediaItemsToAdd= */ mediaItems,
/* mediaQueueItemIdsToAdd= */ mediaQueueItemIds);
}
private void addMediaItemsAndUpdateTimeline(
List<MediaItem> existingMediaItems,
int[] existingMediaQueueItemIds,
List<MediaItem> mediaItemsToAdd,
int[] mediaQueueItemIdsToAdd) {
Assertions.checkState(existingMediaItems.size() == existingMediaQueueItemIds.length);
Assertions.checkState(mediaItemsToAdd.size() == mediaQueueItemIdsToAdd.length);
List<MediaItem> mediaItems = new ArrayList<>();
mediaItems.addAll(existingMediaItems);
mediaItems.addAll(mediaItemsToAdd);
int existingMediaItemCount = existingMediaQueueItemIds.length;
int mediaItemToAddCount = mediaQueueItemIdsToAdd.length;
int[] mediaQueueItemIds = new int[existingMediaItemCount + mediaItemToAddCount];
System.arraycopy(existingMediaQueueItemIds, 0, mediaQueueItemIds, 0, existingMediaItemCount);
System.arraycopy(
mediaQueueItemIdsToAdd, 0, mediaQueueItemIds, existingMediaItemCount, mediaItemToAddCount);
castPlayer.addMediaItems(mediaItemsToAdd);
updateTimeLine(mediaItems, mediaQueueItemIds);
}
private void removeMediaItemsAndUpdateTimeline(
List<MediaItem> existingMediaItems,
int[] existingMediaQueueItemIds,
int fromIndex,
int toIndex) {
Assertions.checkState(existingMediaItems.size() == existingMediaQueueItemIds.length);
Assertions.checkState(fromIndex >= 0);
Assertions.checkState(fromIndex < toIndex);
int existingMediaItemCount = existingMediaItems.size();
Assertions.checkState(toIndex <= existingMediaItemCount);
List<MediaItem> mediaItems = new ArrayList<>();
int[] mediaQueueItemIds = new int[existingMediaItemCount - toIndex + fromIndex];
for (int i = 0; i < existingMediaItemCount; i++) {
if (i < fromIndex || i >= toIndex) {
mediaItems.add(existingMediaItems.get(i));
mediaQueueItemIds[mediaItems.size() - 1] = existingMediaQueueItemIds[i];
}
}
castPlayer.removeMediaItems(fromIndex, toIndex);
updateTimeLine(mediaItems, mediaQueueItemIds);
}
private void updateTimeLine(List<MediaItem> mediaItems, int[] mediaQueueItemIds) {
List<MediaQueueItem> queueItems = new ArrayList<>(); List<MediaQueueItem> queueItems = new ArrayList<>();
DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
for (MediaItem mediaItem : mediaItems) { for (MediaItem mediaItem : mediaItems) {
@ -491,7 +672,6 @@ public class CastPlayerTest {
when(mockMediaInfo.getStreamType()).thenReturn(MediaInfo.STREAM_TYPE_NONE); when(mockMediaInfo.getStreamType()).thenReturn(MediaInfo.STREAM_TYPE_NONE);
when(mockMediaStatus.getQueueItems()).thenReturn(queueItems); when(mockMediaStatus.getQueueItems()).thenReturn(queueItems);
castPlayer.addMediaItems(mediaItems);
// Call listener to update the timeline of the player. // Call listener to update the timeline of the player.
remoteMediaClientCallback.onQueueStatusUpdated(); remoteMediaClientCallback.onQueueStatusUpdated();
} }

View File

@ -71,14 +71,6 @@ public abstract class BasePlayer implements Player {
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1); removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);
} }
@Override
public boolean isCommandAvailable(@Command int command) {
if (command == COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) {
return hasNext();
}
throw new IllegalArgumentException();
}
@Override @Override
public final void play() { public final void play() {
setPlayWhenReady(true); setPlayWhenReady(true);
@ -262,4 +254,8 @@ public abstract class BasePlayer implements Player {
@RepeatMode int repeatMode = getRepeatMode(); @RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; 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();
}
} }

View File

@ -15,8 +15,11 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.content.Context; import android.content.Context;
import android.os.Looper; import android.os.Looper;
import android.util.SparseBooleanArray;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.SurfaceView; import android.view.SurfaceView;
@ -481,6 +484,17 @@ public interface Player {
@Deprecated @Deprecated
default void onLoadingChanged(boolean isLoading) {} 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 * @deprecated Use {@link #onPlaybackStateChanged(int)} and {@link
* #onPlayWhenReadyChanged(boolean, int)} instead. * #onPlayWhenReadyChanged(boolean, int)} instead.
@ -692,6 +706,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 * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}. * {@link #STATE_ENDED}.
@ -881,7 +994,8 @@ public interface Player {
EVENT_SHUFFLE_MODE_ENABLED_CHANGED, EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
EVENT_PLAYER_ERROR, EVENT_PLAYER_ERROR,
EVENT_POSITION_DISCONTINUITY, EVENT_POSITION_DISCONTINUITY,
EVENT_PLAYBACK_PARAMETERS_CHANGED EVENT_PLAYBACK_PARAMETERS_CHANGED,
EVENT_AVAILABLE_COMMANDS_CHANGED
}) })
@interface EventFlags {} @interface EventFlags {}
/** {@link #getCurrentTimeline()} changed. */ /** {@link #getCurrentTimeline()} changed. */
@ -912,6 +1026,8 @@ public interface Player {
int EVENT_POSITION_DISCONTINUITY = 12; int EVENT_POSITION_DISCONTINUITY = 12;
/** {@link #getPlaybackParameters()} changed. */ /** {@link #getPlaybackParameters()} changed. */
int EVENT_PLAYBACK_PARAMETERS_CHANGED = 13; 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 * Commands that can be executed on a {@code Player}. One of {@link
@ -1105,6 +1221,7 @@ public interface Player {
* *
* @param command A {@link Command}. * @param command A {@link Command}.
* @return Whether the {@link Command} is available. * @return Whether the {@link Command} is available.
* @see EventListener#onAvailableCommandsChanged(Commands)
*/ */
boolean isCommandAvailable(@Command int command); boolean isCommandAvailable(@Command int command);

View File

@ -91,6 +91,7 @@ import java.util.List;
private SeekParameters seekParameters; private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private boolean pauseAtEndOfMediaItems; private boolean pauseAtEndOfMediaItems;
private Commands availableCommands;
// Playback information when there is no pending seek/set source operation. // Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo; private PlaybackInfo playbackInfo;
@ -174,6 +175,7 @@ import java.util.List;
new ExoTrackSelection[renderers.length], new ExoTrackSelection[renderers.length],
/* info= */ null); /* info= */ null);
period = new Timeline.Period(); period = new Timeline.Period();
availableCommands = Commands.EMPTY;
maskingWindowIndex = C.INDEX_UNSET; maskingWindowIndex = C.INDEX_UNSET;
playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null); playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null);
playbackInfoUpdateListener = playbackInfoUpdateListener =
@ -283,6 +285,11 @@ import java.util.List;
listeners.remove(listener); listeners.remove(listener);
} }
@Override
public boolean isCommandAvailable(@Command int command) {
return availableCommands.contains(command);
}
@Override @Override
@State @State
public int getPlaybackState() { public int getPlaybackState() {
@ -573,8 +580,10 @@ import java.util.List;
if (this.repeatMode != repeatMode) { if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode; this.repeatMode = repeatMode;
internalPlayer.setRepeatMode(repeatMode); internalPlayer.setRepeatMode(repeatMode);
listeners.sendEvent( listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode)); Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
updateAvailableCommands();
listeners.flushEvents();
} }
} }
@ -588,9 +597,11 @@ import java.util.List;
if (this.shuffleModeEnabled != shuffleModeEnabled) { if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled; this.shuffleModeEnabled = shuffleModeEnabled;
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled); internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
listeners.sendEvent( listeners.queueEvent(
Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled)); listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
updateAvailableCommands();
listeners.flushEvents();
} }
} }
@ -1110,6 +1121,7 @@ import java.util.List;
listener -> listener ->
listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload)); listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload));
} }
updateAvailableCommands();
listeners.flushEvents(); listeners.flushEvents();
} }
@ -1159,6 +1171,16 @@ import java.util.List;
return new Pair<>(/* isTransitioning */ false, /* mediaItemTransitionReason */ C.INDEX_UNSET); 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( private void setMediaSourcesInternal(
List<MediaSource> mediaSources, List<MediaSource> mediaSources,
int startWindowIndex, int startWindowIndex,

View File

@ -1257,6 +1257,12 @@ public class SimpleExoPlayer extends BasePlayer
prepare(); prepare();
} }
@Override
public boolean isCommandAvailable(@Command int command) {
verifyApplicationThread();
return player.isCommandAvailable(command);
}
@Override @Override
public void prepare() { public void prepare() {
verifyApplicationThread(); verifyApplicationThread();

View File

@ -8078,6 +8078,143 @@ public final class ExoPlayerTest {
exoPlayerTestRunner.assertMediaItemsTransitionedSame(initialMediaItem); exoPlayerTestRunner.assertMediaItemsTransitionedSame(initialMediaItem);
} }
@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 @Test
public void public void
mediaSourceMaybeThrowSourceInfoRefreshError_isNotThrownUntilPlaybackReachedFailingItem() mediaSourceMaybeThrowSourceInfoRefreshError_isNotThrownUntilPlaybackReachedFailingItem()

View File

@ -237,6 +237,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public boolean isCommandAvailable(int command) {
throw new UnsupportedOperationException();
}
@Override @Override
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();