diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7c967cd590..f2e9c8f3ed 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -66,6 +66,7 @@ floating point audio without adjustment, pass `enableFloatOutput=true` to the `DefaultAudioSink` constructor ([#7134](https://github.com/google/ExoPlayer/issues/7134)). + * Add media item based playlist API to Player. * Text: * Parse `` and `` tags in WebVTT subtitles (rendering is coming later). @@ -129,6 +130,8 @@ ([#6922](https://github.com/google/ExoPlayer/pull/6922)). * The demo app startup selected item is the last played one. * Add support for x86_64 for the ffmpeg extension. +* Cast extension: Implement playlist API and deprecate the old queue + manipulation API. ### 2.11.3 (2020-02-19) ### diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index c3c4d60a47..5f18ec069a 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -27,10 +27,7 @@ import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; -import com.google.android.exoplayer2.ext.cast.MediaItemConverter; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; @@ -41,7 +38,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; @@ -75,7 +71,6 @@ import java.util.ArrayList; private final ArrayList mediaQueue; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; - private final MediaItemConverter mediaItemConverter; private TrackGroupArray lastSeenTrackGroupArray; private int currentItemIndex; @@ -102,7 +97,6 @@ import java.util.ArrayList; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); - mediaItemConverter = new DefaultMediaItemConverter(); trackSelector = new DefaultTrackSelector(context); exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); @@ -143,7 +137,7 @@ import java.util.ArrayList; mediaQueue.add(item); concatenatingMediaSource.addMediaSource(defaultMediaSourceFactory.createMediaSource(item)); if (currentPlayer == castPlayer) { - castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); + castPlayer.addMediaItem(item); } } @@ -180,7 +174,7 @@ import java.util.ArrayList; if (castTimeline.getPeriodCount() <= itemIndex) { return false; } - castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + castPlayer.removeMediaItem(itemIndex); } } mediaQueue.remove(itemIndex); @@ -196,34 +190,33 @@ import java.util.ArrayList; * Moves an item within the queue. * * @param item The item to move. - * @param toIndex The target index of the item in the queue. + * @param newIndex The target index of the item in the queue. * @return Whether the item move was successful. */ - public boolean moveItem(MediaItem item, int toIndex) { + public boolean moveItem(MediaItem item, int newIndex) { int fromIndex = mediaQueue.indexOf(item); if (fromIndex == -1) { return false; } // Player update. - concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + concatenatingMediaSource.moveMediaSource(fromIndex, newIndex); if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); int periodCount = castTimeline.getPeriodCount(); - if (periodCount <= fromIndex || periodCount <= toIndex) { + if (periodCount <= fromIndex || periodCount <= newIndex) { return false; } - int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; - castPlayer.moveItem(elementId, toIndex); + castPlayer.moveMediaItem(fromIndex, newIndex); } - mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + mediaQueue.add(newIndex, mediaQueue.remove(fromIndex)); // Index update. if (fromIndex == currentItemIndex) { - maybeSetCurrentItemAndNotify(toIndex); - } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(newIndex); + } else if (fromIndex < currentItemIndex && newIndex >= currentItemIndex) { maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + } else if (fromIndex > currentItemIndex && newIndex <= currentItemIndex) { maybeSetCurrentItemAndNotify(currentItemIndex + 1); } @@ -353,7 +346,8 @@ import java.util.ArrayList; // Media queue management. if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); + exoPlayer.setMediaSource(concatenatingMediaSource, /* resetPosition= */ true); + exoPlayer.prepare(); } // Playback transition. @@ -372,11 +366,7 @@ import java.util.ArrayList; private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { maybeSetCurrentItemAndNotify(itemIndex); if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); - } - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + castPlayer.setMediaItems(mediaQueue, itemIndex, positionMs); } else { currentPlayer.seekTo(itemIndex, positionMs); currentPlayer.setPlayWhenReady(playWhenReady); diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index b15fca4154..1903199bcb 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.BasePlayer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -83,6 +84,7 @@ public final class CastPlayer extends BasePlayer { private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; private final CastContext castContext; + private final MediaItemConverter mediaItemConverter; // TODO: Allow custom implementations of CastTimelineTracker. private final CastTimelineTracker timelineTracker; private final Timeline.Period period; @@ -112,10 +114,23 @@ public final class CastPlayer extends BasePlayer { private long pendingSeekPositionMs; /** + * Creates a new cast player that uses a {@link DefaultMediaItemConverter}. + * * @param castContext The context from which the cast session is obtained. */ public CastPlayer(CastContext castContext) { + this(castContext, new DefaultMediaItemConverter()); + } + + /** + * Creates a new cast player. + * + * @param castContext The context from which the cast session is obtained. + * @param mediaItemConverter The {@link MediaItemConverter} to use. + */ + public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter) { this.castContext = castContext; + this.mediaItemConverter = mediaItemConverter; timelineTracker = new CastTimelineTracker(); period = new Timeline.Period(); statusListener = new StatusListener(); @@ -142,105 +157,61 @@ public final class CastPlayer extends BasePlayer { // Media Queue manipulation methods. - /** - * Loads a single item media queue. If no session is available, does nothing. - * - * @param item The item to load. - * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback - * starts at position 0. - * @return The Cast {@code PendingResult}, or null if no session is available. - */ + /** @deprecated Use {@link #setMediaItems(List, int, long)} instead. */ + @Deprecated @Nullable public PendingResult loadItem(MediaQueueItem item, long positionMs) { - return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); + return setMediaItemsInternal( + new MediaQueueItem[] {item}, /* startWindowIndex= */ 0, positionMs, repeatMode.value); } /** - * Loads a media queue. If no session is available, does nothing. - * - * @param items The items to load. - * @param startIndex The index of the item at which playback should start. - * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback - * starts at position 0. - * @param repeatMode The repeat mode for the created media queue. - * @return The Cast {@code PendingResult}, or null if no session is available. + * @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #setRepeatMode(int)} + * instead. */ + @Deprecated @Nullable public PendingResult loadItems( MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { - if (remoteMediaClient != null) { - positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; - return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), - positionMs, null); - } - return null; + return setMediaItemsInternal(items, startIndex, positionMs, repeatMode); } - /** - * Appends a sequence of items to the media queue. If no media queue exists, does nothing. - * - * @param items The items to append. - * @return The Cast {@code PendingResult}, or null if no media queue exists. - */ + /** @deprecated Use {@link #addMediaItems(List)} instead. */ + @Deprecated @Nullable public PendingResult addItems(MediaQueueItem... items) { - return addItems(MediaQueueItem.INVALID_ITEM_ID, items); + return addMediaItemsInternal(items, MediaQueueItem.INVALID_ITEM_ID); } - /** - * Inserts a sequence of items into the media queue. If no media queue or period with id {@code - * periodId} exist, does nothing. - * - * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item - * that will follow immediately after the inserted items. - * @param items The items to insert. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code - * periodId} exist. - */ + /** @deprecated Use {@link #addMediaItems(int, List)} instead. */ + @Deprecated @Nullable public PendingResult addItems(int periodId, MediaQueueItem... items) { - if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID - || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { - return remoteMediaClient.queueInsertItems(items, periodId, null); + if (periodId == MediaQueueItem.INVALID_ITEM_ID + || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return addMediaItemsInternal(items, periodId); } return null; } - /** - * Removes an item from the media queue. If no media queue or period with id {@code periodId} - * exist, does nothing. - * - * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item - * to remove. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code - * periodId} exist. - */ + /** @deprecated Use {@link #removeMediaItem(int)} instead. */ + @Deprecated @Nullable public PendingResult removeItem(int periodId) { - if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { - return remoteMediaClient.queueRemoveItem(periodId, null); + if (currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return removeMediaItemsInternal(new int[] {periodId}); } return null; } - /** - * Moves an existing item within the media queue. If no media queue or period with id {@code - * periodId} exist, does nothing. - * - * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item - * to move. - * @param newIndex The target index of the item in the media queue. Must be in the range 0 <= - * index < {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code - * periodId} exist. - */ + /** @deprecated Use {@link #moveMediaItem(int, int)} instead. */ + @Deprecated @Nullable public PendingResult moveItem(int periodId, int newIndex) { - Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); - if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { - return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null); + Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getWindowCount()); + int fromIndex = currentTimeline.getIndexOfPeriod(periodId); + if (fromIndex != C.INDEX_UNSET && fromIndex != newIndex) { + return moveMediaItemsInternal(new int[] {periodId}, fromIndex, newIndex); } return null; } @@ -325,6 +296,73 @@ public final class CastPlayer extends BasePlayer { } } + @Override + public void setMediaItems( + List mediaItems, int startWindowIndex, long startPositionMs) { + setMediaItemsInternal( + toMediaQueueItems(mediaItems), startWindowIndex, startPositionMs, repeatMode.value); + } + + @Override + public void addMediaItems(List mediaItems) { + addMediaItemsInternal(toMediaQueueItems(mediaItems), MediaQueueItem.INVALID_ITEM_ID); + } + + @Override + public void addMediaItems(int index, List mediaItems) { + Assertions.checkArgument(index >= 0); + int uid = MediaQueueItem.INVALID_ITEM_ID; + if (index < currentTimeline.getWindowCount()) { + uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; + } + addMediaItemsInternal(toMediaQueueItems(mediaItems), uid); + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + Assertions.checkArgument( + fromIndex >= 0 + && fromIndex <= toIndex + && toIndex <= currentTimeline.getWindowCount() + && newIndex >= 0 + && newIndex < currentTimeline.getWindowCount()); + newIndex = Math.min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex)); + if (fromIndex == toIndex || fromIndex == newIndex) { + // Do nothing. + return; + } + int[] uids = new int[toIndex - fromIndex]; + for (int i = 0; i < uids.length; i++) { + uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid; + } + moveMediaItemsInternal(uids, fromIndex, newIndex); + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + Assertions.checkArgument( + fromIndex >= 0 && toIndex >= fromIndex && toIndex <= currentTimeline.getWindowCount()); + if (fromIndex == toIndex) { + // Do nothing. + return; + } + int[] uids = new int[toIndex - fromIndex]; + for (int i = 0; i < uids.length; i++) { + uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid; + } + removeMediaItemsInternal(uids); + } + + @Override + public void clearMediaItems() { + removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount()); + } + + @Override + public void prepare() { + // Do nothing. + } + @Override @Player.State public int getPlaybackState() { @@ -739,6 +777,58 @@ public final class CastPlayer extends BasePlayer { return false; } + @Nullable + private PendingResult setMediaItemsInternal( + MediaQueueItem[] mediaQueueItems, + int startWindowIndex, + long startPositionMs, + @RepeatMode int repeatMode) { + if (remoteMediaClient == null || mediaQueueItems.length == 0) { + return null; + } + startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs; + if (startWindowIndex == C.INDEX_UNSET) { + startWindowIndex = getCurrentWindowIndex(); + startPositionMs = getCurrentPosition(); + } + return remoteMediaClient.queueLoad( + mediaQueueItems, + Math.min(startWindowIndex, mediaQueueItems.length - 1), + getCastRepeatMode(repeatMode), + startPositionMs, + /* customData= */ null); + } + + @Nullable + private PendingResult addMediaItemsInternal(MediaQueueItem[] items, int uid) { + if (remoteMediaClient == null || getMediaStatus() == null) { + return null; + } + return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null); + } + + @Nullable + private PendingResult moveMediaItemsInternal( + int[] uids, int fromIndex, int newIndex) { + if (remoteMediaClient == null || getMediaStatus() == null) { + return null; + } + int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex; + int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID; + if (insertBeforeIndex < currentTimeline.getWindowCount()) { + insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid; + } + return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); + } + + @Nullable + private PendingResult removeMediaItemsInternal(int[] uids) { + if (remoteMediaClient == null || getMediaStatus() == null) { + return null; + } + return remoteMediaClient.queueRemoveItems(uids, /* customData= */ null); + } + private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) { if (this.repeatMode.value != repeatMode) { this.repeatMode.value = repeatMode; @@ -789,6 +879,7 @@ public final class CastPlayer extends BasePlayer { remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); updateInternalStateAndNotifyIfChanged(); } else { + updateTimelineAndNotifyIfChanged(); if (sessionAvailabilityListener != null) { sessionAvailabilityListener.onCastSessionUnavailable(); } @@ -888,6 +979,14 @@ public final class CastPlayer extends BasePlayer { } } + private MediaQueueItem[] toMediaQueueItems(List mediaItems) { + MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()]; + for (int i = 0; i < mediaItems.size(); i++) { + mediaQueueItems[i] = mediaItemConverter.toMediaQueueItem(mediaItems.get(i)); + } + return mediaQueueItems; + } + // Internal classes. private final class StatusListener diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index c408ed3143..79cf9aa85c 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -18,13 +18,22 @@ package com.google.android.exoplayer2.ext.cast; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastSession; @@ -33,6 +42,9 @@ import com.google.android.gms.cast.framework.media.MediaQueue; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,9 +58,13 @@ import org.mockito.Mockito; public class CastPlayerTest { private CastPlayer castPlayer; + + @SuppressWarnings("deprecation") private RemoteMediaClient.Listener remoteMediaClientListener; + @Mock private RemoteMediaClient mockRemoteMediaClient; @Mock private MediaStatus mockMediaStatus; + @Mock private MediaInfo mockMediaInfo; @Mock private MediaQueue mockMediaQueue; @Mock private CastContext mockCastContext; @Mock private SessionManager mockSessionManager; @@ -62,6 +78,9 @@ public class CastPlayerTest { @Captor private ArgumentCaptor listenerArgumentCaptor; + @Captor private ArgumentCaptor queueItemsArgumentCaptor; + + @SuppressWarnings("deprecation") @Before public void setUp() { initMocks(this); @@ -209,4 +228,281 @@ public class CastPlayerTest { verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); } + + @Test + public void setMediaItems_callsRemoteMediaClient() { + List mediaItems = new ArrayList<>(); + String sourceUri1 = "http://www.google.com/video1"; + String sourceUri2 = "http://www.google.com/video2"; + mediaItems.add( + new MediaItem.Builder() + .setSourceUri(sourceUri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .build()); + mediaItems.add( + new MediaItem.Builder() + .setSourceUri(sourceUri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .build()); + + castPlayer.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + + verify(mockRemoteMediaClient) + .queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any()); + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(sourceUri1); + assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(sourceUri2); + } + + @Test + public void setMediaItems_doNotReset_callsRemoteMediaClient() { + MediaItem.Builder builder = new MediaItem.Builder(); + List mediaItems = new ArrayList<>(); + String sourceUri1 = "http://www.google.com/video1"; + String sourceUri2 = "http://www.google.com/video2"; + mediaItems.add(builder.setSourceUri(sourceUri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + mediaItems.add(builder.setSourceUri(sourceUri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + int startWindowIndex = C.INDEX_UNSET; + long startPositionMs = 2000L; + + castPlayer.setMediaItems(mediaItems, startWindowIndex, startPositionMs); + + verify(mockRemoteMediaClient) + .queueLoad(queueItemsArgumentCaptor.capture(), eq(0), anyInt(), eq(0L), any()); + + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(sourceUri1); + assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(sourceUri2); + } + + @Test + public void addMediaItems_callsRemoteMediaClient() { + MediaItem.Builder builder = new MediaItem.Builder(); + List mediaItems = new ArrayList<>(); + String sourceUri1 = "http://www.google.com/video1"; + String sourceUri2 = "http://www.google.com/video2"; + mediaItems.add(builder.setSourceUri(sourceUri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + mediaItems.add(builder.setSourceUri(sourceUri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + + castPlayer.addMediaItems(mediaItems); + + verify(mockRemoteMediaClient) + .queueInsertItems( + queueItemsArgumentCaptor.capture(), eq(MediaQueueItem.INVALID_ITEM_ID), any()); + + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(sourceUri1); + assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(sourceUri2); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void addMediaItems_insertAtIndex_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + String sourceUri = "http://www.google.com/video3"; + MediaItem anotherMediaItem = + new MediaItem.Builder() + .setSourceUri(sourceUri) + .setMimeType(MimeTypes.APPLICATION_MPD) + .build(); + + // Add another on position 1 + int index = 1; + castPlayer.addMediaItems(index, Collections.singletonList(anotherMediaItem)); + + verify(mockRemoteMediaClient) + .queueInsertItems( + queueItemsArgumentCaptor.capture(), + eq((int) mediaItems.get(index).playbackProperties.tag), + any()); + + MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); + assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(sourceUri); + } + + @Test + public void moveMediaItem_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2); + + verify(mockRemoteMediaClient) + .queueReorderItems(new int[] {2}, /* insertBeforeItemId= */ 4, /* customData= */ null); + } + + @Test + public void moveMediaItem_toBegin_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0); + + verify(mockRemoteMediaClient) + .queueReorderItems(new int[] {2}, /* insertBeforeItemId= */ 1, /* customData= */ null); + } + + @Test + public void moveMediaItem_toEnd_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 4); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {2}, + /* insertBeforeItemId= */ MediaQueueItem.INVALID_ITEM_ID, + /* customData= */ null); + } + + @Test + public void moveMediaItems_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 3, /* newIndex= */ 1); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {1, 2, 3}, /* insertBeforeItemId= */ 5, /* customData= */ null); + } + + @Test + public void moveMediaItems_toBeginning_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4, /* newIndex= */ 0); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {2, 3, 4}, /* insertBeforeItemId= */ 1, /* customData= */ null); + } + + @Test + public void moveMediaItems_toEnd_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 3); + + verify(mockRemoteMediaClient) + .queueReorderItems( + new int[] {1, 2}, + /* insertBeforeItemId= */ MediaQueueItem.INVALID_ITEM_ID, + /* customData= */ null); + } + + @Test + public void moveMediaItems_noItems_doesNotCallRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 1, /* newIndex= */ 0); + + verify(mockRemoteMediaClient, never()).queueReorderItems(any(), anyInt(), any()); + } + + @Test + public void moveMediaItems_noMove_doesNotCallRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 1); + + verify(mockRemoteMediaClient, never()).queueReorderItems(any(), anyInt(), any()); + } + + @Test + public void removeMediaItems_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4); + + verify(mockRemoteMediaClient).queueRemoveItems(new int[] {2, 3, 4}, /* customData= */ null); + } + + @Test + public void clearMediaItems_callsRemoteMediaClient() { + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + fillTimeline(mediaItems, mediaQueueItemIds); + + castPlayer.clearMediaItems(); + + verify(mockRemoteMediaClient) + .queueRemoveItems(new int[] {1, 2, 3, 4, 5}, /* customData= */ null); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void addMediaItems_fillsTimeline() { + Timeline.Window window = new Timeline.Window(); + int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5); + List mediaItems = createMediaItems(mediaQueueItemIds); + + fillTimeline(mediaItems, mediaQueueItemIds); + + Timeline currentTimeline = castPlayer.getCurrentTimeline(); + for (int i = 0; i < mediaItems.size(); i++) { + assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).uid) + .isEqualTo(mediaItems.get(i).playbackProperties.tag); + } + } + + private int[] createMediaQueueItemIds(int numberOfIds) { + int[] mediaQueueItemIds = new int[numberOfIds]; + for (int i = 0; i < numberOfIds; i++) { + mediaQueueItemIds[i] = i + 1; + } + return mediaQueueItemIds; + } + + private List createMediaItems(int[] mediaQueueItemIds) { + MediaItem.Builder builder = new MediaItem.Builder(); + List mediaItems = new ArrayList<>(); + for (int mediaQueueItemId : mediaQueueItemIds) { + MediaItem mediaItem = + builder + .setSourceUri("http://www.google.com/video" + mediaQueueItemId) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(mediaQueueItemId) + .build(); + mediaItems.add(mediaItem); + } + return mediaItems; + } + + private void fillTimeline(List mediaItems, int[] mediaQueueItemIds) { + Assertions.checkState(mediaItems.size() == mediaQueueItemIds.length); + List queueItems = new ArrayList<>(); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + for (MediaItem mediaItem : mediaItems) { + queueItems.add(converter.toMediaQueueItem(mediaItem)); + } + + // Set up mocks to allow the player to update the timeline. + when(mockMediaQueue.getItemIds()).thenReturn(mediaQueueItemIds); + when(mockMediaStatus.getCurrentItemId()).thenReturn(1); + when(mockMediaStatus.getMediaInfo()).thenReturn(mockMediaInfo); + when(mockMediaInfo.getStreamType()).thenReturn(MediaInfo.STREAM_TYPE_NONE); + when(mockMediaStatus.getQueueItems()).thenReturn(queueItems); + + castPlayer.addMediaItems(mediaItems); + // Call listener to update the timeline of the player. + remoteMediaClientListener.onQueueStatusUpdated(); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 0f00621676..5692b1dae7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.List; /** Abstract base {@link Player} which implements common implementation independent methods. */ public abstract class BasePlayer implements Player { @@ -27,6 +29,54 @@ public abstract class BasePlayer implements Player { window = new Timeline.Window(); } + @Override + public void setMediaItem(MediaItem mediaItem) { + setMediaItems(Collections.singletonList(mediaItem)); + } + + @Override + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); + } + + @Override + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + setMediaItems(Collections.singletonList(mediaItem), resetPosition); + } + + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + setMediaItems( + mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET); + } + + @Override + public void setMediaItems(List mediaItems) { + setMediaItems(mediaItems, /* resetPosition= */ true); + } + + @Override + public void addMediaItem(int index, MediaItem mediaItem) { + addMediaItems(index, Collections.singletonList(mediaItem)); + } + + @Override + public void addMediaItem(MediaItem mediaItem) { + addMediaItems(Collections.singletonList(mediaItem)); + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + if (currentIndex != newIndex) { + moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); + } + } + + @Override + public void removeMediaItem(int index) { + removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1); + } + @Override public final void play() { setPlayWhenReady(true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 23d854c0cb..d779037817 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -410,9 +410,6 @@ public interface ExoPlayer extends Player { @Deprecated void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); - /** Prepares the player. */ - void prepare(); - /** * Clears the playlist, adds the specified {@link MediaSource MediaSources} and resets the * position to the default position. @@ -500,131 +497,6 @@ public interface ExoPlayer extends Player { */ void addMediaSources(int index, List mediaSources); - /** - * Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to - * the default position. - * - * @param mediaItems The new {@link MediaItem MediaItems}. - */ - void setMediaItems(List mediaItems); - - /** - * Clears the playlist and adds the specified {@link MediaItem MediaItems}. - * - * @param mediaItems The new {@link MediaItem MediaItems}. - * @param resetPosition Whether the playback position should be reset to the default position in - * the first {@link Timeline.Window}. If false, playback will start from the position defined - * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. - */ - void setMediaItems(List mediaItems, boolean resetPosition); - - /** - * Clears the playlist and adds the specified {@link MediaItem MediaItems}. - * - * @param mediaItems The new {@link MediaItem MediaItems}. - * @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is - * passed, the current position is not reset. - * @param startPositionMs The position in milliseconds to start playback from. If {@link - * C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if - * {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the - * position is not reset at all. - */ - void setMediaItems(List mediaItems, int startWindowIndex, long startPositionMs); - - /** - * Clears the playlist, adds the specified {@link MediaItem} and resets the position to the - * default position. - * - * @param mediaItem The new {@link MediaItem}. - */ - void setMediaItem(MediaItem mediaItem); - - /** - * Clears the playlist and adds the specified {@link MediaItem}. - * - * @param mediaItem The new {@link MediaItem}. - * @param startPositionMs The position in milliseconds to start playback from. - */ - void setMediaItem(MediaItem mediaItem, long startPositionMs); - - /** - * Clears the playlist and adds the specified {@link MediaItem}. - * - * @param mediaItem The new {@link MediaItem}. - * @param resetPosition Whether the playback position should be reset to the default position. If - * false, playback will start from the position defined by {@link #getCurrentWindowIndex()} - * and {@link #getCurrentPosition()}. - */ - void setMediaItem(MediaItem mediaItem, boolean resetPosition); - - /** - * Adds a media item to the end of the playlist. - * - * @param mediaItem The {@link MediaItem} to add. - */ - void addMediaItem(MediaItem mediaItem); - - /** - * Adds a media item at the given index of the playlist. - * - * @param index The index at which to add the item. - * @param mediaItem The {@link MediaItem} to add. - */ - void addMediaItem(int index, MediaItem mediaItem); - - /** - * Adds a list of media items to the end of the playlist. - * - * @param mediaItems The {@link MediaItem MediaItems} to add. - */ - void addMediaItems(List mediaItems); - - /** - * Adds a list of media items at the given index of the playlist. - * - * @param index The index at which to add the media items. - * @param mediaItems The {@link MediaItem MediaItems} to add. - */ - void addMediaItems(int index, List mediaItems); - - /** - * Moves the media item at the current index to the new index. - * - * @param currentIndex The current index of the media item to move. - * @param newIndex The new index of the media item. If the new index is larger than the size of - * the playlist the item is moved to the end of the playlist. - */ - void moveMediaItem(int currentIndex, int newIndex); - - /** - * Moves the media item range to the new index. - * - * @param fromIndex The start of the range to move. - * @param toIndex The first item not to be included in the range (exclusive). - * @param newIndex The new index of the first media item of the range. If the new index is larger - * than the size of the remaining playlist after removing the range, the range is moved to the - * end of the playlist. - */ - void moveMediaItems(int fromIndex, int toIndex, int newIndex); - - /** - * Removes the media item at the given index of the playlist. - * - * @param index The index at which to remove the media item. - */ - void removeMediaItem(int index); - - /** - * Removes a range of media items from the playlist. - * - * @param fromIndex The index at which to start removing media items. - * @param toIndex The index of the first item to be kept (exclusive). - */ - void removeMediaItems(int fromIndex, int toIndex); - - /** Clears the playlist. */ - void clearMediaItems(); - /** * Sets the shuffle order. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 015c606070..efc8c14dcc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -321,31 +321,6 @@ import java.util.concurrent.TimeoutException; prepare(); } - @Override - public void setMediaItem(MediaItem mediaItem) { - setMediaItems(Collections.singletonList(mediaItem)); - } - - @Override - public void setMediaItem(MediaItem mediaItem, long startPositionMs) { - setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); - } - - @Override - public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { - setMediaItems(Collections.singletonList(mediaItem), resetPosition); - } - - @Override - public void setMediaItems(List mediaItems) { - setMediaItems(mediaItems, /* resetPosition= */ true); - } - - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - setMediaSources(createMediaSources(mediaItems), resetPosition); - } - @Override public void setMediaItems( List mediaItems, int startWindowIndex, long startPositionMs) { @@ -389,16 +364,6 @@ import java.util.concurrent.TimeoutException; mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false); } - @Override - public void addMediaItem(int index, MediaItem mediaItem) { - addMediaItems(index, Collections.singletonList(mediaItem)); - } - - @Override - public void addMediaItem(MediaItem mediaItem) { - addMediaItems(Collections.singletonList(mediaItem)); - } - @Override public void addMediaItems(List mediaItems) { addMediaItems(/* index= */ mediaSourceHolders.size(), mediaItems); @@ -444,23 +409,12 @@ import java.util.concurrent.TimeoutException; /* seekProcessed= */ false); } - @Override - public void removeMediaItem(int index) { - removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); - } - @Override public void removeMediaItems(int fromIndex, int toIndex) { Assertions.checkArgument(toIndex > fromIndex); removeMediaItemsInternal(fromIndex, toIndex); } - @Override - public void moveMediaItem(int currentIndex, int newIndex) { - Assertions.checkArgument(currentIndex != newIndex); - moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); - } - @Override public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { Assertions.checkArgument( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index eab0ea6891..0184ea29e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; /** * A media player interface defining traditional high-level functionality, such as the ability to @@ -754,6 +755,134 @@ public interface Player { */ void removeListener(EventListener listener); + /** + * Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to + * the default position. + * + * @param mediaItems The new {@link MediaItem MediaItems}. + */ + void setMediaItems(List mediaItems); + + /** + * Clears the playlist and adds the specified {@link MediaItem MediaItems}. + * + * @param mediaItems The new {@link MediaItem MediaItems}. + * @param resetPosition Whether the playback position should be reset to the default position in + * the first {@link Timeline.Window}. If false, playback will start from the position defined + * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. + */ + void setMediaItems(List mediaItems, boolean resetPosition); + + /** + * Clears the playlist and adds the specified {@link MediaItem MediaItems}. + * + * @param mediaItems The new {@link MediaItem MediaItems}. + * @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is + * passed, the current position is not reset. + * @param startPositionMs The position in milliseconds to start playback from. If {@link + * C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if + * {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the + * position is not reset at all. + */ + void setMediaItems(List mediaItems, int startWindowIndex, long startPositionMs); + + /** + * Clears the playlist, adds the specified {@link MediaItem} and resets the position to the + * default position. + * + * @param mediaItem The new {@link MediaItem}. + */ + void setMediaItem(MediaItem mediaItem); + + /** + * Clears the playlist and adds the specified {@link MediaItem}. + * + * @param mediaItem The new {@link MediaItem}. + * @param startPositionMs The position in milliseconds to start playback from. + */ + void setMediaItem(MediaItem mediaItem, long startPositionMs); + + /** + * Clears the playlist and adds the specified {@link MediaItem}. + * + * @param mediaItem The new {@link MediaItem}. + * @param resetPosition Whether the playback position should be reset to the default position. If + * false, playback will start from the position defined by {@link #getCurrentWindowIndex()} + * and {@link #getCurrentPosition()}. + */ + void setMediaItem(MediaItem mediaItem, boolean resetPosition); + + /** + * Adds a media item to the end of the playlist. + * + * @param mediaItem The {@link MediaItem} to add. + */ + void addMediaItem(MediaItem mediaItem); + + /** + * Adds a media item at the given index of the playlist. + * + * @param index The index at which to add the item. + * @param mediaItem The {@link MediaItem} to add. + */ + void addMediaItem(int index, MediaItem mediaItem); + + /** + * Adds a list of media items to the end of the playlist. + * + * @param mediaItems The {@link MediaItem MediaItems} to add. + */ + void addMediaItems(List mediaItems); + + /** + * Adds a list of media items at the given index of the playlist. + * + * @param index The index at which to add the media items. + * @param mediaItems The {@link MediaItem MediaItems} to add. + */ + void addMediaItems(int index, List mediaItems); + + /** + * Moves the media item at the current index to the new index. + * + * @param currentIndex The current index of the media item to move. + * @param newIndex The new index of the media item. If the new index is larger than the size of + * the playlist the item is moved to the end of the playlist. + */ + void moveMediaItem(int currentIndex, int newIndex); + + /** + * Moves the media item range to the new index. + * + * @param fromIndex The start of the range to move. + * @param toIndex The first item not to be included in the range (exclusive). + * @param newIndex The new index of the first media item of the range. If the new index is larger + * than the size of the remaining playlist after removing the range, the range is moved to the + * end of the playlist. + */ + void moveMediaItems(int fromIndex, int toIndex, int newIndex); + + /** + * Removes the media item at the given index of the playlist. + * + * @param index The index at which to remove the media item. + */ + void removeMediaItem(int index); + + /** + * Removes a range of media items from the playlist. + * + * @param fromIndex The index at which to start removing media items. + * @param toIndex The index of the first item to be kept (exclusive). + */ + void removeMediaItems(int fromIndex, int toIndex); + + /** Clears the playlist. */ + void clearMediaItems(); + + /** Prepares the player. */ + void prepare(); + /** * Returns the current {@link State playback state} of the player. *