From cd2c1f2f24c3a17ffa59f0c5ba9d17d55f141793 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 6 Nov 2019 16:38:26 +0000 Subject: [PATCH] Playlist API: Add setMediaItem() and prepare() PiperOrigin-RevId: 278867153 --- .../exoplayer2/demo/PlayerActivity.java | 3 +- .../google/android/exoplayer2/ExoPlayer.java | 55 +++++-- .../android/exoplayer2/ExoPlayerImpl.java | 71 +++++--- .../android/exoplayer2/SimpleExoPlayer.java | 53 ++++-- .../android/exoplayer2/ExoPlayerTest.java | 154 ++++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 3 +- .../exoplayer2/testutil/StubExoPlayer.java | 15 ++ 7 files changed, 305 insertions(+), 49 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 2f8d0045d3..2de117e9d7 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -394,7 +394,8 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.prepare(mediaSource, !haveStartPosition, false); + player.setMediaItem(mediaSource); + player.prepare(); updateButtonVisibility(); } 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 7c8a454191..99089a2afc 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 @@ -331,26 +331,51 @@ public interface ExoPlayer extends Player { */ void retry(); - /** - * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code - * prepare(mediaSource, true, true)}. - */ - void prepare(MediaSource mediaSource); + /** Prepares the player. */ + void prepare(); /** - * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback - * position the default position in the first {@link Timeline.Window}. - * - * @param mediaSource The {@link MediaSource} to play. - * @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()}. - * @param resetState Whether the timeline, manifest, tracks and track selections should be reset. - * Should be true unless the player is being prepared to play the same media as it was playing - * previously (e.g. if playback failed and is being retried). + * @deprecated Use {@code setMediaItem(mediaSource, C.TIME_UNSET)} and {@link #prepare()} instead. */ + @Deprecated + void prepare(MediaSource mediaSource); + + /** @deprecated Use {@link #setMediaItem(MediaSource, long)} and {@link #prepare()} instead. */ + @Deprecated void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); + /** + * Sets the specified {@link MediaSource}. + * + *

Note: This is an intermediate implementation towards a larger change. Until then {@link + * #prepare()} has to be called immediately after calling this method. + * + * @param mediaItem The new {@link MediaSource}. + */ + void setMediaItem(MediaSource mediaItem); + + /** + * Sets the specified {@link MediaSource}. + * + *

Note: This is an intermediate implementation towards a larger change. Until then {@link + * #prepare()} has to be called immediately after calling this method. + * + *

This intermediate implementation calls {@code stop(true)} before seeking to avoid seeking in + * a media item that has been set previously. It is equivalent with calling + * + *


+   *   if (!getCurrentTimeline().isEmpty()) {
+   *     player.stop(true);
+   *   }
+   *   player.seekTo(0, startPositionMs);
+   *   player.setMediaItem(mediaItem);
+   * 
+ * + * @param mediaItem The new {@link MediaSource}. + * @param startPositionMs The position in milliseconds to start playback from. + */ + void setMediaItem(MediaSource mediaItem, long startPositionMs); + /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * will be delivered immediately without blocking on the playback thread. The default {@link 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 dd8fbee53c..97658d2906 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 @@ -62,7 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; - private MediaSource mediaSource; + @Nullable private MediaSource mediaSource; private boolean playWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; @@ -219,34 +219,38 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override + @Deprecated public void prepare(MediaSource mediaSource) { - prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + setMediaItem(mediaSource); + prepareInternal(/* resetPosition= */ true, /* resetState= */ true); } @Override + @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - this.mediaSource = mediaSource; - PlaybackInfo playbackInfo = - getResetPlaybackInfo( - resetPosition, - resetState, - /* resetError= */ true, - /* playbackState= */ Player.STATE_BUFFERING); - // Trigger internal prepare first before updating the playback info and notifying external - // listeners to ensure that new operations issued in the listener notifications reach the - // player after this prepare. The internal player can't change the playback info immediately - // because it uses a callback. - hasPendingPrepare = true; - pendingOperationAcks++; - internalPlayer.prepare(mediaSource, resetPosition, resetState); - updatePlaybackInfo( - playbackInfo, - /* positionDiscontinuity= */ false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + setMediaItem(mediaSource); + prepareInternal(resetPosition, resetState); } + @Override + public void prepare() { + Assertions.checkNotNull(mediaSource); + prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + } + + @Override + public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + if (!getCurrentTimeline().isEmpty()) { + stop(/* reset= */ true); + } + seekTo(/* windowIndex= */ 0, startPositionMs); + setMediaItem(mediaItem); + } + + @Override + public void setMediaItem(MediaSource mediaItem) { + mediaSource = mediaItem; + } @Override public void setPlayWhenReady(boolean playWhenReady) { @@ -606,6 +610,29 @@ import java.util.concurrent.CopyOnWriteArrayList; } } + /* package */ void prepareInternal(boolean resetPosition, boolean resetState) { + Assertions.checkNotNull(mediaSource); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, + resetState, + /* resetError= */ true, + /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; + internalPlayer.prepare(mediaSource, resetPosition, resetState); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); + } + private void handlePlaybackParameters( PlaybackParameters playbackParameters, boolean operationAck) { if (operationAck) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 78d6f1d9d8..c0a45249e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1140,6 +1140,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @SuppressWarnings("deprecation") public void retry() { verifyApplicationThread(); if (mediaSource != null @@ -1149,23 +1150,38 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @Deprecated + @SuppressWarnings("deprecation") public void prepare(MediaSource mediaSource) { prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override + @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { verifyApplicationThread(); - if (this.mediaSource != null) { - this.mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - } - this.mediaSource = mediaSource; - mediaSource.addEventListener(eventHandler, analyticsCollector); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepare(mediaSource, resetPosition, resetState); + setMediaItem(mediaSource); + prepareInternal(resetPosition, resetState); + } + + @Override + public void prepare() { + verifyApplicationThread(); + prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + } + + @Override + public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + verifyApplicationThread(); + setMediaItemInternal(mediaItem); + player.setMediaItem(mediaItem, startPositionMs); + } + + @Override + public void setMediaItem(MediaSource mediaItem) { + verifyApplicationThread(); + setMediaItemInternal(mediaItem); + player.setMediaItem(mediaItem); } @Override @@ -1410,6 +1426,23 @@ public class SimpleExoPlayer extends BasePlayer // Internal methods. + private void prepareInternal(boolean resetPosition, boolean resetState) { + Assertions.checkNotNull(mediaSource); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + player.prepareInternal(resetPosition, resetState); + } + + private void setMediaItemInternal(MediaSource mediaItem) { + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + mediaSource = mediaItem; + mediaSource.addEventListener(eventHandler, analyticsCollector); + } + private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 79103bf0be..7146e2e405 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; @@ -33,7 +34,10 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.ClippingMediaSource; +import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.LoopingMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -1582,6 +1586,7 @@ public final class ExoPlayerTest { AtomicInteger counter = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessagesFromStartPositionOnlyOnce") + .waitForTimelineChanged() .pause() .sendMessage( (messageType, payload) -> { @@ -2860,6 +2865,155 @@ public final class ExoPlayerTest { assertThat(seenPlaybackSuppression.get()).isFalse(); } + @Test + public void testDelegatingMediaSourceApproach() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + final ConcatenatingMediaSource underlyingSource = new ConcatenatingMediaSource(); + CompositeMediaSource delegatingMediaSource = + new CompositeMediaSource() { + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + underlyingSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + underlyingSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + prepareChildSource(null, underlyingSource); + } + + @Override + public MediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + return underlyingSource.createPeriod(id, allocator, startPositionUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + underlyingSource.releasePeriod(mediaPeriod); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline timeline) { + refreshSourceInfo(timeline); + } + }; + int[] currentWindowIndices = new int[1]; + long[] currentPlaybackPositions = new long[1]; + long[] windowCounts = new long[1]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testDelegatingMediaSourceApproach") + .seek(/* windowIndex= */ 1, /* positionMs= */ 5000) + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + windowCounts[0] = player.getCurrentTimeline().getWindowCount(); + } + }) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSource(delegatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + assertArrayEquals(new long[] {2}, windowCounts); + assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); + assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); + } + + @Test + public void testSeekTo_windowIndexIsReset_deprecated() throws Exception { + FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); + LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); + final int[] windowIndex = {C.INDEX_UNSET}; + final long[] positionMs = {C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekTo_windowIndexIsReset_deprecated") + .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .waitForSeekProcessed() + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + //noinspection deprecation + player.prepare(mediaSource); + player.seekTo(/* positionMs= */ 5000); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[0] = player.getCurrentWindowIndex(); + positionMs[0] = player.getCurrentPosition(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(loopingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(5000L); + } + + @Test + public void testSeekTo_windowIndexIsReset() throws Exception { + FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); + LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); + final int[] windowIndex = {C.INDEX_UNSET}; + final long[] positionMs = {C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekTo_windowIndexIsReset") + .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .waitForSeekProcessed() + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setMediaItem(mediaSource, /* positionMs= */ 5000); + player.prepare(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[0] = player.getCurrentWindowIndex(); + positionMs[0] = player.getCurrentPosition(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(loopingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(5000L); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index d64a44ac04..bf3cc90a78 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -431,7 +431,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + player.setMediaItem(mediaSource); + player.prepare(); } catch (Exception e) { handleException(e); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 18eaec2cd7..47f34712b9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -96,6 +96,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void prepare() { + throw new UnsupportedOperationException(); + } + @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -106,6 +111,16 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setMediaItem(MediaSource mediaItem) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + throw new UnsupportedOperationException(); + } + @Override public void setPlayWhenReady(boolean playWhenReady) { throw new UnsupportedOperationException();