From 4ad4e3e4fcf2480bc36e7034026a3538ec7664be Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 23 Sep 2019 19:59:09 +0100 Subject: [PATCH] Rollback of https://github.com/google/ExoPlayer/commit/3b22db33ba944df6829b1eff328efb0cd25e1678 *** Original commit *** add top-level playlist API to ExoPlayer Public design doc: https://docs.google.com/document/d/11h0S91KI5TB3NNZUtsCzg0S7r6nyTnF_tDZZAtmY93g Issue: #6161 *** PiperOrigin-RevId: 270728267 --- .../exoplayer2/demo/PlayerActivity.java | 52 +- .../exoplayer2/ext/cast/CastPlayer.java | 14 +- .../exoplayer2/ext/ima/FakePlayer.java | 15 +- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 4 +- .../google/android/exoplayer2/ExoPlayer.java | 158 +-- .../android/exoplayer2/ExoPlayerFactory.java | 10 +- .../android/exoplayer2/ExoPlayerImpl.java | 293 +---- .../exoplayer2/ExoPlayerImplInternal.java | 333 ++--- .../android/exoplayer2/MediaPeriodHolder.java | 24 +- .../android/exoplayer2/MediaPeriodQueue.java | 7 +- .../com/google/android/exoplayer2/Player.java | 25 +- .../google/android/exoplayer2/Playlist.java | 708 ----------- .../android/exoplayer2/SimpleExoPlayer.java | 179 +-- .../google/android/exoplayer2/Timeline.java | 69 - .../analytics/AnalyticsCollector.java | 17 +- .../AbstractConcatenatedTimeline.java | 50 +- .../source/ConcatenatingMediaSource.java | 1 - .../exoplayer2/source/LoopingMediaSource.java | 1 - .../exoplayer2/source/MaskingMediaSource.java | 12 +- .../android/exoplayer2/util/EventLogger.java | 10 +- .../google/android/exoplayer2/util/Util.java | 37 - .../android/exoplayer2/ExoPlayerTest.java | 1106 ++++------------- .../exoplayer2/MediaPeriodQueueTest.java | 111 +- .../android/exoplayer2/PlaylistTest.java | 510 -------- .../android/exoplayer2/TimelineTest.java | 140 --- .../analytics/AnalyticsCollectorTest.java | 101 +- .../android/exoplayer2/testutil/Action.java | 269 +--- .../exoplayer2/testutil/ActionSchedule.java | 129 +- .../exoplayer2/testutil/ExoHostedTest.java | 3 +- .../testutil/ExoPlayerTestRunner.java | 81 +- .../exoplayer2/testutil/StubExoPlayer.java | 83 -- .../android/exoplayer2/testutil/TestUtil.java | 58 - 32 files changed, 755 insertions(+), 3855 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/Playlist.java rename library/core/src/main/java/com/google/android/exoplayer2/{ => source}/AbstractConcatenatedTimeline.java (89%) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java 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 a9d1db64ad..347f49e27c 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 @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -78,7 +79,6 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.ArrayList; -import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -141,7 +141,7 @@ public class PlayerActivity extends AppCompatActivity private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private List mediaSources; + private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; @@ -343,8 +343,8 @@ public class PlayerActivity extends AppCompatActivity Intent intent = getIntent(); releaseMediaDrms(); - mediaSources = createTopLevelMediaSources(intent); - if (mediaSources.isEmpty()) { + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { return; } @@ -388,12 +388,12 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.setMediaItems(mediaSources, /* resetPosition= */ !haveStartPosition); - player.prepare(); + player.prepare(mediaSource, !haveStartPosition, false); updateButtonVisibility(); } - private List createTopLevelMediaSources(Intent intent) { + @Nullable + private MediaSource createTopLevelMediaSource(Intent intent) { String action = intent.getAction(); boolean actionIsListView = ACTION_VIEW_LIST.equals(action); if (!actionIsListView && !ACTION_VIEW.equals(action)) { @@ -421,30 +421,34 @@ public class PlayerActivity extends AppCompatActivity } } - List mediaSources = new ArrayList<>(); - for (UriSample sample : samples) { - mediaSources.add(createLeafMediaSource(sample)); + MediaSource[] mediaSources = new MediaSource[samples.length]; + for (int i = 0; i < samples.length; i++) { + mediaSources[i] = createLeafMediaSource(samples[i]); } - if (seenAdsTagUri && mediaSources.size() == 1) { + MediaSource mediaSource = + mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); + + if (seenAdsTagUri) { Uri adTagUri = samples[0].adTagUri; - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri); - if (adsMediaSource != null) { - mediaSources.set(0, adsMediaSource); + if (actionIsListView) { + showToast(R.string.unsupported_ads_in_concatenation); } else { - showToast(R.string.ima_not_loaded); + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); + if (adsMediaSource != null) { + mediaSource = adsMediaSource; + } else { + showToast(R.string.ima_not_loaded); + } } - } else if (seenAdsTagUri && mediaSources.size() > 1) { - showToast(R.string.unsupported_ads_in_concatenation); - releaseAdsLoader(); } else { releaseAdsLoader(); } - return mediaSources; + return mediaSource; } private MediaSource createLeafMediaSource(UriSample parameters) { @@ -544,7 +548,7 @@ public class PlayerActivity extends AppCompatActivity debugViewHelper = null; player.release(); player = null; - mediaSources = null; + mediaSource = null; trackSelector = null; } if (adsLoader != null) { 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 91a4c86cf2..2210f2c8b2 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 @@ -106,6 +106,7 @@ public final class CastPlayer extends BasePlayer { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; + private boolean waitingForInitialTimeline; /** * @param castContext The context from which the cast session is obtained. @@ -167,6 +168,7 @@ public final class CastPlayer extends BasePlayer { MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; + waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); } @@ -592,13 +594,15 @@ public final class CastPlayer extends BasePlayer { private void maybeUpdateTimelineAndNotify() { if (updateTimeline()) { - // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and - // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. + @Player.TimelineChangeReason + int reason = + waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; notificationsBatch.add( new ListenerNotificationTask( - listener -> - listener.onTimelineChanged( - currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE))); + listener -> listener.onTimelineChanged(currentTimeline, reason))); } } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index a6a725ee9e..a9572b7a8d 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -30,6 +30,7 @@ import java.util.ArrayList; private final Timeline.Period period; private final Timeline timeline; + private boolean prepared; @Player.State private int state; private boolean playWhenReady; private long position; @@ -46,17 +47,13 @@ import java.util.ArrayList; timeline = Timeline.EMPTY; } - /** - * Sets the timeline on this fake player, which notifies listeners with the changed timeline and - * the given timeline change reason. - * - * @param timeline The new timeline. - * @param timelineChangeReason The reason for the timeline change. - */ - public void updateTimeline(Timeline timeline, @TimelineChangeReason int timelineChangeReason) { + /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ + public void updateTimeline(Timeline timeline) { for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(timeline, timelineChangeReason); + listener.onTimelineChanged( + timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED); } + prepared = true; } /** diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index a5c6b00619..2995df4ab4 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -285,9 +285,7 @@ public class ImaAdsLoaderTest { public void onAdPlaybackState(AdPlaybackState adPlaybackState) { adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); this.adPlaybackState = adPlaybackState; - fakeExoPlayer.updateTimeline( - new SinglePeriodAdTimeline(contentTimeline, adPlaybackState), - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); } @Override 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 4418549c8b..7c8a454191 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 @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -40,7 +39,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; -import java.util.List; /** * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link @@ -141,7 +139,7 @@ public interface ExoPlayer extends Player { private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private Looper looper; - @Nullable private AnalyticsCollector analyticsCollector; + private AnalyticsCollector analyticsCollector; private boolean useLazyPreparation; private boolean buildCalled; @@ -172,7 +170,7 @@ public interface ExoPlayer extends Player { new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), - /* analyticsCollector= */ null, + new AnalyticsCollector(Clock.DEFAULT), /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -199,7 +197,7 @@ public interface ExoPlayer extends Player { LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper, - @Nullable AnalyticsCollector analyticsCollector, + AnalyticsCollector analyticsCollector, boolean useLazyPreparation, Clock clock) { Assertions.checkArgument(renderers.length > 0); @@ -320,156 +318,38 @@ public interface ExoPlayer extends Player { Assertions.checkState(!buildCalled); buildCalled = true; return new ExoPlayerImpl( - renderers, - trackSelector, - loadControl, - bandwidthMeter, - analyticsCollector, - useLazyPreparation, - clock, - looper); + renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); } } /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); - /** @deprecated Use {@link #prepare()} instead. */ - @Deprecated + /** + * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback + * has not failed or been stopped. + */ void retry(); - /** @deprecated Use {@link #setMediaItem(MediaSource)} and {@link #prepare()} instead. */ - @Deprecated + /** + * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code + * prepare(mediaSource, true, true)}. + */ void prepare(MediaSource mediaSource); - /** @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #prepare()} instead. */ - @Deprecated - void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); - - /** Prepares the player. */ - void prepare(); - /** - * Clears the playlist and adds the specified {@link MediaSource MediaSources}. + * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback + * position the default position in the first {@link Timeline.Window}. * - * @param mediaItems The new {@link MediaSource MediaSources}. - */ - void setMediaItems(List mediaItems); - - /** - * Clears the playlist and adds the specified {@link MediaSource MediaSources}. - * - * @param mediaItems The new {@link MediaSource MediaSources}. + * @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). */ - void setMediaItems(List mediaItems, boolean resetPosition); - - /** - * Clears the playlist and adds the specified {@link MediaSource MediaSources}. - * - * @param mediaItems The new {@link MediaSource MediaSources}. - * @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 and adds the specified {@link MediaSource}. - * - * @param mediaItem The new {@link MediaSource}. - */ - void setMediaItem(MediaSource mediaItem); - - /** - * Clears the playlist and adds the specified {@link MediaSource}. - * - * @param mediaItem The new {@link MediaSource}. - * @param startPositionMs The position in milliseconds to start playback from. - */ - void setMediaItem(MediaSource mediaItem, long startPositionMs); - - /** - * Adds a media item to the end of the playlist. - * - * @param mediaSource The {@link MediaSource} to add. - */ - void addMediaItem(MediaSource mediaSource); - - /** - * Adds a media item at the given index of the playlist. - * - * @param index The index at which to add the item. - * @param mediaSource The {@link MediaSource} to add. - */ - void addMediaItem(int index, MediaSource mediaSource); - - /** - * Adds a list of media items to the end of the playlist. - * - * @param mediaSources The {@link MediaSource MediaSources} to add. - */ - void addMediaItems(List mediaSources); - - /** - * 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 mediaSources The {@link MediaSource MediaSources} to add. - */ - void addMediaItems(int index, List mediaSources); - - /** - * 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. - * @return The removed {@link MediaSource} or null if no item exists at the given index. - */ - @Nullable - MediaSource 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. - * - * @param shuffleOrder The shuffle order. - */ - void setShuffleOrder(ShuffleOrder shuffleOrder); + void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index b900491b1d..efe351c70a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -296,7 +296,6 @@ public final class ExoPlayerFactory { drmSessionManager, bandwidthMeter, analyticsCollector, - /* useLazyPreparation= */ true, Clock.DEFAULT, looper); } @@ -345,13 +344,6 @@ public final class ExoPlayerFactory { BandwidthMeter bandwidthMeter, Looper looper) { return new ExoPlayerImpl( - renderers, - trackSelector, - loadControl, - bandwidthMeter, - /* analyticsCollector= */ null, - /* useLazyPreparation= */ true, - Clock.DEFAULT, - looper); + renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); } } 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 b9f29e2cb3..cbbf5cacbc 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 @@ -22,10 +22,8 @@ import android.os.Message; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -37,9 +35,6 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -66,20 +61,19 @@ import java.util.concurrent.CopyOnWriteArrayList; private final CopyOnWriteArrayList listeners; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; - private final List mediaSourceHolders; - private final boolean useLazyPreparation; + private MediaSource mediaSource; private boolean playWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; + private boolean hasPendingPrepare; private boolean hasPendingSeek; private boolean foregroundMode; private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; - private ShuffleOrder shuffleOrder; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -96,10 +90,6 @@ import java.util.concurrent.CopyOnWriteArrayList; * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollector The {@link AnalyticsCollector} that will be used by the instance. - * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest - * loads and other initial preparation steps happen immediately. If true, these initial - * preparations are triggered only when the player starts buffering the media. * @param clock The {@link Clock} that will be used by the instance. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. @@ -110,8 +100,6 @@ import java.util.concurrent.CopyOnWriteArrayList; TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, - @Nullable AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" @@ -119,13 +107,10 @@ import java.util.concurrent.CopyOnWriteArrayList; Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); - this.useLazyPreparation = useLazyPreparation; - playWhenReady = false; - repeatMode = Player.REPEAT_MODE_OFF; - shuffleModeEnabled = false; - listeners = new CopyOnWriteArrayList<>(); - mediaSourceHolders = new ArrayList<>(); - shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); + this.playWhenReady = false; + this.repeatMode = Player.REPEAT_MODE_OFF; + this.shuffleModeEnabled = false; + this.listeners = new CopyOnWriteArrayList<>(); emptyTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[renderers.length], @@ -144,9 +129,6 @@ import java.util.concurrent.CopyOnWriteArrayList; }; playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); pendingListenerNotifications = new ArrayDeque<>(); - if (analyticsCollector != null) { - analyticsCollector.setPlayer(this); - } internalPlayer = new ExoPlayerImplInternal( renderers, @@ -157,7 +139,6 @@ import java.util.concurrent.CopyOnWriteArrayList; playWhenReady, repeatMode, shuffleModeEnabled, - analyticsCollector, eventHandler, clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); @@ -231,168 +212,41 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override - @Deprecated public void retry() { - prepare(); + if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } } @Override - public void prepare() { - if (playbackInfo.playbackState != Player.STATE_IDLE) { - return; - } + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + this.mediaSource = mediaSource; PlaybackInfo playbackInfo = getResetPlaybackInfo( - /* clearPlaylist= */ false, + 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(); + internalPlayer.prepare(mediaSource, resetPosition, resetState); updatePlaybackInfo( playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + TIMELINE_CHANGE_REASON_RESET, /* seekProcessed= */ false); } - @Override - @Deprecated - public void prepare(MediaSource mediaSource) { - setMediaItem(mediaSource); - prepare(); - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - setMediaItem( - mediaSource, /* startPositionMs= */ resetPosition ? C.TIME_UNSET : getCurrentPosition()); - prepare(); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - setMediaItems(Collections.singletonList(mediaItem)); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); - } - - @Override - public void setMediaItems(List mediaItems) { - setMediaItems( - mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs */ C.TIME_UNSET); - } - - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - setMediaItems( - mediaItems, - /* startWindowIndex= */ resetPosition ? C.INDEX_UNSET : getCurrentWindowIndex(), - /* startPositionMs= */ resetPosition ? C.TIME_UNSET : getCurrentPosition()); - } - - @Override - public void setMediaItems( - List mediaItems, int startWindowIndex, long startPositionMs) { - pendingOperationAcks++; - if (!mediaSourceHolders.isEmpty()) { - removeMediaSourceHolders( - /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); - } - List holders = addMediaSourceHolders(/* index= */ 0, mediaItems); - Timeline timeline = maskTimeline(); - internalPlayer.setMediaItems( - holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } - - @Override - public void addMediaItem(MediaSource mediaSource) { - addMediaItems(Collections.singletonList(mediaSource)); - } - - @Override - public void addMediaItem(int index, MediaSource mediaSource) { - addMediaItems(index, Collections.singletonList(mediaSource)); - } - - @Override - public void addMediaItems(List mediaSources) { - addMediaItems(/* index= */ mediaSourceHolders.size(), mediaSources); - } - - @Override - public void addMediaItems(int index, List mediaSources) { - Assertions.checkArgument(index >= 0); - pendingOperationAcks++; - List holders = addMediaSourceHolders(index, mediaSources); - Timeline timeline = maskTimeline(); - internalPlayer.addMediaItems(index, holders, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } - - @Override - public MediaSource removeMediaItem(int index) { - List mediaSourceHolders = - removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); - return mediaSourceHolders.isEmpty() ? null : mediaSourceHolders.get(0).mediaSource; - } - - @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( - fromIndex >= 0 - && fromIndex <= toIndex - && toIndex <= mediaSourceHolders.size() - && newFromIndex >= 0); - pendingOperationAcks++; - newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); - Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); - Timeline timeline = maskTimeline(); - internalPlayer.moveMediaItems(fromIndex, toIndex, newFromIndex, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } - - @Override - public void clearMediaItems() { - if (mediaSourceHolders.isEmpty()) { - return; - } - removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); - } - - @Override - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - pendingOperationAcks++; - this.shuffleOrder = shuffleOrder; - Timeline timeline = maskTimeline(); - internalPlayer.setShuffleOrder(shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } @Override public void setPlayWhenReady(boolean playWhenReady) { @@ -550,9 +404,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void stop(boolean reset) { + if (reset) { + mediaSource = null; + } PlaybackInfo playbackInfo = getResetPlaybackInfo( - /* clearPlaylist= */ reset, + /* resetPosition= */ reset, + /* resetState= */ reset, /* resetError= */ reset, /* playbackState= */ Player.STATE_IDLE); // Trigger internal stop first before updating the playback info and notifying external @@ -565,7 +423,7 @@ import java.util.concurrent.CopyOnWriteArrayList; playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + TIMELINE_CHANGE_REASON_RESET, /* seekProcessed= */ false); } @@ -574,11 +432,13 @@ import java.util.concurrent.CopyOnWriteArrayList; Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + ExoPlayerLibraryInfo.registeredModules() + "]"); + mediaSource = null; internalPlayer.release(); eventHandler.removeCallbacksAndMessages(null); playbackInfo = getResetPlaybackInfo( - /* clearPlaylist= */ false, + /* resetPosition= */ false, + /* resetState= */ false, /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } @@ -726,11 +586,10 @@ import java.util.concurrent.CopyOnWriteArrayList; // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { - switch (msg.what) { case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: handlePlaybackInfo( - /* playbackInfo= */ (PlaybackInfo) msg.obj, + (PlaybackInfo) msg.obj, /* operationAcks= */ msg.arg1, /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, /* positionDiscontinuityReason= */ msg.arg2); @@ -778,23 +637,29 @@ import java.util.concurrent.CopyOnWriteArrayList; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } + @Player.TimelineChangeReason + int timelineChangeReason = + hasPendingPrepare + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; boolean seekProcessed = hasPendingSeek; + hasPendingPrepare = false; hasPendingSeek = false; updatePlaybackInfo( playbackInfo, positionDiscontinuity, positionDiscontinuityReason, - TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + timelineChangeReason, seekProcessed); } } private PlaybackInfo getResetPlaybackInfo( - boolean clearPlaylist, boolean resetError, @Player.State int playbackState) { - if (clearPlaylist) { - // Reset list of media source holders which are used for creating the masking timeline. - removeMediaSourceHolders( - /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + boolean resetPosition, + boolean resetState, + boolean resetError, + @Player.State int playbackState) { + if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; maskingWindowPositionMs = 0; @@ -803,22 +668,24 @@ import java.util.concurrent.CopyOnWriteArrayList; maskingPeriodIndex = getCurrentPeriodIndex(); maskingWindowPositionMs = getCurrentPosition(); } + // Also reset period-based PlaybackInfo positions if resetting the state. + resetPosition = resetPosition || resetState; MediaPeriodId mediaPeriodId = - clearPlaylist + resetPosition ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) : playbackInfo.periodId; - long startPositionUs = clearPlaylist ? 0 : playbackInfo.positionUs; - long contentPositionUs = clearPlaylist ? C.TIME_UNSET : playbackInfo.contentPositionUs; + long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; + long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; return new PlaybackInfo( - clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? Timeline.EMPTY : playbackInfo.timeline, mediaPeriodId, startPositionUs, contentPositionUs, playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, startPositionUs, /* totalBufferedDurationUs= */ 0, @@ -828,8 +695,8 @@ import java.util.concurrent.CopyOnWriteArrayList; private void updatePlaybackInfo( PlaybackInfo playbackInfo, boolean positionDiscontinuity, - @DiscontinuityReason int positionDiscontinuityReason, - @TimelineChangeReason int timelineChangeReason, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, boolean seekProcessed) { boolean previousIsPlaying = isPlaying(); // Assign playback info immediately such that all getters return the right values. @@ -850,53 +717,6 @@ import java.util.concurrent.CopyOnWriteArrayList; /* isPlayingChanged= */ previousIsPlaying != isPlaying)); } - private List addMediaSourceHolders( - int index, List mediaSources) { - List holders = new ArrayList<>(); - for (int i = 0; i < mediaSources.size(); i++) { - Playlist.MediaSourceHolder holder = - new Playlist.MediaSourceHolder(mediaSources.get(i), useLazyPreparation); - holders.add(holder); - mediaSourceHolders.add(i + index, holder); - } - shuffleOrder = - shuffleOrder.cloneAndInsert( - /* insertionIndex= */ index, /* insertionCount= */ holders.size()); - return holders; - } - - private List removeMediaItemsInternal(int fromIndex, int toIndex) { - Assertions.checkArgument( - fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); - pendingOperationAcks++; - List mediaSourceHolders = - removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); - Timeline timeline = maskTimeline(); - internalPlayer.removeMediaItems(fromIndex, toIndex, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - return mediaSourceHolders; - } - - private List removeMediaSourceHolders( - int fromIndex, int toIndexExclusive) { - List removed = new ArrayList<>(); - for (int i = toIndexExclusive - 1; i >= fromIndex; i--) { - removed.add(mediaSourceHolders.remove(i)); - } - shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); - return removed; - } - - private Timeline maskTimeline() { - playbackInfo = - playbackInfo.copyWithTimeline( - mediaSourceHolders.isEmpty() - ? Timeline.EMPTY - : new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); - return playbackInfo.timeline; - } - private void notifyListeners(ListenerInvocation listenerInvocation) { CopyOnWriteArrayList listenerSnapshot = new CopyOnWriteArrayList<>(listeners); notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation)); @@ -932,7 +752,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final TrackSelector trackSelector; private final boolean positionDiscontinuity; private final @Player.DiscontinuityReason int positionDiscontinuityReason; - private final int timelineChangeReason; + private final @Player.TimelineChangeReason int timelineChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; private final boolean playbackErrorChanged; @@ -966,16 +786,15 @@ import java.util.concurrent.CopyOnWriteArrayList; playbackErrorChanged = previousPlaybackInfo.playbackError != playbackInfo.playbackError && playbackInfo.playbackError != null; + timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; - timelineChanged = - !Util.areTimelinesSame(previousPlaybackInfo.timeline, playbackInfo.timeline); trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; } @Override public void run() { - if (timelineChanged) { + if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { invokeAll( listenerSnapshot, listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 9cad08ab7b..9d0692cf0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -26,11 +26,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; 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.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** Implements the internal behavior of {@link ExoPlayerImpl}. */ @@ -53,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - Playlist.PlaylistInfoRefreshListener, + MediaSourceCaller, PlaybackParameterListener, PlayerMessage.Sender { @@ -72,21 +71,16 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SET_SEEK_PARAMETERS = 5; private static final int MSG_STOP = 6; private static final int MSG_RELEASE = 7; - private static final int MSG_PERIOD_PREPARED = 8; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; - private static final int MSG_SET_REPEAT_MODE = 11; - private static final int MSG_SET_SHUFFLE_ENABLED = 12; - private static final int MSG_SET_FOREGROUND_MODE = 13; - private static final int MSG_SEND_MESSAGE = 14; - private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; - private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16; - private static final int MSG_SET_MEDIA_ITEMS = 17; - private static final int MSG_ADD_MEDIA_ITEMS = 18; - private static final int MSG_MOVE_MEDIA_ITEMS = 19; - private static final int MSG_REMOVE_MEDIA_ITEMS = 20; - private static final int MSG_SET_SHUFFLE_ORDER = 21; - private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22; + private static final int MSG_REFRESH_SOURCE_INFO = 8; + private static final int MSG_PERIOD_PREPARED = 9; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; + private static final int MSG_SET_REPEAT_MODE = 12; + private static final int MSG_SET_SHUFFLE_ENABLED = 13; + private static final int MSG_SET_FOREGROUND_MODE = 14; + private static final int MSG_SEND_MESSAGE = 15; + private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; + private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -109,12 +103,12 @@ import java.util.concurrent.atomic.AtomicBoolean; private final ArrayList pendingMessages; private final Clock clock; private final MediaPeriodQueue queue; - private final Playlist playlist; @SuppressWarnings("unused") private SeekParameters seekParameters; private PlaybackInfo playbackInfo; + private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; private boolean playWhenReady; @@ -123,7 +117,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean shuffleModeEnabled; private boolean foregroundMode; - @Nullable private SeekPosition pendingInitialSeekPosition; + private int pendingPrepareCount; + private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private int nextPendingMessageIndex; @@ -136,7 +131,6 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, - @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock) { this.renderers = renderers; @@ -176,14 +170,12 @@ import java.util.concurrent.atomic.AtomicBoolean; new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = clock.createHandler(internalPlaybackThread.getLooper(), this); - playlist = new Playlist(this); - if (analyticsCollector != null) { - playlist.setAnalyticsCollector(eventHandler, analyticsCollector); - } } - public void prepare() { - handler.obtainMessage(MSG_PREPARE).sendToTarget(); + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + handler + .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource) + .sendToTarget(); } public void setPlayWhenReady(boolean playWhenReady) { @@ -216,62 +208,6 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void setMediaItems( - List mediaSources, ShuffleOrder shuffleOrder) { - setMediaItems( - mediaSources, - /* windowIndex= */ C.INDEX_UNSET, - /* positionUs= */ C.TIME_UNSET, - shuffleOrder); - } - - public void setMediaItems( - List mediaSources, - int windowIndex, - long positionUs, - ShuffleOrder shuffleOrder) { - handler - .obtainMessage( - MSG_SET_MEDIA_ITEMS, - new PlaylistUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs)) - .sendToTarget(); - } - - public void addMediaItems( - List mediaSources, ShuffleOrder shuffleOrder) { - addMediaItems(C.INDEX_UNSET, mediaSources, shuffleOrder); - } - - public void addMediaItems( - int index, List mediaSources, ShuffleOrder shuffleOrder) { - handler - .obtainMessage( - MSG_ADD_MEDIA_ITEMS, - index, - /* ignored */ 0, - new PlaylistUpdateMessage( - mediaSources, - shuffleOrder, - /* windowIndex= */ C.INDEX_UNSET, - /* positionUs= */ C.TIME_UNSET)) - .sendToTarget(); - } - - public void removeMediaItems(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { - handler.obtainMessage(MSG_REMOVE_MEDIA_ITEMS, fromIndex, toIndex, shuffleOrder).sendToTarget(); - } - - public void moveMediaItems( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - MoveMediaItemsMessage moveMediaItemsMessage = - new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder); - handler.obtainMessage(MSG_MOVE_MEDIA_ITEMS, moveMediaItemsMessage).sendToTarget(); - } - - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget(); - } - @Override public synchronized void sendMessage(PlayerMessage message) { if (released) { @@ -328,11 +264,13 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // Playlist.PlaylistInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override - public void onPlaylistUpdateRequested() { - handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + handler + .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) + .sendToTarget(); } // MediaPeriod.Callback implementation. @@ -364,12 +302,14 @@ import java.util.concurrent.atomic.AtomicBoolean; // Handler.Callback implementation. @Override - @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { try { switch (msg.what) { case MSG_PREPARE: - prepareInternal(); + prepareInternal( + (MediaSource) msg.obj, + /* resetPosition= */ msg.arg1 != 0, + /* resetState= */ msg.arg2 != 0); break; case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); @@ -405,6 +345,9 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); break; + case MSG_REFRESH_SOURCE_INFO: + handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); + break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); break; @@ -421,24 +364,6 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SEND_MESSAGE_TO_TARGET_THREAD: sendMessageToTargetThread((PlayerMessage) msg.obj); break; - case MSG_SET_MEDIA_ITEMS: - setMediaItemsInternal((PlaylistUpdateMessage) msg.obj); - break; - case MSG_ADD_MEDIA_ITEMS: - addMediaItemsInternal((PlaylistUpdateMessage) msg.obj, msg.arg1); - break; - case MSG_MOVE_MEDIA_ITEMS: - moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj); - break; - case MSG_REMOVE_MEDIA_ITEMS: - removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj); - break; - case MSG_SET_SHUFFLE_ORDER: - setShuffleOrderInternal((ShuffleOrder) msg.obj); - break; - case MSG_PLAYLIST_UPDATE_REQUESTED: - playlistUpdateRequestedInternal(); - break; case MSG_RELEASE: releaseInternal(); // Return immediately to not send playback info updates after release. @@ -508,77 +433,21 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void prepareInternal() { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + pendingPrepareCount++; resetInternal( /* resetRenderers= */ false, - /* resetPosition= */ false, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, + /* releaseMediaSource= */ true, + resetPosition, + resetState, /* resetError= */ true); loadControl.onPrepared(); - setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING); - playlist.prepare(bandwidthMeter.getTransferListener()); + this.mediaSource = mediaSource; + setState(Player.STATE_BUFFERING); + mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - private void setMediaItemsInternal(PlaylistUpdateMessage playlistUpdateMessage) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - if (playlistUpdateMessage.windowIndex != C.INDEX_UNSET) { - pendingInitialSeekPosition = - new SeekPosition( - new Playlist.PlaylistTimeline( - playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder), - playlistUpdateMessage.windowIndex, - playlistUpdateMessage.positionUs); - } - Timeline timeline = - playlist.setMediaSources( - playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void addMediaItemsInternal(PlaylistUpdateMessage addMessage, int insertionIndex) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = - playlist.addMediaSources( - insertionIndex == C.INDEX_UNSET ? playlist.getSize() : insertionIndex, - addMessage.mediaSourceHolders, - addMessage.shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = - playlist.moveMediaSourceRange( - moveMediaItemsMessage.fromIndex, - moveMediaItemsMessage.toIndex, - moveMediaItemsMessage.newFromIndex, - moveMediaItemsMessage.shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = playlist.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void playlistUpdateRequestedInternal() throws ExoPlaybackException { - handlePlaylistInfoRefreshed(playlist.createTimeline()); - } - - private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = playlist.setShuffleOrder(shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { rebuffering = false; this.playWhenReady = playWhenReady; @@ -796,7 +665,6 @@ import java.util.concurrent.atomic.AtomicBoolean; long periodPositionUs; long contentPositionUs; boolean seekPositionAdjusted; - @Nullable Pair resolvedSeekPosition = resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (resolvedSeekPosition == null) { @@ -821,7 +689,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } try { - if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { + if (mediaSource == null || pendingPrepareCount > 0) { // Save seek position for later, as we are still waiting for a prepared source. pendingInitialSeekPosition = seekPosition; } else if (periodPositionUs == C.TIME_UNSET) { @@ -829,9 +697,9 @@ import java.util.concurrent.atomic.AtomicBoolean; setState(Player.STATE_ENDED); resetInternal( /* resetRenderers= */ false, + /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, + /* resetState= */ false, /* resetError= */ true); } else { // Execute the seek in the current media periods. @@ -978,11 +846,13 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { resetInternal( /* resetRenderers= */ forceResetRenderers || !foregroundMode, + /* releaseMediaSource= */ true, /* resetPosition= */ resetPositionAndState, - /* releasePlaylist= */ true, - /* clearPlaylist= */ resetPositionAndState, + /* resetState= */ resetPositionAndState, /* resetError= */ resetPositionAndState); - playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0); + playbackInfoUpdate.incrementPendingOperationAcks( + pendingPrepareCount + (acknowledgeStop ? 1 : 0)); + pendingPrepareCount = 0; loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -990,9 +860,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void releaseInternal() { resetInternal( /* resetRenderers= */ true, + /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* releasePlaylist= */ true, - /* clearPlaylist= */ true, + /* resetState= */ true, /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); @@ -1005,9 +875,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void resetInternal( boolean resetRenderers, + boolean releaseMediaSource, boolean resetPosition, - boolean releasePlaylist, - boolean clearPlaylist, + boolean resetState, boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; @@ -1035,8 +905,8 @@ import java.util.concurrent.atomic.AtomicBoolean; if (resetPosition) { pendingInitialSeekPosition = null; - } else if (clearPlaylist) { - // When clearing the playlist, also reset the period-based PlaybackInfo position and convert + } else if (resetState) { + // When resetting the state, also reset the period-based PlaybackInfo position and convert // existing position to initial seek instead. resetPosition = true; if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) { @@ -1047,10 +917,10 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist); + queue.clear(/* keepFrontPeriodUid= */ !resetState); setIsLoading(false); - if (clearPlaylist) { - queue.setTimeline(playlist.clear(/* shuffleOrder= */ null)); + if (resetState) { + queue.setTimeline(Timeline.EMPTY); for (PendingMessageInfo pendingMessageInfo : pendingMessages) { pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } @@ -1066,21 +936,24 @@ import java.util.concurrent.atomic.AtomicBoolean; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; playbackInfo = new PlaybackInfo( - clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? Timeline.EMPTY : playbackInfo.timeline, mediaPeriodId, startPositionUs, contentPositionUs, playbackInfo.playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs); - if (releasePlaylist) { - playlist.release(); + if (releaseMediaSource) { + if (mediaSource != null) { + mediaSource.releaseSource(/* caller= */ this); + mediaSource = null; + } } } @@ -1088,7 +961,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (message.getPositionMs() == C.TIME_UNSET) { // If no delivery time is specified, trigger immediate message delivery. sendMessageToTarget(message); - } else if (playbackInfo.timeline.isEmpty()) { + } else if (mediaSource == null || pendingPrepareCount > 0) { // Still waiting for initial timeline to resolve position. pendingMessages.add(new PendingMessageInfo(message)); } else { @@ -1403,11 +1276,20 @@ import java.util.concurrent.atomic.AtomicBoolean; } } } - playlist.maybeThrowSourceInfoRefreshError(); + mediaSource.maybeThrowSourceInfoRefreshError(); } - private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException { + private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) + throws ExoPlaybackException { + if (sourceRefreshInfo.source != mediaSource) { + // Stale event. + return; + } + playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); + pendingPrepareCount = 0; + Timeline oldTimeline = playbackInfo.timeline; + Timeline timeline = sourceRefreshInfo.timeline; queue.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline); resolvePendingMessagePositions(); @@ -1418,7 +1300,6 @@ import java.util.concurrent.atomic.AtomicBoolean; long newContentPositionUs = oldContentPositionUs; if (pendingInitialSeekPosition != null) { // Resolve initial seek position. - @Nullable Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; @@ -1523,12 +1404,12 @@ import java.util.concurrent.atomic.AtomicBoolean; if (playbackInfo.playbackState != Player.STATE_IDLE) { setState(Player.STATE_ENDED); } - // Reset, but retain the playlist so that it can still be used should a seek occur. + // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* resetRenderers= */ false, + /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, + /* resetState= */ false, /* resetError= */ true); } @@ -1571,7 +1452,6 @@ import java.util.concurrent.atomic.AtomicBoolean; * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - @Nullable private Pair resolveSeekPosition( SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; @@ -1628,9 +1508,13 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updatePeriods() throws ExoPlaybackException, IOException { - if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { + if (mediaSource == null) { + // The player has no media source yet. + return; + } + if (pendingPrepareCount > 0) { // We're waiting to get information about periods. - playlist.maybeThrowSourceInfoRefreshError(); + mediaSource.maybeThrowSourceInfoRefreshError(); return; } maybeUpdateLoadingPeriod(); @@ -1650,7 +1534,7 @@ import java.util.concurrent.atomic.AtomicBoolean; rendererCapabilities, trackSelector, loadControl.getAllocator(), - playlist, + mediaSource, info, emptyTrackSelectorResult); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); @@ -1661,7 +1545,7 @@ import java.util.concurrent.atomic.AtomicBoolean; handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } } - @Nullable MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (!playbackInfo.isLoading) { @@ -1670,7 +1554,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void maybeUpdateReadingPeriod() throws ExoPlaybackException { - @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; } @@ -2079,38 +1963,14 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private static final class PlaylistUpdateMessage { + private static final class MediaSourceRefreshInfo { - private final List mediaSourceHolders; - private final ShuffleOrder shuffleOrder; - private final int windowIndex; - private final long positionUs; + public final MediaSource source; + public final Timeline timeline; - private PlaylistUpdateMessage( - List mediaSourceHolders, - ShuffleOrder shuffleOrder, - int windowIndex, - long positionUs) { - this.mediaSourceHolders = mediaSourceHolders; - this.shuffleOrder = shuffleOrder; - this.windowIndex = windowIndex; - this.positionUs = positionUs; - } - } - - private static class MoveMediaItemsMessage { - - public final int fromIndex; - public final int toIndex; - public final int newFromIndex; - public final ShuffleOrder shuffleOrder; - - public MoveMediaItemsMessage( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - this.fromIndex = fromIndex; - this.toIndex = toIndex; - this.newFromIndex = newFromIndex; - this.shuffleOrder = shuffleOrder; + public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { + this.source = source; + this.timeline = timeline; } } @@ -2119,7 +1979,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private PlaybackInfo lastPlaybackInfo; private int operationAcks; private boolean positionDiscontinuity; - @DiscontinuityReason private int discontinuityReason; + private @DiscontinuityReason int discontinuityReason; public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; @@ -2147,4 +2007,5 @@ import java.util.concurrent.atomic.AtomicBoolean; this.discontinuityReason = discontinuityReason; } } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 5bbbcbea2a..850d2b7d10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; 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.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final boolean[] mayRetainStreamFlags; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; - private final Playlist playlist; + private final MediaSource mediaSource; @Nullable private MediaPeriodHolder next; private TrackGroupArray trackGroups; @@ -69,7 +70,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. - * @param playlist The playlist. + * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. @@ -79,13 +80,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, - Playlist playlist, + MediaSource mediaSource, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; - this.playlist = playlist; + this.mediaSource = mediaSource; this.uid = info.id.periodUid; this.info = info; this.trackGroups = TrackGroupArray.EMPTY; @@ -93,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = - createMediaPeriod(info.id, playlist, allocator, info.startPositionUs, info.endPositionUs); + createMediaPeriod( + info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); } /** @@ -303,7 +305,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod); + releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); } /** @@ -400,11 +402,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( MediaPeriodId id, - Playlist playlist, + MediaSource mediaSource, Allocator allocator, long startPositionUs, long endPositionUs) { - MediaPeriod mediaPeriod = playlist.createPeriod(id, allocator, startPositionUs); + MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( @@ -415,12 +417,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ private static void releaseMediaPeriod( - long endPositionUs, Playlist playlist, MediaPeriod mediaPeriod) { + long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) { try { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { - playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { - playlist.releasePeriod(mediaPeriod); + mediaSource.releasePeriod(mediaPeriod); } } catch (RuntimeException e) { // There's nothing we can do. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 5b39db54aa..901b7b4d94 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -19,6 +19,7 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.RepeatMode; 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.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -133,7 +134,7 @@ import com.google.android.exoplayer2.util.Assertions; * @param rendererCapabilities The renderer capabilities. * @param trackSelector The track selector. * @param allocator The allocator. - * @param playlist The playlist. + * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. @@ -142,7 +143,7 @@ import com.google.android.exoplayer2.util.Assertions; RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, - Playlist playlist, + MediaSource mediaSource, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = @@ -157,7 +158,7 @@ import com.google.android.exoplayer2.util.Assertions; rendererPositionOffsetUs, trackSelector, allocator, - playlist, + mediaSource, info, emptyTrackSelectorResult); if (loading != null) { 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 b9ab69c45f..fafbd25c32 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 @@ -356,8 +356,7 @@ public interface Player { * {@link #onPositionDiscontinuity(int)}. * * @param timeline The latest timeline. Never null, but may be empty. - * @param manifest The latest manifest in case the timeline has a single window only. Always - * null if the timeline has more than a single window. + * @param manifest The latest manifest. May be null. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, @@ -585,17 +584,25 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link - * #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}. + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link + * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. */ @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE}) + @IntDef({ + TIMELINE_CHANGE_REASON_PREPARED, + TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC + }) @interface TimelineChangeReason {} - /** Timeline changed as a result of a change of the playlist items or the order of the items. */ - int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0; - /** Timeline changed as a result of a dynamic update introduced by the played media. */ - int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; + /** Timeline and manifest changed as a result of a player initialization with new media. */ + int TIMELINE_CHANGE_REASON_PREPARED = 0; + /** Timeline and manifest changed as a result of a player reset. */ + int TIMELINE_CHANGE_REASON_RESET = 1; + /** + * Timeline or manifest changed as a result of an dynamic update introduced by the played media. + */ + int TIMELINE_CHANGE_REASON_DYNAMIC = 2; /** Returns the component of this player for audio output, or null if audio is not supported. */ @Nullable diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java deleted file mode 100644 index c5476a151b..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java +++ /dev/null @@ -1,708 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2; - -import android.os.Handler; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; -import com.google.android.exoplayer2.source.MaskingMediaPeriod; -import com.google.android.exoplayer2.source.MaskingMediaSource; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified - * during playback. It is valid for the same {@link MediaSource} instance to be present more than - * once in the playlist. - * - *

With the exception of the constructor, all methods are called on the playback thread. - */ -/* package */ class Playlist { - - /** Listener for source events. */ - public interface PlaylistInfoRefreshListener { - - /** - * Called when the timeline of a media item has changed and a new timeline that reflects the - * current playlist state needs to be created by calling {@link #createTimeline()}. - * - *

Called on the playback thread. - */ - void onPlaylistUpdateRequested(); - } - - private final List mediaSourceHolders; - private final Map mediaSourceByMediaPeriod; - private final Map mediaSourceByUid; - private final PlaylistInfoRefreshListener playlistInfoListener; - private final MediaSourceEventListener.EventDispatcher eventDispatcher; - private final HashMap childSources; - private final Set enabledMediaSourceHolders; - - private ShuffleOrder shuffleOrder; - private boolean isPrepared; - - @Nullable private TransferListener mediaTransferListener; - - @SuppressWarnings("initialization") - public Playlist(PlaylistInfoRefreshListener listener) { - playlistInfoListener = listener; - shuffleOrder = new DefaultShuffleOrder(0); - mediaSourceByMediaPeriod = new IdentityHashMap<>(); - mediaSourceByUid = new HashMap<>(); - mediaSourceHolders = new ArrayList<>(); - eventDispatcher = new MediaSourceEventListener.EventDispatcher(); - childSources = new HashMap<>(); - enabledMediaSourceHolders = new HashSet<>(); - } - - /** - * Sets the media sources replacing any sources previously contained in the playlist. - * - * @param holders The list of {@link MediaSourceHolder}s to set. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - */ - public final Timeline setMediaSources( - List holders, ShuffleOrder shuffleOrder) { - removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); - return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder); - } - - /** - * Adds multiple {@link MediaSourceHolder}s to the playlist. - * - * @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index - * must be in the range of 0 <= index <= {@link #getSize()}. - * @param holders A list of {@link MediaSourceHolder}s to be added. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - */ - public final Timeline addMediaSources( - int index, List holders, ShuffleOrder shuffleOrder) { - if (!holders.isEmpty()) { - this.shuffleOrder = shuffleOrder; - for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) { - MediaSourceHolder holder = holders.get(insertionIndex - index); - if (insertionIndex > 0) { - MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1); - Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); - holder.reset( - /* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild - + previousTimeline.getWindowCount()); - } else { - holder.reset(/* firstWindowIndexInChild= */ 0); - } - Timeline newTimeline = holder.mediaSource.getTimeline(); - correctOffsets( - /* startIndex= */ insertionIndex, - /* windowOffsetUpdate= */ newTimeline.getWindowCount()); - mediaSourceHolders.add(insertionIndex, holder); - mediaSourceByUid.put(holder.uid, holder); - if (isPrepared) { - prepareChildSource(holder); - if (mediaSourceByMediaPeriod.isEmpty()) { - enabledMediaSourceHolders.add(holder); - } else { - disableChildSource(holder); - } - } - } - } - return createTimeline(); - } - - /** - * Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index - * (included) and a final index (excluded). - * - *

Note: when specified range is empty, no actual media source is removed and no exception is - * thrown. - * - * @param fromIndex The initial range index, pointing to the first media source that will be - * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param toIndex The final range index, pointing to the first media source that will be left - * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, - * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} - */ - public final Timeline removeMediaSourceRange( - int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { - Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize()); - this.shuffleOrder = shuffleOrder; - removeMediaSourcesInternal(fromIndex, toIndex); - return createTimeline(); - } - - /** - * Moves an existing media source within the playlist. - * - * @param currentIndex The current index of the media source in the playlist. This index must be - * in the range of 0 <= index < {@link #getSize()}. - * @param newIndex The target index of the media source in the playlist. This index must be in the - * range of 0 <= index < {@link #getSize()}. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0, - * {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0 - */ - public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) { - return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder); - } - - /** - * Moves a range of media sources within the playlist. - * - *

Note: when specified range is empty or the from index equals the new from index, no actual - * media source is moved and no exception is thrown. - * - * @param fromIndex The initial range index, pointing to the first media source of the range that - * will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param toIndex The final range index, pointing to the first media source that will be left - * untouched. This index must be larger or equals than {@code fromIndex}. - * @param newFromIndex The target index of the first media source of the range that will be moved. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, - * {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code - * newFromIndex} < 0 - */ - public Timeline moveMediaSourceRange( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - Assertions.checkArgument( - fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0); - this.shuffleOrder = shuffleOrder; - if (fromIndex == toIndex || fromIndex == newFromIndex) { - return createTimeline(); - } - int startIndex = Math.min(fromIndex, newFromIndex); - int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1; - int endIndex = Math.max(newEndIndex, toIndex - 1); - int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; - moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); - for (int i = startIndex; i <= endIndex; i++) { - MediaSourceHolder holder = mediaSourceHolders.get(i); - holder.firstWindowIndexInChild = windowOffset; - windowOffset += holder.mediaSource.getTimeline().getWindowCount(); - } - return createTimeline(); - } - - /** Clears the playlist. */ - public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) { - this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear(); - removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize()); - return createTimeline(); - } - - /** Whether the playlist is prepared. */ - public final boolean isPrepared() { - return isPrepared; - } - - /** Returns the number of media sources in the playlist. */ - public final int getSize() { - return mediaSourceHolders.size(); - } - - /** - * Sets the {@link AnalyticsCollector}. - * - * @param handler The handler on which to call the collector. - * @param analyticsCollector The analytics collector. - */ - public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) { - eventDispatcher.addEventListener(handler, analyticsCollector); - } - - /** - * Sets a new shuffle order to use when shuffling the child media sources. - * - * @param shuffleOrder A {@link ShuffleOrder}. - */ - public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) { - int size = getSize(); - if (shuffleOrder.getLength() != size) { - shuffleOrder = - shuffleOrder - .cloneAndClear() - .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); - } - this.shuffleOrder = shuffleOrder; - return createTimeline(); - } - - /** Prepares the playlist. */ - public final void prepare(@Nullable TransferListener mediaTransferListener) { - Assertions.checkState(!isPrepared); - this.mediaTransferListener = mediaTransferListener; - for (int i = 0; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - prepareChildSource(mediaSourceHolder); - enabledMediaSourceHolders.add(mediaSourceHolder); - } - isPrepared = true; - } - - /** - * Returns a new {@link MediaPeriod} identified by {@code periodId}. - * - * @param id The identifier of the period. - * @param allocator An {@link Allocator} from which to obtain media buffer allocations. - * @param startPositionUs The expected start position, in microseconds. - * @return A new {@link MediaPeriod}. - */ - public MediaPeriod createPeriod( - MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) { - Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); - MediaSource.MediaPeriodId childMediaPeriodId = - id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); - MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); - enableMediaSource(holder); - holder.activeMediaPeriodIds.add(childMediaPeriodId); - MediaPeriod mediaPeriod = - holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); - mediaSourceByMediaPeriod.put(mediaPeriod, holder); - disableUnusedMediaSources(); - return mediaPeriod; - } - - /** - * Releases the period. - * - * @param mediaPeriod The period to release. - */ - public final void releasePeriod(MediaPeriod mediaPeriod) { - MediaSourceHolder holder = - Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); - holder.mediaSource.releasePeriod(mediaPeriod); - holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); - if (!mediaSourceByMediaPeriod.isEmpty()) { - disableUnusedMediaSources(); - } - maybeReleaseChildSource(holder); - } - - /** Releases the playlist. */ - public final void release() { - for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.caller); - childSource.mediaSource.removeEventListener(childSource.eventListener); - } - childSources.clear(); - enabledMediaSourceHolders.clear(); - isPrepared = false; - } - - /** Throws any pending error encountered while loading or refreshing. */ - public final void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - /** Creates a timeline reflecting the current state of the playlist. */ - public final Timeline createTimeline() { - if (mediaSourceHolders.isEmpty()) { - return Timeline.EMPTY; - } - int windowOffset = 0; - for (int i = 0; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - mediaSourceHolder.firstWindowIndexInChild = windowOffset; - windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount(); - } - return new PlaylistTimeline(mediaSourceHolders, shuffleOrder); - } - - // Internal methods. - - private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { - enabledMediaSourceHolders.add(mediaSourceHolder); - @Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder); - if (enabledChild != null) { - enabledChild.mediaSource.enable(enabledChild.caller); - } - } - - private void disableUnusedMediaSources() { - Iterator iterator = enabledMediaSourceHolders.iterator(); - while (iterator.hasNext()) { - MediaSourceHolder holder = iterator.next(); - if (holder.activeMediaPeriodIds.isEmpty()) { - disableChildSource(holder); - iterator.remove(); - } - } - } - - private void disableChildSource(MediaSourceHolder holder) { - @Nullable MediaSourceAndListener disabledChild = childSources.get(holder); - if (disabledChild != null) { - disabledChild.mediaSource.disable(disabledChild.caller); - } - } - - private void removeMediaSourcesInternal(int fromIndex, int toIndex) { - for (int index = toIndex - 1; index >= fromIndex; index--) { - MediaSourceHolder holder = mediaSourceHolders.remove(index); - mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.mediaSource.getTimeline(); - correctOffsets( - /* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount()); - holder.isRemoved = true; - if (isPrepared) { - maybeReleaseChildSource(holder); - } - } - } - - private void correctOffsets(int startIndex, int windowOffsetUpdate) { - for (int i = startIndex; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate; - } - } - - // Internal methods to manage child sources. - - @Nullable - private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) { - for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) { - // Ensure the reported media period id has the same window sequence number as the one created - // by this media source. Otherwise it does not belong to this child source. - if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber - == mediaPeriodId.windowSequenceNumber) { - Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); - return mediaPeriodId.copyWithPeriodUid(periodUid); - } - } - return null; - } - - private static int getWindowIndexForChildWindowIndex( - MediaSourceHolder mediaSourceHolder, int windowIndex) { - return windowIndex + mediaSourceHolder.firstWindowIndexInChild; - } - - private void prepareChildSource(MediaSourceHolder holder) { - MediaSource mediaSource = holder.mediaSource; - MediaSource.MediaSourceCaller caller = - (source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested(); - MediaSourceEventListener eventListener = new ForwardingEventListener(holder); - childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); - mediaSource.addEventListener(new Handler(), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); - } - - private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { - // Release if the source has been removed from the playlist and no periods are still active. - if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { - MediaSourceAndListener removedChild = - Assertions.checkNotNull(childSources.remove(mediaSourceHolder)); - removedChild.mediaSource.releaseSource(removedChild.caller); - removedChild.mediaSource.removeEventListener(removedChild.eventListener); - enabledMediaSourceHolders.remove(mediaSourceHolder); - } - } - - /** Return uid of media source holder from period uid of concatenated source. */ - private static Object getMediaSourceHolderUid(Object periodUid) { - return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); - } - - /** Return uid of child period from period uid of concatenated source. */ - private static Object getChildPeriodUid(Object periodUid) { - return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); - } - - private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { - return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid); - } - - /* package */ static void moveMediaSourceHolders( - List mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) { - MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex]; - for (int i = removedItems.length - 1; i >= 0; i--) { - removedItems[i] = mediaSourceHolders.remove(fromIndex + i); - } - mediaSourceHolders.addAll( - Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems)); - } - - /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder { - - public final MaskingMediaSource mediaSource; - public final Object uid; - public final List activeMediaPeriodIds; - - public int firstWindowIndexInChild; - public boolean isRemoved; - - public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { - this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); - this.activeMediaPeriodIds = new ArrayList<>(); - this.uid = new Object(); - } - - public void reset(int firstWindowIndexInChild) { - this.firstWindowIndexInChild = firstWindowIndexInChild; - this.isRemoved = false; - this.activeMediaPeriodIds.clear(); - } - } - - /** Timeline exposing concatenated timelines of playlist media sources. */ - /* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline { - - private final int windowCount; - private final int periodCount; - private final int[] firstPeriodInChildIndices; - private final int[] firstWindowInChildIndices; - private final Timeline[] timelines; - private final Object[] uids; - private final HashMap childIndexByUid; - - public PlaylistTimeline( - Collection mediaSourceHolders, ShuffleOrder shuffleOrder) { - super(/* isAtomic= */ false, shuffleOrder); - int childCount = mediaSourceHolders.size(); - firstPeriodInChildIndices = new int[childCount]; - firstWindowInChildIndices = new int[childCount]; - timelines = new Timeline[childCount]; - uids = new Object[childCount]; - childIndexByUid = new HashMap<>(); - int index = 0; - int windowCount = 0; - int periodCount = 0; - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - timelines[index] = mediaSourceHolder.mediaSource.getTimeline(); - firstWindowInChildIndices[index] = windowCount; - firstPeriodInChildIndices[index] = periodCount; - windowCount += timelines[index].getWindowCount(); - periodCount += timelines[index].getPeriodCount(); - uids[index] = mediaSourceHolder.uid; - childIndexByUid.put(uids[index], index++); - } - this.windowCount = windowCount; - this.periodCount = periodCount; - } - - @Override - protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); - } - - @Override - protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); - } - - @Override - protected int getChildIndexByChildUid(Object childUid) { - Integer index = childIndexByUid.get(childUid); - return index == null ? C.INDEX_UNSET : index; - } - - @Override - protected Timeline getTimelineByChildIndex(int childIndex) { - return timelines[childIndex]; - } - - @Override - protected int getFirstPeriodIndexByChildIndex(int childIndex) { - return firstPeriodInChildIndices[childIndex]; - } - - @Override - protected int getFirstWindowIndexByChildIndex(int childIndex) { - return firstWindowInChildIndices[childIndex]; - } - - @Override - protected Object getChildUidByChildIndex(int childIndex) { - return uids[childIndex]; - } - - @Override - public int getWindowCount() { - return windowCount; - } - - @Override - public int getPeriodCount() { - return periodCount; - } - } - - private static final class MediaSourceAndListener { - - public final MediaSource mediaSource; - public final MediaSource.MediaSourceCaller caller; - public final MediaSourceEventListener eventListener; - - public MediaSourceAndListener( - MediaSource mediaSource, - MediaSource.MediaSourceCaller caller, - MediaSourceEventListener eventListener) { - this.mediaSource = mediaSource; - this.caller = caller; - this.eventListener = eventListener; - } - } - - private final class ForwardingEventListener implements MediaSourceEventListener { - - private final Playlist.MediaSourceHolder id; - private EventDispatcher eventDispatcher; - - public ForwardingEventListener(Playlist.MediaSourceHolder id) { - eventDispatcher = Playlist.this.eventDispatcher; - this.id = id; - } - - @Override - public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodCreated(); - } - } - - @Override - public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodReleased(); - } - } - - @Override - public void onLoadStarted( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadStarted(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadCompleted( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadCompleted(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadCanceled( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadCanceled(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadError( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData, - IOException error, - boolean wasCanceled) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled); - } - } - - @Override - public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.readingStarted(); - } - } - - @Override - public void onUpstreamDiscarded( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.upstreamDiscarded(mediaLoadData); - } - } - - @Override - public void onDownstreamFormatChanged( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.downstreamFormatChanged(mediaLoadData); - } - } - - /** Updates the event dispatcher and returns whether the event should be dispatched. */ - private boolean maybeUpdateEventDispatcher( - int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { - @Nullable MediaSource.MediaPeriodId mediaPeriodId = null; - if (childMediaPeriodId != null) { - mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); - if (mediaPeriodId == null) { - // Media period not found. Ignore event. - return false; - } - } - int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); - if (eventDispatcher.windowIndex != windowIndex - || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { - eventDispatcher = - Playlist.this.eventDispatcher.withParameters( - windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L); - } - return true; - } - } -} - 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 de9802357c..43a5ebab99 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 @@ -43,7 +43,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; @@ -164,9 +163,7 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. * @param analyticsCollector An {@link AnalyticsCollector}. - * @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all - * initial preparation steps (e.g., manifest loads) happen immediately. If true, these - * initial preparations are triggered only when the player starts buffering the media. + * @param useLazyPreparation Whether media sources should be initialized lazily. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. */ public Builder( @@ -303,7 +300,6 @@ public class SimpleExoPlayer extends BasePlayer loadControl, bandwidthMeter, analyticsCollector, - useLazyPreparation, clock, looper); } @@ -343,6 +339,7 @@ public class SimpleExoPlayer extends BasePlayer private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; + @Nullable private MediaSource mediaSource; private List currentCues; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private CameraMotionListener cameraMotionListener; @@ -358,9 +355,6 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will * collect and forward all player events. - * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest - * loads and other initial preparation steps happen immediately. If true, these initial - * preparations are triggered only when the player starts buffering the media. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. * @param looper The {@link Looper} which must be used for all calls to the player and which is @@ -374,7 +368,6 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { this( @@ -385,14 +378,26 @@ public class SimpleExoPlayer extends BasePlayer DrmSessionManager.getDummyDrmSessionManager(), bandwidthMeter, analyticsCollector, - useLazyPreparation, clock, looper); } /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, AnalyticsCollector, boolean, Clock, Looper)} instead, and pass the {@link + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link * DrmSessionManager} to the {@link MediaSource} factories. */ @Deprecated @@ -404,7 +409,6 @@ public class SimpleExoPlayer extends BasePlayer @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; @@ -435,15 +439,7 @@ public class SimpleExoPlayer extends BasePlayer // Build the player and associated objects. player = - new ExoPlayerImpl( - renderers, - trackSelector, - loadControl, - bandwidthMeter, - analyticsCollector, - useLazyPreparation, - clock, - looper); + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); analyticsCollector.setPlayer(player); addListener(analyticsCollector); addListener(componentListener); @@ -1103,133 +1099,32 @@ public class SimpleExoPlayer extends BasePlayer } @Override - @Deprecated public void retry() { verifyApplicationThread(); - prepare(); + if (mediaSource != null + && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } } @Override - public void prepare() { - verifyApplicationThread(); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepare(); - } - - @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(); - setMediaItems( - Collections.singletonList(mediaSource), - /* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET, - /* startPositionMs= */ C.TIME_UNSET); - prepare(); - } - - @Override - public void setMediaItems(List mediaItems) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems); - } - - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems, resetPosition); - } - - @Override - public void setMediaItems( - List mediaItems, int startWindowIndex, long startPositionMs) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems, startWindowIndex, startPositionMs); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem, startPositionMs); - } - - @Override - public void addMediaItem(MediaSource mediaSource) { - verifyApplicationThread(); - player.addMediaItem(mediaSource); - } - - @Override - public void addMediaItem(int index, MediaSource mediaSource) { - verifyApplicationThread(); - player.addMediaItem(index, mediaSource); - } - - @Override - public void addMediaItems(List mediaSources) { - verifyApplicationThread(); - player.addMediaItems(mediaSources); - } - - @Override - public void addMediaItems(int index, List mediaSources) { - verifyApplicationThread(); - player.addMediaItems(index, mediaSources); - } - - @Override - public void moveMediaItem(int currentIndex, int newIndex) { - verifyApplicationThread(); - player.moveMediaItem(currentIndex, newIndex); - } - - @Override - public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - verifyApplicationThread(); - player.moveMediaItems(fromIndex, toIndex, newIndex); - } - - @Override - public MediaSource removeMediaItem(int index) { - verifyApplicationThread(); - return player.removeMediaItem(index); - } - - @Override - public void removeMediaItems(int fromIndex, int toIndex) { - verifyApplicationThread(); - player.removeMediaItems(fromIndex, toIndex); - } - - @Override - public void clearMediaItems() { - verifyApplicationThread(); - player.clearMediaItems(); - } - - @Override - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - verifyApplicationThread(); - player.setShuffleOrder(shuffleOrder); + 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); } @Override @@ -1309,7 +1204,6 @@ public class SimpleExoPlayer extends BasePlayer @Override public void setForegroundMode(boolean foregroundMode) { - verifyApplicationThread(); player.setForegroundMode(foregroundMode); } @@ -1317,6 +1211,13 @@ public class SimpleExoPlayer extends BasePlayer public void stop(boolean reset) { verifyApplicationThread(); player.stop(reset); + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + if (reset) { + mediaSource = null; + } + } audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @@ -1333,6 +1234,10 @@ public class SimpleExoPlayer extends BasePlayer } surface = null; } + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + mediaSource = null; + } if (isPriorityTaskManagerRegistered) { Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); isPriorityTaskManagerRegistered = false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 458532c86d..c496052f94 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -19,7 +19,6 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; /** * A flexible representation of the structure of media. A timeline is able to represent the @@ -271,46 +270,6 @@ public abstract class Timeline { return positionInFirstPeriodUs; } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !getClass().equals(obj.getClass())) { - return false; - } - Window that = (Window) obj; - return Util.areEqual(uid, that.uid) - && Util.areEqual(tag, that.tag) - && Util.areEqual(manifest, that.manifest) - && presentationStartTimeMs == that.presentationStartTimeMs - && windowStartTimeMs == that.windowStartTimeMs - && isSeekable == that.isSeekable - && isDynamic == that.isDynamic - && defaultPositionUs == that.defaultPositionUs - && durationUs == that.durationUs - && firstPeriodIndex == that.firstPeriodIndex - && lastPeriodIndex == that.lastPeriodIndex - && positionInFirstPeriodUs == that.positionInFirstPeriodUs; - } - - @Override - public int hashCode() { - int result = 7; - result = 31 * result + uid.hashCode(); - result = 31 * result + (tag == null ? 0 : tag.hashCode()); - result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); - result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); - result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); - result = 31 * result + (isSeekable ? 1 : 0); - result = 31 * result + (isDynamic ? 1 : 0); - result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + firstPeriodIndex; - result = 31 * result + lastPeriodIndex; - result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); - return result; - } } /** @@ -567,34 +526,6 @@ public abstract class Timeline { return adPlaybackState.adResumePositionUs; } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !getClass().equals(obj.getClass())) { - return false; - } - Period that = (Period) obj; - return Util.areEqual(id, that.id) - && Util.areEqual(uid, that.uid) - && windowIndex == that.windowIndex - && durationUs == that.durationUs - && positionInWindowUs == that.positionInWindowUs - && Util.areEqual(adPlaybackState, that.adPlaybackState); - } - - @Override - public int hashCode() { - int result = 7; - result = 31 * result + (id == null ? 0 : id.hashCode()); - result = 31 * result + (uid == null ? 0 : uid.hashCode()); - result = 31 * result + windowIndex; - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); - result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode()); - return result; - } } /** An empty timeline. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 2cb160d092..43154a4b3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -132,8 +132,11 @@ public class AnalyticsCollector } } - /** Resets the analytics collector for a new playlist. */ - public final void resetForNewPlaylist() { + /** + * Resets the analytics collector for a new media source. Should be called before the player is + * prepared with a new media source. + */ + public final void resetForNewMediaSource() { // Copying the list is needed because onMediaPeriodReleased will modify the list. List mediaPeriodInfos = new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); @@ -783,13 +786,9 @@ public class AnalyticsCollector /** Updates the queue with a newly created media period. */ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); - boolean isInTimeline = periodIndex != C.INDEX_UNSET; + boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET; MediaPeriodInfo mediaPeriodInfo = - new MediaPeriodInfo( - mediaPeriodId, - isInTimeline ? timeline : Timeline.EMPTY, - isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); + new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); @@ -805,7 +804,7 @@ public class AnalyticsCollector public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); if (mediaPeriodInfo == null) { - // The media period has already been removed from the queue in resetForNewPlaylist(). + // The media period has already been removed from the queue in resetForNewMediaSource(). return false; } mediaPeriodInfoQueue.remove(mediaPeriodInfo); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java similarity index 89% rename from library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index a307e4b35d..29ef1faa80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2; +package com.google.android.exoplayer2.source; import android.util.Pair; -import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Assertions; /** Abstract base class for the concatenation of one or more {@link Timeline}s. */ -public abstract class AbstractConcatenatedTimeline extends Timeline { +/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; private final ShuffleOrder shuffleOrder; @@ -74,8 +76,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled) { + public int getNextWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { if (isAtomic) { // Adapt repeat and shuffle mode to atomic concatenation. repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; @@ -84,10 +86,12 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); - int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( - windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, - shuffleModeEnabled); + int nextWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getNextWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } @@ -108,8 +112,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled) { + public int getPreviousWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { if (isAtomic) { // Adapt repeat and shuffle mode to atomic concatenation. repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; @@ -118,10 +122,12 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); - int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( - windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, - shuffleModeEnabled); + int previousWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getPreviousWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } @@ -219,8 +225,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { int childIndex = getChildIndexByPeriodIndex(periodIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); - getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, - setIds); + getTimelineByChildIndex(childIndex) + .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { period.uid = @@ -242,7 +248,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { return C.INDEX_UNSET; } int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); - return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET + return periodIndexInChild == C.INDEX_UNSET + ? C.INDEX_UNSET : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; } @@ -307,13 +314,14 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { protected abstract Object getChildUidByChildIndex(int childIndex); private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + return shuffleModeEnabled + ? shuffleOrder.getNextIndex(childIndex) : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; } private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + return shuffleModeEnabled + ? shuffleOrder.getPreviousIndex(childIndex) : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index cfd0ad9377..8dfea1e511 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -19,7 +19,6 @@ import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 68bed250e8..ac23e2a831 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 33bbf795be..8727fc5ed9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import android.util.Pair; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; @@ -62,7 +61,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Returns the {@link Timeline}. */ - public synchronized Timeline getTimeline() { + public Timeline getTimeline() { return timeline; } @@ -130,7 +129,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - protected synchronized void onChildSourceInfoRefreshed( + protected void onChildSourceInfoRefreshed( Void id, MediaSource mediaSource, Timeline newTimeline) { if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); @@ -294,8 +293,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ - @VisibleForTesting - public static final class DummyTimeline extends Timeline { + private static final class DummyTimeline extends Timeline { @Nullable private final Object tag; @@ -334,8 +332,8 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { return period.set( - /* id= */ setIds ? 0 : null, - /* uid= */ setIds ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : null, + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID, /* windowIndex= */ 0, /* durationUs = */ C.TIME_UNSET, /* positionInWindowUs= */ 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 66e78eb3a5..c37e98776e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -594,10 +594,12 @@ public class EventLogger implements AnalyticsListener { private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { switch (reason) { - case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE: - return "SOURCE_UPDATE"; - case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED: - return "PLAYLIST_CHANGED"; + case Player.TIMELINE_CHANGE_REASON_PREPARED: + return "PREPARED"; + case Player.TIMELINE_CHANGE_REASON_RESET: + return "RESET"; + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + return "DYNAMIC"; default: return "?"; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index cbd246cf22..e11aa53b0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -53,7 +53,6 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SeekParameters; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -2025,42 +2024,6 @@ public final class Util { } } - /** - * Checks whether the timelines are the same. - * - * @param firstTimeline The first {@link Timeline}. - * @param secondTimeline The second {@link Timeline} to compare with. - * @return {@code true} if the both timelines are the same. - */ - public static boolean areTimelinesSame(Timeline firstTimeline, Timeline secondTimeline) { - if (firstTimeline == secondTimeline) { - return true; - } - if (secondTimeline.getWindowCount() != firstTimeline.getWindowCount() - || secondTimeline.getPeriodCount() != firstTimeline.getPeriodCount()) { - return false; - } - Timeline.Window firstWindow = new Timeline.Window(); - Timeline.Period firstPeriod = new Timeline.Period(); - Timeline.Window secondWindow = new Timeline.Window(); - Timeline.Period secondPeriod = new Timeline.Period(); - for (int i = 0; i < firstTimeline.getWindowCount(); i++) { - if (!firstTimeline - .getWindow(i, firstWindow) - .equals(secondTimeline.getWindow(i, secondWindow))) { - return false; - } - } - for (int i = 0; i < firstTimeline.getPeriodCount(); i++) { - if (!firstTimeline - .getPeriod(i, firstPeriod, /* setIds= */ true) - .equals(secondTimeline.getPeriod(i, secondPeriod, /* setIds= */ true))) { - return false; - } - } - return true; - } - private static HashMap createIso3ToIso2Map() { String[] iso2Languages = Locale.getISOLanguages(); HashMap iso3ToIso2 = 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 ee51a28052..37c026db74 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,7 +16,6 @@ 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 android.content.Context; @@ -32,7 +31,6 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -87,12 +85,10 @@ public final class ExoPlayerTest { private static final int TIMEOUT_MS = 10000; private Context context; - private Timeline dummyTimeline; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); - dummyTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ 0); } /** @@ -102,7 +98,6 @@ public final class ExoPlayerTest { @Test public void testPlayEmptyTimeline() throws Exception { Timeline timeline = Timeline.EMPTY; - Timeline expectedMaskingTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); FakeRenderer renderer = new FakeRenderer(); ExoPlayerTestRunner testRunner = new Builder() @@ -112,10 +107,7 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesSame(expectedMaskingTimeline, Timeline.EMPTY); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); assertThat(renderer.formatReadCount).isEqualTo(0); assertThat(renderer.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.isEnded).isFalse(); @@ -136,10 +128,8 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.formatReadCount).isEqualTo(1); assertThat(renderer.sampleBufferReadCount).isEqualTo(1); @@ -161,10 +151,8 @@ public final class ExoPlayerTest { testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertThat(renderer.formatReadCount).isEqualTo(3); assertThat(renderer.sampleBufferReadCount).isEqualTo(3); assertThat(renderer.isEnded).isTrue(); @@ -187,10 +175,8 @@ public final class ExoPlayerTest { Integer[] expectedReasons = new Integer[99]; Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertThat(renderer.formatReadCount).isEqualTo(100); assertThat(renderer.sampleBufferReadCount).isEqualTo(100); assertThat(renderer.isEnded).isTrue(); @@ -262,17 +248,14 @@ public final class ExoPlayerTest { testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); assertThat(audioRenderer.positionResetCount).isEqualTo(1); assertThat(videoRenderer.isEnded).isTrue(); assertThat(audioRenderer.isEnded).isTrue(); } @Test - public void testResettingMediaItemsGivesFreshSourceInfo() throws Exception { + public void testRepreparationGivesFreshSourceInfo() throws Exception { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); @@ -288,8 +271,8 @@ public final class ExoPlayerTest { @Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); // We've queued a source info refresh on the playback thread's event queue. Allow the - // test thread to set the third source to the playlist, and block this thread (the - // playback thread) until the test thread's call to setMediaItems() has returned. + // test thread to prepare the player with the third source, and block this thread (the + // playback thread) until the test thread's call to prepare() has returned. queuedSourceInfoCountDownLatch.countDown(); try { completePreparationCountDownLatch.await(); @@ -304,13 +287,12 @@ public final class ExoPlayerTest { // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare // the player again with a source and a new manifest, which will never be exposed. Allow the - // test thread to set a third source, and block the playback thread until the test thread's call - // to setMediaItems() has returned. + // test thread to prepare the player with a third source, and block the playback thread until + // the test thread's call to prepare() has returned. ActionSchedule actionSchedule = - new ActionSchedule.Builder("testResettingMediaItemsGivesFreshSourceInfo") - .waitForTimelineChanged( - firstTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .setMediaItems(secondSource) + new ActionSchedule.Builder("testRepreparation") + .waitForTimelineChanged(firstTimeline) + .prepareSource(secondSource) .executeRunnable( () -> { try { @@ -319,32 +301,26 @@ public final class ExoPlayerTest { // Ignore. } }) - .setMediaItems(thirdSource) + .prepareSource(thirdSource) .executeRunnable(completePreparationCountDownLatch::countDown) - .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSources(firstSource) + .setMediaSource(firstSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - // The first source's preparation completed with a real timeline. When the second source was - // prepared, it immediately exposed a dummy timeline, but the source info refresh from the - // second source was suppressed as we replace it with the third source before the update - // arrives. - testRunner.assertTimelinesSame( - dummyTimeline, firstTimeline, dummyTimeline, dummyTimeline, thirdTimeline); + // The first source's preparation completed with a non-empty timeline. When the player was + // re-prepared with the second source, it immediately exposed an empty timeline, but the source + // info refresh from the second source was suppressed as we re-prepared with the third source. + testRunner.assertTimelinesEqual(firstTimeline, Timeline.EMPTY, thirdTimeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.isEnded).isTrue(); } @@ -356,8 +332,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .playUntilStartOfWindow(/* windowIndex= */ 1) .setRepeatMode(Player.REPEAT_MODE_ONE) .playUntilStartOfWindow(/* windowIndex= */ 1) @@ -392,10 +367,8 @@ public final class ExoPlayerTest { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertThat(renderer.isEnded).isTrue(); } @@ -424,7 +397,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) @@ -470,13 +443,12 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_READY) .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null)) - .waitForTimelineChanged( - adErrorTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(adErrorTimeline) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(fakeMediaSource) + .setMediaSource(fakeMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -571,31 +543,26 @@ public final class ExoPlayerTest { } @Test - public void testIllegalSeekPositionDoesThrow() throws Exception { - final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; + public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception { ActionSchedule actionSchedule = - new ActionSchedule.Builder("testIllegalSeekPositionDoesThrow") + new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition") .waitForPlaybackState(Player.STATE_BUFFERING) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - try { - player.seekTo(/* windowIndex= */ 100, /* positionMs= */ 0); - } catch (IllegalSeekPositionException e) { - exception[0] = e; - } - } - }) + // The illegal seek position will end playback. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) .waitForPlaybackState(Player.STATE_ENDED) .build(); - new Builder() - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - assertThat(exception[0]).isNotNull(); + final boolean[] onSeekProcessedCalled = new boolean[1]; + EventListener listener = + new EventListener() { + @Override + public void onSeekProcessed() { + onSeekProcessedCalled[0] = true; + } + }; + ExoPlayerTestRunner testRunner = + new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context); + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + assertThat(onSeekProcessedCalled[0]).isTrue(); } @Test @@ -639,7 +606,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -667,7 +634,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -693,7 +660,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -711,7 +678,7 @@ public final class ExoPlayerTest { FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -740,7 +707,7 @@ public final class ExoPlayerTest { FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -777,7 +744,7 @@ public final class ExoPlayerTest { .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -816,7 +783,7 @@ public final class ExoPlayerTest { .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -840,35 +807,31 @@ public final class ExoPlayerTest { @Test public void testDynamicTimelineChangeReason() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline1) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null)) - .waitForTimelineChanged( - timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline2) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline, timeline2); + testRunner.assertTimelinesEqual(timeline1, timeline2); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC); } @Test - public void testResetMediaItemsWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -891,19 +854,17 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_READY) .setShuffleModeEnabled(true) - // Set the second media source (with position reset). + // Reprepare with second media source (keeping state, but with position reset). // Plays period 1 and 0 because of the reversed fake shuffle order. - .setMediaItems(/* resetPosition= */ true, secondMediaSource) + .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) .play() - .waitForPositionDiscontinuity() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(firstMediaSource) + .setMediaSource(firstMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0); } @@ -948,7 +909,7 @@ public final class ExoPlayerTest { .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -981,10 +942,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @@ -1015,10 +974,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @@ -1049,11 +1006,9 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isEqualTo(0); } @@ -1099,29 +1054,15 @@ public final class ExoPlayerTest { } @Test - public void testSettingNewStartPositionPossibleAfterStopWithReset() throws Exception { + public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); - AtomicInteger windowIndexAfterStop = new AtomicInteger(); - AtomicLong positionAfterStop = new AtomicLong(); + MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSettingNewStartPositionPossibleAfterStopWithReset") + new ActionSchedule.Builder("testRepreparationAfterStop") .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ true) .waitForPlaybackState(Player.STATE_IDLE) - .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) - .setMediaItems(secondSource) - .prepare() - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - windowIndexAfterStop.set(player.getCurrentWindowIndex()); - positionAfterStop.set(player.getCurrentPosition()); - } - }) - .waitForPlaybackState(Player.STATE_READY) + .prepareSource(secondSource) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -1131,51 +1072,62 @@ public final class ExoPlayerTest { .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_IDLE, - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_ENDED); - testRunner.assertTimelinesSame( - dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, secondTimeline); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // stop(true) - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - assertThat(windowIndexAfterStop.get()).isEqualTo(1); - assertThat(positionAfterStop.get()).isAtLeast(1000L); + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + @Test + public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekAfterStopWithReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + // If we were still using the first timeline, this would throw. + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); testRunner.assertPlayedPeriodIndices(0, 1); } @Test - public void testResetPlaylistWithPreviousPosition() throws Exception { - Object firstWindowId = new Object(); + public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception { Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); - Timeline firstExpectedMaskingTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); - Object secondWindowId = new Object(); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); Timeline secondTimeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); - Timeline secondExpectedMaskingTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); MediaSource secondSource = new FakeMediaSource(secondTimeline); AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testResetPlaylistWithPreviousPosition") + new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .setMediaItems(/* windowIndex= */ 0, /* positionMs= */ 2000, secondSource) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .waitForTimelineChanged(secondTimeline) .executeRunnable( new PlayerRunnable() { @Override @@ -1194,68 +1146,10 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame( - firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); assertThat(positionAfterReprepare.get()).isAtLeast(2000L); } - @Test - public void testResetPlaylistStartsFromDefaultPosition() throws Exception { - Object firstWindowId = new Object(); - Timeline timeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); - Timeline firstExpectedDummyTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); - Object secondWindowId = new Object(); - Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); - Timeline secondExpectedDummyTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); - MediaSource secondSource = new FakeMediaSource(secondTimeline); - AtomicLong positionAfterReprepare = new AtomicLong(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testResetPlaylistStartsFromDefaultPosition") - .pause() - .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .setMediaItems(secondSource) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - positionAfterReprepare.set(player.getCurrentPosition()); - } - }) - .play() - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - testRunner.assertTimelinesSame( - firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - assertThat(positionAfterReprepare.get()).isEqualTo(0L); - } - @Test public void testStopDuringPreparationOverwritesPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); @@ -1274,10 +1168,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, Timeline.EMPTY); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + testRunner.assertTimelinesEqual(Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @@ -1302,10 +1194,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @@ -1317,7 +1207,8 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepare() + .prepareSource( + new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = @@ -1331,10 +1222,9 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); } @Test @@ -1356,7 +1246,8 @@ public final class ExoPlayerTest { positionHolder[0] = player.getCurrentPosition(); } }) - .prepare() + .prepareSource( + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @@ -1378,29 +1269,52 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); assertThat(positionHolder[0]).isEqualTo(50); assertThat(positionHolder[1]).isEqualTo(50); } + @Test + public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception { + final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Seeking to an invalid position will end playback. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(context); + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + } + @Test public void testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); AtomicInteger windowIndexAfterUpdate = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod") - .setShuffleOrder(new FakeShuffleOrder(/* length= */ 0)) .setShuffleModeEnabled(true) .waitForPlaybackState(Player.STATE_BUFFERING) // Seeking to an invalid position will end playback. - .seek( - /* windowIndex= */ 100, /* positionMs= */ 0, /* catchIllegalSeekException= */ true) + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) .waitForPlaybackState(Player.STATE_ENDED) .executeRunnable( new PlayerRunnable() { @@ -1410,13 +1324,12 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context); + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); assertThat(windowIndexAfterUpdate.get()).isEqualTo(1); } @@ -1450,7 +1363,7 @@ public final class ExoPlayerTest { }) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1464,7 +1377,7 @@ public final class ExoPlayerTest { final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); final long[] positionHolder = new long[3]; final int[] windowIndexHolder = new int[3]; - final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline); + final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1481,7 +1394,8 @@ public final class ExoPlayerTest { windowIndexHolder[0] = player.getCurrentWindowIndex(); } }) - .prepare() + .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_BUFFERING) .executeRunnable( new PlayerRunnable() { @Override @@ -1489,6 +1403,7 @@ public final class ExoPlayerTest { // Position while repreparing. positionHolder[1] = player.getCurrentPosition(); windowIndexHolder[1] = player.getCurrentWindowIndex(); + secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null); } }) .waitForPlaybackState(Player.STATE_READY) @@ -1505,7 +1420,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(firstMediaSource) + .setTimeline(timeline) .setActionSchedule(actionSchedule) .build(context); try { @@ -1532,8 +1447,7 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .seek(0, C.TIME_UNSET) - .prepare() + .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .play() .build(); @@ -1550,7 +1464,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .setAnalyticsListener(listener) .build(context); @@ -1566,15 +1480,14 @@ public final class ExoPlayerTest { @Test public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource2 = new FakeMediaSource(timeline); + final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .setMediaItems(/* resetPosition= */ false, mediaSource2) - .prepare() + .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_BUFFERING) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) @@ -1590,12 +1503,9 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesSame(dummyTimeline, timeline, dummyTimeline, timeline); + testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); } @Test @@ -1625,8 +1535,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* positionMs= */ 50) .play() .build(); @@ -1720,12 +1629,17 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .pause() .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) .play() + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ true) + .waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_ENDED) .build(); new Builder() @@ -1772,8 +1686,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .seek(/* positionMs= */ 50) .build(); new Builder() @@ -1814,8 +1727,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .pause() .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .seek(/* positionMs= */ 51) .play() .build(); @@ -1894,16 +1806,14 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* positionMs= */ 50) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(secondTimeline) .play() .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1919,7 +1829,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); @@ -1940,8 +1850,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); @@ -1970,17 +1879,15 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(secondTimeline) .seek(/* windowIndex= */ 0, /* positionMs= */ 0) .play() .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2014,7 +1921,7 @@ public final class ExoPlayerTest { .play() .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2152,21 +2059,16 @@ public final class ExoPlayerTest { /* windowIndex= */ 0, /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null)) - .waitForTimelineChanged( - timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline2) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPlayedPeriodIndices(0, 1); // Assert that the second period was re-created from the new timeline. assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3); @@ -2208,7 +2110,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2326,56 +2228,6 @@ public final class ExoPlayerTest { assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder(); } - @Test - public void testRecursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception { - Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 3); - final AtomicReference playerReference = new AtomicReference<>(); - FakeMediaSource secondMediaSource = new FakeMediaSource(secondTimeline); - final EventListener eventListener = - new EventListener() { - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - if (state == Player.STATE_IDLE) { - playerReference.get().setMediaItem(secondMediaSource); - } - } - }; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRecursiveTimelineChangeInStopAreReportedInCorrectOrder") - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - playerReference.set(player); - player.addListener(eventListener); - } - }) - .waitForTimelineChanged(firstTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - // Ensure there are no further pending callbacks. - .delay(1) - .stop(/* reset= */ true) - .prepare() - .waitForTimelineChanged(secondTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setActionSchedule(actionSchedule) - .setTimeline(firstTimeline) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, firstTimeline, Timeline.EMPTY, dummyTimeline, secondTimeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - } - @Test public void testClippedLoopedPeriodsArePlayedFully() throws Exception { long startPositionUs = 300_000; @@ -2428,7 +2280,7 @@ public final class ExoPlayerTest { .build(); new ExoPlayerTestRunner.Builder() .setClock(clock) - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2462,7 +2314,7 @@ public final class ExoPlayerTest { List trackGroupsList = new ArrayList<>(); List trackSelectionsList = new ArrayList<>(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .setActionSchedule(actionSchedule) .setEventListener( @@ -2510,7 +2362,7 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setRenderers(renderer) .build(context); try { @@ -2553,7 +2405,49 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .setRenderers(renderer) + .build(context); + try { + testRunner.start().blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertThat(renderer.sampleBufferReadCount).isAtLeast(1); + assertThat(renderer.hasReadStreamToEnd()).isTrue(); + } + + @Test + public void failingDynamicUpdateOnlyThrowsWhenAvailablePeriodHasBeenFullyRead() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ 10 * C.MICROS_PER_SECOND)); + AtomicReference wasReadyOnce = new AtomicReference<>(false); + MediaSource mediaSource = + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) { + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + if (wasReadyOnce.get()) { + throw new IOException(); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testFailingDynamicMediaSourceInTimelineOnlyThrowsLater") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(() -> wasReadyOnce.set(true)) + .play() + .build(); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = + new Builder() + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .setRenderers(renderer) .build(context); @@ -2587,7 +2481,7 @@ public final class ExoPlayerTest { .executeRunnable(concatenatingMediaSource::clear) .build(); new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2611,7 +2505,7 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .seek(/* positionMs= */ 10) - .waitForSeekProcessed() + .waitForTimelineChanged() .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) @@ -2625,7 +2519,7 @@ public final class ExoPlayerTest { .play() .build(); new Builder() - .setMediaSources(concatenatedMediaSource) + .setMediaSource(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2656,7 +2550,7 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_BUFFERING) // Seek 10ms into the second period. .seek(/* positionMs= */ periodDurationMs + 10) - .waitForSeekProcessed() + .waitForTimelineChanged() .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) @@ -2671,7 +2565,7 @@ public final class ExoPlayerTest { .play() .build(); new Builder() - .setMediaSources(concatenatedMediaSource) + .setMediaSource(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2772,10 +2666,10 @@ public final class ExoPlayerTest { player.addListener(eventListener); } }) - .seek(/* positionMs= */ 5_000) + .seek(5_000) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(fakeMediaSource) + .setMediaSource(fakeMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2873,506 +2767,6 @@ public final class ExoPlayerTest { .inOrder(); } - @Test - public void testMoveMediaItem() throws Exception { - TimelineWindowDefinition firstWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition secondWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - Timeline timeline1 = new FakeTimeline(firstWindowDefinition); - Timeline timeline2 = new FakeTimeline(secondWindowDefinition); - MediaSource mediaSource1 = new FakeMediaSource(timeline1); - MediaSource mediaSource2 = new FakeMediaSource(timeline2); - Timeline expectedDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testMoveMediaItem") - .waitForTimelineChanged( - /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource1, mediaSource2) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - Timeline expectedRealTimeline = new FakeTimeline(firstWindowDefinition, secondWindowDefinition); - Timeline expectedRealTimelineAfterMove = - new FakeTimeline(secondWindowDefinition, firstWindowDefinition); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - exoPlayerTestRunner.assertTimelinesSame( - expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterMove); - } - - @Test - public void testRemoveMediaItem() throws Exception { - TimelineWindowDefinition firstWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition secondWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition thirdWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - Timeline timeline1 = new FakeTimeline(firstWindowDefinition); - Timeline timeline2 = new FakeTimeline(secondWindowDefinition); - Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); - MediaSource mediaSource1 = new FakeMediaSource(timeline1); - MediaSource mediaSource2 = new FakeMediaSource(timeline2); - MediaSource mediaSource3 = new FakeMediaSource(timeline3); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRemoveMediaItems") - .waitForPlaybackState(Player.STATE_READY) - .removeMediaItem(/* index= */ 0) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource1, mediaSource2, mediaSource3) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - Timeline expectedDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - Timeline expectedRealTimeline = - new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); - Timeline expectedRealTimelineAfterRemove = - new FakeTimeline(secondWindowDefinition, thirdWindowDefinition); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - exoPlayerTestRunner.assertTimelinesSame( - expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); - } - - @Test - public void testRemoveMediaItems() throws Exception { - TimelineWindowDefinition firstWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition secondWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition thirdWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - Timeline timeline1 = new FakeTimeline(firstWindowDefinition); - Timeline timeline2 = new FakeTimeline(secondWindowDefinition); - Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); - MediaSource mediaSource1 = new FakeMediaSource(timeline1); - MediaSource mediaSource2 = new FakeMediaSource(timeline2); - MediaSource mediaSource3 = new FakeMediaSource(timeline3); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRemoveMediaItems") - .waitForPlaybackState(Player.STATE_READY) - .removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource1, mediaSource2, mediaSource3) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - Timeline expectedDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - Timeline expectedRealTimeline = - new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); - Timeline expectedRealTimelineAfterRemove = new FakeTimeline(firstWindowDefinition); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - exoPlayerTestRunner.assertTimelinesSame( - expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); - } - - @Test - public void testClearMediaItems() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testClearMediaItems") - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .waitForPlaybackState(Player.STATE_READY) - .clearMediaItems() - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */); - } - - @Test - public void testMultipleModificationWithRecursiveListenerInvocations() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource mediaSource = new FakeMediaSource(timeline); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testMultipleModificationWithRecursiveListenerInvocations") - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .clearMediaItems() - .setMediaItems(secondMediaSource) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, secondTimeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - } - - @Test - public void testModifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering() - throws Exception { - Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); - int[] playbackStates = new int[4]; - int[] timelineWindowCounts = new int[4]; - ActionSchedule actionSchedule = - new ActionSchedule.Builder( - "testModifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering") - .waitForTimelineChanged(dummyTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 0, playbackStates, timelineWindowCounts)) - .clearMediaItems() - .executeRunnable( - new PlaybackStateCollector(/* index= */ 1, playbackStates, timelineWindowCounts)) - .setMediaItems(/* windowIndex= */ 0, /* positionMs= */ 1000, firstMediaSource) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 2, playbackStates, timelineWindowCounts)) - .addMediaItems(secondMediaSource) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts)) - .seek(/* windowIndex= */ 1, /* positionMs= */ 2000) - .waitForSeekProcessed() - .prepare() - // The first expected buffering state arrives after prepare but not before. - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(firstMediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start(/* doPrepare= */ false) - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertArrayEquals( - new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, - playbackStates); - assertArrayEquals(new int[] {1, 0, 1, 2}, timelineWindowCounts); - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING /* first buffering state after prepare */, - Player.STATE_READY, - Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* initial setMediaItems */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* set media items */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* add media items */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source update after prepare */); - Timeline expectedSecondDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - Timeline expectedSecondRealTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ 10_000_000), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ 10_000_000)); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, - Timeline.EMPTY, - dummyTimeline, - expectedSecondDummyTimeline, - expectedSecondRealTimeline); - } - - @Test - public void testModifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); - ActionSchedule actionSchedule = - new ActionSchedule.Builder( - "testModifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering") - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .clearMediaItems() - .waitForPlaybackState(Player.STATE_ENDED) - .addMediaItems(secondMediaSource) // add must not transition to buffering - .waitForTimelineChanged() - .clearMediaItems() // clear must remain in ended - .addMediaItems(secondMediaSource) // add again to be able to test the seek - .waitForTimelineChanged() - .seek(/* positionMs= */ 2_000) // seek must transition to buffering - .waitForSeekProcessed() - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING, // first buffering - Player.STATE_READY, - Player.STATE_ENDED, // clear playlist - Player.STATE_BUFFERING, // second buffering after seek - Player.STATE_READY, - Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, - timeline, - Timeline.EMPTY, - dummyTimeline, - timeline, - Timeline.EMPTY, - dummyTimeline, - timeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); - } - - @Test - public void testStopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering() - throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); - int[] playbackStateHolder = new int[3]; - int[] windowCountHolder = new int[3]; - ActionSchedule actionSchedule = - new ActionSchedule.Builder( - "testStopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering") - .waitForPlaybackState(Player.STATE_READY) - .stop(/* reset= */ false) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 0, playbackStateHolder, windowCountHolder)) - .clearMediaItems() - .executeRunnable( - new PlaybackStateCollector(/* index= */ 1, playbackStateHolder, windowCountHolder)) - .addMediaItems(secondMediaSource) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 2, playbackStateHolder, windowCountHolder)) - .prepare() - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertArrayEquals( - new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, playbackStateHolder); - assertArrayEquals(new int[] {1, 0, 1}, windowCountHolder); - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING, // first buffering - Player.STATE_READY, - Player.STATE_IDLE, // stop - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, timeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, /* source prepared */ - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear media items */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item add (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); - } - - @Test - public void testPrepareWhenAlreadyPreparedIsANoop() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testPrepareWhenAlreadyPreparedIsANoop") - .waitForPlaybackState(Player.STATE_READY) - .prepare() - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); - } - // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index b137cd3cff..afcce904e9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -21,17 +21,15 @@ import static org.mockito.Mockito.mock; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; -import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; -import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,20 +49,19 @@ public final class MediaPeriodQueueTest { private MediaPeriodQueue mediaPeriodQueue; private AdPlaybackState adPlaybackState; + private Timeline timeline; private Object periodUid; private PlaybackInfo playbackInfo; private RendererCapabilities[] rendererCapabilities; private TrackSelector trackSelector; private Allocator allocator; - private Playlist playlist; - private FakeMediaSource fakeMediaSource; - private Playlist.MediaSourceHolder mediaSourceHolder; + private MediaSource mediaSource; @Before public void setUp() { mediaPeriodQueue = new MediaPeriodQueue(); - playlist = mock(Playlist.class); + mediaSource = mock(MediaSource.class); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); @@ -72,7 +69,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupTimeline(); + setupTimeline(/* initialPositionUs= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, @@ -83,7 +80,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* adGroupTimesUs= */ 0); + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); advance(); @@ -97,7 +94,10 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, @@ -132,7 +132,10 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, @@ -165,7 +168,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { - setupTimeline(/* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_END_OF_SOURCE, @@ -185,7 +188,10 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -195,8 +201,10 @@ public final class MediaPeriodQueueTest { enqueueNext(); // Second ad. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US + 1); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -210,7 +218,10 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -221,8 +232,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading first ad. // Change position of first ad (= change duration of content before first ad). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, + SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -237,6 +250,7 @@ public final class MediaPeriodQueueTest { public void updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { setupTimeline( + /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -250,8 +264,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; @@ -268,6 +284,7 @@ public final class MediaPeriodQueueTest { public void updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { setupTimeline( + /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -281,8 +298,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; @@ -299,6 +318,7 @@ public final class MediaPeriodQueueTest { public void updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { setupTimeline( + /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -312,8 +332,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -324,25 +346,16 @@ public final class MediaPeriodQueueTest { assertThat(getQueueLength()).isEqualTo(3); } - private void setupTimeline(long... adGroupTimesUs) { + private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); - - // Create a media source holder. - SinglePeriodAdTimeline adTimeline = - new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - fakeMediaSource = new FakeMediaSource(adTimeline); - mediaSourceHolder = new Playlist.MediaSourceHolder(fakeMediaSource, false); - mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null); - - Timeline timeline = createPlaylistTimeline(); + timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); mediaPeriodQueue.setTimeline(timeline); - playbackInfo = new PlaybackInfo( timeline, - mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0), + mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, Player.STATE_READY, @@ -356,25 +369,6 @@ public final class MediaPeriodQueueTest { /* positionUs= */ 0); } - private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) { - adPlaybackState = - new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); - updateTimeline(); - } - - private void updateTimeline() { - SinglePeriodAdTimeline adTimeline = - new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null); - mediaPeriodQueue.setTimeline(createPlaylistTimeline()); - } - - private Playlist.PlaylistTimeline createPlaylistTimeline() { - return new Playlist.PlaylistTimeline( - Collections.singleton(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - } - private void advance() { enqueueNext(); if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { @@ -395,7 +389,7 @@ public final class MediaPeriodQueueTest { rendererCapabilities, trackSelector, allocator, - playlist, + mediaSource, getNextMediaPeriodInfo(), new TrackSelectorResult( new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); @@ -427,6 +421,11 @@ public final class MediaPeriodQueueTest { updateTimeline(); } + private void updateTimeline() { + timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + mediaPeriodQueue.setTimeline(timeline); + } + private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( long startPositionUs, long endPositionUs, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java deleted file mode 100644 index cc551db8ac..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java +++ /dev/null @@ -1,510 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeMediaSource; -import com.google.android.exoplayer2.testutil.FakeShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeTimeline; -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; - -/** Unit test for {@link Playlist}. */ -@RunWith(AndroidJUnit4.class) -public class PlaylistTest { - - private static final int PLAYLIST_SIZE = 4; - - private Playlist playlist; - - @Before - public void setUp() { - playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class)); - } - - @Test - public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); - List fakeHolders = createFakeHolders(); - - Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); - assertNotSame(timeline, Timeline.EMPTY); - - // Remove all media sources. - timeline = - playlist.removeMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder); - assertSame(timeline, Timeline.EMPTY); - - timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); - assertNotSame(timeline, Timeline.EMPTY); - // Clear. - timeline = playlist.clear(shuffleOrder); - assertSame(timeline, Timeline.EMPTY); - } - - @Test - public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.setMediaSources( - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); - // Verify prepare is called once on prepare. - verify(mockMediaSource1, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - playlist.prepare(/* mediaTransferListener= */ null); - assertThat(playlist.isPrepared()).isTrue(); - // Verify prepare is called once on prepare. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - playlist.release(); - playlist.prepare(/* mediaTransferListener= */ null); - // Verify prepare is called a second time on re-prepare. - verify(mockMediaSource1, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder); - - assertThat(timeline.getWindowCount()).isEqualTo(2); - assertThat(playlist.getSize()).isEqualTo(2); - - // Assert holder offsets have been set properly - for (int i = 0; i < mediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); - } - - // Set media items again. The second holder is re-used. - List moreMediaSources = - createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); - moreMediaSources.add(mediaSources.get(1)); - timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - assertThat(timeline.getWindowCount()).isEqualTo(2); - for (int i = 0; i < moreMediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); - } - // Expect removed holders and sources to be removed without releasing. - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(0).isRemoved).isTrue(); - // Expect re-used holder and source not to be removed. - verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(1).isRemoved).isFalse(); - } - - @Test - public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources(mediaSources, shuffleOrder); - - // Verify sources are prepared. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - // Set media items again. The second holder is re-used. - List moreMediaSources = - createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); - moreMediaSources.add(mediaSources.get(1)); - playlist.setMediaSources(moreMediaSources, shuffleOrder); - - // Expect removed holders and sources to be removed and released. - verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(0).isRemoved).isTrue(); - // Expect re-used holder and source not to be removed but released. - verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(1).isRemoved).isFalse(); - verify(mockMediaSource2, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2)); - - assertThat(playlist.getSize()).isEqualTo(2); - // Verify lazy initialization does not call prepare on sources. - verify(mockMediaSource1, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - for (int i = 0; i < mediaSources.size(); i++) { - assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i); - assertThat(mediaSources.get(i).isRemoved).isFalse(); - } - - // Add for more sources in between. - List moreMediaSources = createFakeHolders(); - playlist.addMediaSources( - /* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3)); - - assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0); - assertThat(moreMediaSources.get(0).firstWindowIndexInChild).isEqualTo(1); - assertThat(moreMediaSources.get(3).firstWindowIndexInChild).isEqualTo(4); - assertThat(mediaSources.get(1).firstWindowIndexInChild).isEqualTo(5); - } - - @Test - public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.prepare(/* mediaTransferListener= */ null); - playlist.addMediaSources( - /* index= */ 0, - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); - - // Verify prepare is called on sources when added. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testMoveMediaSources() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - List holders = createFakeHolders(); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - - assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder); - assertFirstWindowInChildIndices(holders, 3, 0, 1, 2); - playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); - assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( - /* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); - assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( - /* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder); - assertFirstWindowInChildIndices(holders, 0, 3, 1, 2); - playlist.moveMediaSourceRange( - /* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - // No-ops. - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - } - - @Test - public void testRemoveMediaSources_whenUnprepared_expectNoRelease() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - MediaSource mockMediaSource3 = mock(MediaSource.class); - MediaSource mockMediaSource4 = mock(MediaSource.class); - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, - mockMediaSource1, - mockMediaSource2, - mockMediaSource3, - mockMediaSource4); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - Playlist.MediaSourceHolder removedHolder1 = holders.remove(1); - Playlist.MediaSourceHolder removedHolder2 = holders.remove(1); - - assertDefaultFirstWindowInChildIndexOrder(holders); - assertThat(removedHolder1.isRemoved).isTrue(); - assertThat(removedHolder2.isRemoved).isTrue(); - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource3, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - } - - @Test - public void testRemoveMediaSources_whenPrepared_expectRelease() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - MediaSource mockMediaSource3 = mock(MediaSource.class); - MediaSource mockMediaSource4 = mock(MediaSource.class); - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, - mockMediaSource1, - mockMediaSource2, - mockMediaSource3, - mockMediaSource4); - playlist.prepare(/* mediaTransferListener */ null); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - holders.remove(2); - holders.remove(1); - - assertDefaultFirstWindowInChildIndexOrder(holders); - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource3, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - } - - @Test - public void testRelease_playlistUnprepared_expectSourcesNotReleased() { - MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - - playlist.setMediaSources( - Collections.singletonList(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - verify(mockMediaSource, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - } - - @Test - public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() { - MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources( - Collections.singletonList(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - verify(mockMediaSource, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - } - - @Test - public void testClearPlaylist_expectSourcesReleasedAndRemoved() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.setMediaSources(holders, shuffleOrder); - playlist.prepare(/* mediaTransferListener= */ null); - - Timeline timeline = playlist.clear(shuffleOrder); - assertThat(timeline.isEmpty()).isTrue(); - assertThat(holders.get(0).isRemoved).isTrue(); - assertThat(holders.get(1).isRemoved).isTrue(); - verify(mockMediaSource1, times(1)).releaseSource(any()); - verify(mockMediaSource2, times(1)).releaseSource(any()); - } - - @Test - public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() { - Timeline timeline = - playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() { - Timeline timeline = - playlist.addMediaSources( - /* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.moveMediaSource( - /* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, - /* toIndex= */ 2, - /* newFromIndex= */ 2, - new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.removeMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() { - playlist.setMediaSources( - createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder( - playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE))); - } - - // Internal methods. - - private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) { - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(-1); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ timeline.getWindowCount() - 1, - Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(-1); - } - - private static void assertDefaultFirstWindowInChildIndexOrder( - List holders) { - int[] indices = new int[holders.size()]; - for (int i = 0; i < indices.length; i++) { - indices[i] = i; - } - assertFirstWindowInChildIndices(holders, indices); - } - - private static void assertFirstWindowInChildIndices( - List holders, int... firstWindowInChildIndices) { - assertThat(holders).hasSize(firstWindowInChildIndices.length); - for (int i = 0; i < holders.size(); i++) { - assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]); - } - } - - private static List createFakeHolders() { - MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1)); - List holders = new ArrayList<>(); - for (int i = 0; i < PLAYLIST_SIZE; i++) { - holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true)); - } - return holders; - } - - private static List createFakeHoldersWithSources( - boolean useLazyPreparation, MediaSource... sources) { - List holders = new ArrayList<>(); - for (MediaSource mediaSource : sources) { - holders.add( - new Playlist.MediaSourceHolder( - mediaSource, /* useLazyPreparation= */ useLazyPreparation)); - } - return holders; - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index ba05af385a..d6e65cb34d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2; -import static com.google.common.truth.Truth.assertThat; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; @@ -60,142 +58,4 @@ public class TimelineTest { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } - - @Test - public void testWindowEquals() { - Timeline.Window window = new Timeline.Window(); - assertThat(window).isEqualTo(new Timeline.Window()); - - Timeline.Window otherWindow = new Timeline.Window(); - otherWindow.tag = new Object(); - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.manifest = new Object(); - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.presentationStartTimeMs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.windowStartTimeMs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isSeekable = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isDynamic = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.defaultPositionUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.durationUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.firstPeriodIndex = 1; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.lastPeriodIndex = 1; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.positionInFirstPeriodUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - window.uid = new Object(); - window.tag = new Object(); - window.manifest = new Object(); - window.presentationStartTimeMs = C.TIME_UNSET; - window.windowStartTimeMs = C.TIME_UNSET; - window.isSeekable = true; - window.isDynamic = true; - window.defaultPositionUs = C.TIME_UNSET; - window.durationUs = C.TIME_UNSET; - window.firstPeriodIndex = 1; - window.lastPeriodIndex = 1; - window.positionInFirstPeriodUs = C.TIME_UNSET; - otherWindow = - otherWindow.set( - window.uid, - window.tag, - window.manifest, - window.presentationStartTimeMs, - window.windowStartTimeMs, - window.isSeekable, - window.isDynamic, - window.defaultPositionUs, - window.durationUs, - window.firstPeriodIndex, - window.lastPeriodIndex, - window.positionInFirstPeriodUs); - assertThat(window).isEqualTo(otherWindow); - } - - @Test - public void testWindowHashCode() { - Timeline.Window window = new Timeline.Window(); - Timeline.Window otherWindow = new Timeline.Window(); - assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - - window.tag = new Object(); - assertThat(window.hashCode()).isNotEqualTo(otherWindow.hashCode()); - otherWindow.tag = window.tag; - assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - } - - @Test - public void testPeriodEquals() { - Timeline.Period period = new Timeline.Period(); - assertThat(period).isEqualTo(new Timeline.Period()); - - Timeline.Period otherPeriod = new Timeline.Period(); - otherPeriod.id = new Object(); - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.uid = new Object(); - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.windowIndex = 12; - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.durationUs = 11L; - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - period.id = new Object(); - period.uid = new Object(); - period.windowIndex = 1; - period.durationUs = 123L; - otherPeriod = - otherPeriod.set( - period.id, - period.uid, - period.windowIndex, - period.durationUs, - /* positionInWindowUs= */ 0); - assertThat(period).isEqualTo(otherPeriod); - } - - @Test - public void testPeriodHashCode() { - Timeline.Period period = new Timeline.Period(); - Timeline.Period otherPeriod = new Timeline.Period(); - assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); - - period.windowIndex = 12; - assertThat(period.hashCode()).isNotEqualTo(otherPeriod.hashCode()); - otherPeriod.windowIndex = period.windowIndex; - assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 7117f426f3..fb3e0936ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -132,29 +133,24 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); listener.assertNoMoreEvents(); } @Test public void testSinglePeriod() throws Exception { FakeMediaSource mediaSource = - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); - populateEventIds(listener.lastReportedTimeline); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); @@ -183,14 +179,9 @@ public final class AnalyticsCollectorTest { public void testAutomaticPeriodTransition() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT), - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -200,8 +191,7 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* BUFFERING */, period0 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -243,8 +233,8 @@ public final class AnalyticsCollectorTest { public void testPeriodTransitionWithRendererChange() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -256,8 +246,7 @@ public final class AnalyticsCollectorTest { period1 /* BUFFERING */, period1 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -297,8 +286,8 @@ public final class AnalyticsCollectorTest { public void testSeekToOtherPeriod() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -319,8 +308,7 @@ public final class AnalyticsCollectorTest { period1 /* READY */, period1 /* setPlayWhenReady=true */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); @@ -362,11 +350,9 @@ public final class AnalyticsCollectorTest { public void testSeekBackAfterReadingAhead() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); long periodDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); ActionSchedule actionSchedule = @@ -394,8 +380,7 @@ public final class AnalyticsCollectorTest { period1Seq2 /* BUFFERING */, period1Seq2 /* READY */, period1Seq2 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); @@ -443,28 +428,18 @@ public final class AnalyticsCollectorTest { @Test public void testPrepareNewSource() throws Exception { - MediaSource mediaSource1 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); - MediaSource mediaSource2 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() .waitForPlaybackState(Player.STATE_READY) - .setMediaItems(/* resetPosition= */ false, mediaSource2) + .prepareSource(mediaSource2) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); - // Populate all event ids with last timeline (after second prepare). - populateEventIds(listener.lastReportedTimeline); - // Populate event id of period 0, sequence 0 with timeline of initial preparation. - period0Seq0 = - new EventWindowAndPeriodId( - /* windowIndex= */ 0, - new MediaPeriodId( - listener.reportedTimelines.get(1).getUidOfPeriod(/* periodIndex= */ 0), - /* windowSequenceNumber= */ 0)); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -476,16 +451,12 @@ public final class AnalyticsCollectorTest { period0Seq1 /* READY */, period0Seq1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly( - WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* DYNAMIC */, - WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) .containsExactly( - period0Seq0 /* prepared */, WINDOW_0 /* setMediaItems */, period0Seq1 /* prepared */); + period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, @@ -519,20 +490,19 @@ public final class AnalyticsCollectorTest { @Test public void testReprepareAfterError() throws Exception { - MediaSource mediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .seek(/* positionMs= */ 0) - .prepare() + .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_ENDED) .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(listener.lastReportedTimeline); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -586,7 +556,7 @@ public final class AnalyticsCollectorTest { @Test public void testDynamicTimelineChange() throws Exception { MediaSource childMediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); long periodDurationMs = @@ -618,11 +588,7 @@ public final class AnalyticsCollectorTest { period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* BUFFERING */, period1Seq0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly( - WINDOW_0 /* PLAYLIST_CHANGED */, - window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */, - period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly( window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); @@ -676,7 +642,7 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(listener.lastReportedTimeline); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); } @@ -743,7 +709,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = new TestAnalyticsListener(); try { new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderersFactory(renderersFactory) .setAnalyticsListener(listener) .setActionSchedule(actionSchedule) @@ -765,7 +731,7 @@ public final class AnalyticsCollectorTest { private boolean renderedFirstFrame; public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { - super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + super(Builder.VIDEO_FORMAT); eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); decoderCounters = new DecoderCounters(); } @@ -823,7 +789,7 @@ public final class AnalyticsCollectorTest { private boolean notifiedAudioSessionId; public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { - super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT); + super(Builder.AUDIO_FORMAT); eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); decoderCounters = new DecoderCounters(); } @@ -907,12 +873,10 @@ public final class AnalyticsCollectorTest { public Timeline lastReportedTimeline; - private final List reportedTimelines; private final ArrayList reportedEvents; public TestAnalyticsListener() { reportedEvents = new ArrayList<>(); - reportedTimelines = new ArrayList<>(); lastReportedTimeline = Timeline.EMPTY; } @@ -942,7 +906,6 @@ public final class AnalyticsCollectorTest { @Override public void onTimelineChanged(EventTime eventTime, int reason) { lastReportedTimeline = eventTime.timeline; - reportedTimelines.add(eventTime.timeline); reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 09539bcb76..af6b91fa23 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -21,7 +21,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.IllegalSeekPositionException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlayerMessage; @@ -29,7 +28,6 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; @@ -38,8 +36,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Log; -import java.util.Arrays; -import java.util.List; /** Base class for actions to perform during playback tests. */ public abstract class Action { @@ -116,7 +112,6 @@ public abstract class Action { private final Integer windowIndex; private final long positionMs; - private final boolean catchIllegalSeekException; /** * Action calls {@link Player#seekTo(long)}. @@ -128,7 +123,6 @@ public abstract class Action { super(tag, "Seek:" + positionMs); this.windowIndex = null; this.positionMs = positionMs; - catchIllegalSeekException = false; } /** @@ -137,191 +131,24 @@ public abstract class Action { * @param tag A tag to use for logging. * @param windowIndex The window to seek to. * @param positionMs The seek position. - * @param catchIllegalSeekException Whether {@link IllegalSeekPositionException} should be - * silently caught or not. */ - public Seek(String tag, int windowIndex, long positionMs, boolean catchIllegalSeekException) { + public Seek(String tag, int windowIndex, long positionMs) { super(tag, "Seek:" + positionMs); this.windowIndex = windowIndex; this.positionMs = positionMs; - this.catchIllegalSeekException = catchIllegalSeekException; } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - try { - if (windowIndex == null) { - player.seekTo(positionMs); - } else { - player.seekTo(windowIndex, positionMs); - } - } catch (IllegalSeekPositionException e) { - if (!catchIllegalSeekException) { - throw e; - } + if (windowIndex == null) { + player.seekTo(positionMs); + } else { + player.seekTo(windowIndex, positionMs); } } } - /** Calls {@link SimpleExoPlayer#setMediaItems(List, int, long)}. */ - public static final class SetMediaItems extends Action { - - private final int windowIndex; - private final long positionMs; - private final MediaSource[] mediaSources; - - /** - * @param tag A tag to use for logging. - * @param windowIndex The window index to start playback from. - * @param positionMs The position in milliseconds to start playback from. - * @param mediaSources The media sources to populate the playlist with. - */ - public SetMediaItems( - String tag, int windowIndex, long positionMs, MediaSource... mediaSources) { - super(tag, "SetMediaItems"); - this.windowIndex = windowIndex; - this.positionMs = positionMs; - this.mediaSources = mediaSources; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.setMediaItems(Arrays.asList(mediaSources), windowIndex, positionMs); - } - } - - /** Calls {@link SimpleExoPlayer#addMediaItems(List)}. */ - public static final class AddMediaItems extends Action { - - private final MediaSource[] mediaSources; - - /** - * @param tag A tag to use for logging. - * @param mediaSources The media sources to be added to the playlist. - */ - public AddMediaItems(String tag, MediaSource... mediaSources) { - super(tag, /* description= */ "AddMediaItems"); - this.mediaSources = mediaSources; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.addMediaItems(Arrays.asList(mediaSources)); - } - } - - /** Calls {@link SimpleExoPlayer#setMediaItems(List, boolean)}. */ - public static final class SetMediaItemsResetPosition extends Action { - - private final boolean resetPosition; - private final MediaSource[] mediaSources; - - /** - * @param tag A tag to use for logging. - * @param resetPosition Whether the position should be reset. - * @param mediaSources The media sources to populate the playlist with. - */ - public SetMediaItemsResetPosition( - String tag, boolean resetPosition, MediaSource... mediaSources) { - super(tag, "SetMediaItems"); - this.resetPosition = resetPosition; - this.mediaSources = mediaSources; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.setMediaItems(Arrays.asList(mediaSources), resetPosition); - } - } - - /** Calls {@link SimpleExoPlayer#moveMediaItem(int, int)}. */ - public static class MoveMediaItem extends Action { - - private final int currentIndex; - private final int newIndex; - - /** - * @param tag A tag to use for logging. - * @param currentIndex The current index of the media item. - * @param newIndex The new index of the media item. - */ - public MoveMediaItem(String tag, int currentIndex, int newIndex) { - super(tag, "MoveMediaItem"); - this.currentIndex = currentIndex; - this.newIndex = newIndex; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.moveMediaItem(currentIndex, newIndex); - } - } - - /** Calls {@link SimpleExoPlayer#removeMediaItem(int)}. */ - public static class RemoveMediaItem extends Action { - - private final int index; - - /** - * @param tag A tag to use for logging. - * @param index The index of the item to remove. - */ - public RemoveMediaItem(String tag, int index) { - super(tag, "RemoveMediaItem"); - this.index = index; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.removeMediaItem(index); - } - } - - /** Calls {@link SimpleExoPlayer#removeMediaItems(int, int)}. */ - public static class RemoveMediaItems extends Action { - - private final int fromIndex; - private final int toIndex; - - /** - * @param tag A tag to use for logging. - * @param fromIndex The start if the range of media items to remove. - * @param toIndex The end of the range of media items to remove (exclusive). - */ - public RemoveMediaItems(String tag, int fromIndex, int toIndex) { - super(tag, "RemoveMediaItem"); - this.fromIndex = fromIndex; - this.toIndex = toIndex; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.removeMediaItems(fromIndex, toIndex); - } - } - - /** Calls {@link SimpleExoPlayer#clearMediaItems()}}. */ - public static class ClearMediaItems extends Action { - - /** @param tag A tag to use for logging. */ - public ClearMediaItems(String tag) { - super(tag, "ClearMediaItems"); - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.clearMediaItems(); - } - } - /** Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { @@ -380,6 +207,7 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlayWhenReady(playWhenReady); } + } /** @@ -440,28 +268,42 @@ public abstract class Action { } } - /** Calls {@link ExoPlayer#prepare()}. */ - public static final class Prepare extends Action { + /** Calls {@link ExoPlayer#prepare(MediaSource)}. */ + public static final class PrepareSource extends Action { + + private final MediaSource mediaSource; + private final boolean resetPosition; + private final boolean resetState; + /** @param tag A tag to use for logging. */ - public Prepare(String tag) { - super(tag, "Prepare"); + public PrepareSource(String tag, MediaSource mediaSource) { + this(tag, mediaSource, true, true); + } + + /** @param tag A tag to use for logging. */ + public PrepareSource( + String tag, MediaSource mediaSource, boolean resetPosition, boolean resetState) { + super(tag, "PrepareSource"); + this.mediaSource = mediaSource; + this.resetPosition = resetPosition; + this.resetState = resetState; } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.prepare(); + player.prepare(mediaSource, resetPosition, resetState); } } /** Calls {@link Player#setRepeatMode(int)}. */ public static final class SetRepeatMode extends Action { - @Player.RepeatMode private final int repeatMode; + private final @Player.RepeatMode int repeatMode; /** @param tag A tag to use for logging. */ public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { - super(tag, "SetRepeatMode: " + repeatMode); + super(tag, "SetRepeatMode:" + repeatMode); this.repeatMode = repeatMode; } @@ -472,27 +314,6 @@ public abstract class Action { } } - /** Calls {@link ExoPlayer#setShuffleOrder(ShuffleOrder)} . */ - public static final class SetShuffleOrder extends Action { - - private final ShuffleOrder shuffleOrder; - - /** - * @param tag A tag to use for logging. - * @param shuffleOrder The shuffle order. - */ - public SetShuffleOrder(String tag, ShuffleOrder shuffleOrder) { - super(tag, "SetShufflerOrder"); - this.shuffleOrder = shuffleOrder; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.setShuffleOrder(shuffleOrder); - } - } - /** Calls {@link Player#setShuffleModeEnabled(boolean)}. */ public static final class SetShuffleModeEnabled extends Action { @@ -500,7 +321,7 @@ public abstract class Action { /** @param tag A tag to use for logging. */ public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { - super(tag, "SetShuffleModeEnabled: " + shuffleModeEnabled); + super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); this.shuffleModeEnabled = shuffleModeEnabled; } @@ -587,6 +408,7 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlaybackParameters(playbackParameters); } + } /** Throws a playback exception on the playback thread. */ @@ -683,35 +505,18 @@ public abstract class Action { /** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */ public static final class WaitForTimelineChanged extends Action { - private final Timeline expectedTimeline; - private final boolean ignoreExpectedReason; - @Player.TimelineChangeReason private final int expectedReason; + @Nullable private final Timeline expectedTimeline; /** - * Creates action waiting for a timeline change for a given reason. + * Creates action waiting for a timeline change. * * @param tag A tag to use for logging. - * @param expectedTimeline The expected timeline or null if any timeline change is relevant. - * @param expectedReason The expected timeline change reason. + * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline + * change. */ - public WaitForTimelineChanged( - String tag, Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { + public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) { super(tag, "WaitForTimelineChanged"); this.expectedTimeline = expectedTimeline; - this.ignoreExpectedReason = false; - this.expectedReason = expectedReason; - } - - /** - * Creates action waiting for any timeline change for any reason. - * - * @param tag A tag to use for logging. - */ - public WaitForTimelineChanged(String tag) { - super(tag, "WaitForTimelineChanged"); - this.expectedTimeline = null; - this.ignoreExpectedReason = true; - this.expectedReason = Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } @Override @@ -729,9 +534,7 @@ public abstract class Action { @Override public void onTimelineChanged( Timeline timeline, @Player.TimelineChangeReason int reason) { - if ((expectedTimeline == null - || TestUtil.areTimelinesSame(expectedTimeline, timeline)) - && (ignoreExpectedReason || expectedReason == reason)) { + if (expectedTimeline == null || timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); } @@ -919,7 +722,7 @@ public abstract class Action { } } - /** Calls {@code Runnable.run()}. */ + /** Calls {@link Runnable#run()}. */ public static final class ExecuteRunnable extends Action { private final Runnable runnable; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index b977956a92..c77e88c981 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -27,10 +27,10 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition; +import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; -import com.google.android.exoplayer2.testutil.Action.SetShuffleOrder; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; @@ -170,19 +169,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder seek(int windowIndex, long positionMs) { - return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false)); - } - - /** - * Schedules a seek action to be executed. - * - * @param windowIndex The window to seek to. - * @param positionMs The seek position. - * @param catchIllegalSeekException Whether an illegal seek position should be caught or not. - * @return The builder, for convenience. - */ - public Builder seek(int windowIndex, long positionMs, boolean catchIllegalSeekException) { - return apply(new Seek(tag, windowIndex, positionMs, catchIllegalSeekException)); + return apply(new Seek(tag, windowIndex, positionMs)); } /** @@ -314,100 +301,23 @@ public final class ActionSchedule { } /** - * Schedules a set media items action to be executed. + * Schedules a new source preparation action to be executed. * - * @param windowIndex The window index to start playback from or {@link C#INDEX_UNSET} if the - * playback position should not be reset. - * @param positionMs The position in milliseconds from where playback should start. If {@link - * C#TIME_UNSET} is passed the default position is used. In any case, if {@code windowIndex} - * is set to {@link C#INDEX_UNSET} the position is not reset at all and this parameter is - * ignored. * @return The builder, for convenience. */ - public Builder setMediaItems(int windowIndex, long positionMs, MediaSource... sources) { - return apply(new Action.SetMediaItems(tag, windowIndex, positionMs, sources)); + public Builder prepareSource(MediaSource mediaSource) { + return apply(new PrepareSource(tag, mediaSource)); } /** - * Schedules a set media items action to be executed. + * Schedules a new source preparation action to be executed. * - * @param resetPosition Whether the playback position should be reset. - * @return The builder, for convenience. - */ - public Builder setMediaItems(boolean resetPosition, MediaSource... sources) { - return apply(new Action.SetMediaItemsResetPosition(tag, resetPosition, sources)); - } - - /** - * Schedules a set media items action to be executed. - * - * @param mediaSources The media sources to add. - * @return The builder, for convenience. - */ - public Builder setMediaItems(MediaSource... mediaSources) { - return apply( - new Action.SetMediaItems( - tag, /* windowIndex= */ C.INDEX_UNSET, /* positionMs= */ C.TIME_UNSET, mediaSources)); - } - /** - * Schedules a add media items action to be executed. - * - * @param mediaSources The media sources to add. - * @return The builder, for convenience. - */ - public Builder addMediaItems(MediaSource... mediaSources) { - return apply(new Action.AddMediaItems(tag, mediaSources)); - } - - /** - * Schedules a move media item action to be executed. - * - * @param currentIndex The current index of the item to move. - * @param newIndex The index after the item has been moved. - * @return The builder, for convenience. - */ - public Builder moveMediaItem(int currentIndex, int newIndex) { - return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex)); - } - - /** - * Schedules a remove media item action to be executed. - * - * @param index The index of the media item to be removed. * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean) * @return The builder, for convenience. */ - public Builder removeMediaItem(int index) { - return apply(new Action.RemoveMediaItem(tag, index)); - } - - /** - * Schedules a remove media items action to be executed. - * - * @param fromIndex The start of the range of media items to be removed. - * @param toIndex The end of the range of media items to be removed (exclusive). - * @return The builder, for convenience. - */ - public Builder removeMediaItems(int fromIndex, int toIndex) { - return apply(new Action.RemoveMediaItems(tag, fromIndex, toIndex)); - } - - /** - * Schedules a prepare action to be executed. - * - * @return The builder, for convenience. - */ - public Builder prepare() { - return apply(new Action.Prepare(tag)); - } - - /** - * Schedules a clear media items action to be created. - * - * @return The builder. for convenience, - */ - public Builder clearMediaItems() { - return apply(new Action.ClearMediaItems(tag)); + public Builder prepareSource( + MediaSource mediaSource, boolean resetPosition, boolean resetState) { + return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); } /** @@ -419,16 +329,6 @@ public final class ActionSchedule { return apply(new SetRepeatMode(tag, repeatMode)); } - /** - * Schedules a set shuffle order action to be executed. - * - * @param shuffleOrder The shuffle order. - * @return The builder, for convenience. - */ - public Builder setShuffleOrder(ShuffleOrder shuffleOrder) { - return apply(new SetShuffleOrder(tag, shuffleOrder)); - } - /** * Schedules a shuffle setting action to be executed. * @@ -482,19 +382,18 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder waitForTimelineChanged() { - return apply(new WaitForTimelineChanged(tag)); + return apply(new WaitForTimelineChanged(tag, /* expectedTimeline= */ null)); } /** * Schedules a delay until the timeline changed to a specified expected timeline. * - * @param expectedTimeline The expected timeline. - * @param expectedReason The expected reason of the timeline change. + * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline + * change. * @return The builder, for convenience. */ - public Builder waitForTimelineChanged( - Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { - return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason)); + public Builder waitForTimelineChanged(Timeline expectedTimeline) { + return apply(new WaitForTimelineChanged(tag, expectedTimeline)); } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index b00ad287bb..5f01d7724b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -141,8 +141,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { pendingSchedule = null; } DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); - player.setMediaItem(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); - player.prepare(); + player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); } @Override 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 f5d322d0ca..59afaf7dca 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; -import static junit.framework.TestCase.assertTrue; import android.content.Context; import android.os.HandlerThread; @@ -45,7 +44,6 @@ import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -74,8 +72,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private Clock clock; private Timeline timeline; - private List mediaSources; private Object manifest; + private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private LoadControl loadControl; private BandwidthMeter bandwidthMeter; @@ -87,22 +85,18 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private AnalyticsListener analyticsListener; private Integer expectedPlayerEndedCount; - public Builder() { - mediaSources = new ArrayList<>(); - } - /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link * FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is - * not allowed after a call to {@link #setMediaSources(MediaSource...)}. + * not allowed after a call to {@link #setMediaSource(MediaSource)}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. * @return This builder. */ public Builder setTimeline(Timeline timeline) { - assertThat(mediaSources).isEmpty(); + assertThat(mediaSource).isNull(); this.timeline = timeline; return this; } @@ -110,30 +104,30 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value * is null. Setting the manifest is not allowed after a call to {@link - * #setMediaSources(MediaSource...)}. + * #setMediaSource(MediaSource)}. * * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. * @return This builder. */ public Builder setManifest(Object manifest) { - assertThat(mediaSources).isEmpty(); + assertThat(mediaSource).isNull(); this.manifest = manifest; return this; } /** - * Sets the {@link MediaSource}s to be used by the test runner. The default value is a {@link + * Sets a {@link MediaSource} to be used by the test runner. The default value is a {@link * FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)} - * and {@link #setManifest(Object)}. Setting media sources is not allowed after calls to {@link - * #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. + * and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to + * {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. * - * @param mediaSources The {@link MediaSource}s to be used by the test runner. + * @param mediaSource A {@link MediaSource} to be used by the test runner. * @return This builder. */ - public Builder setMediaSources(MediaSource... mediaSources) { + public Builder setMediaSource(MediaSource mediaSource) { assertThat(timeline).isNull(); assertThat(manifest).isNull(); - this.mediaSources = Arrays.asList(mediaSources); + this.mediaSource = mediaSource; return this; } @@ -177,7 +171,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media * periods and for setting up a {@link FakeRenderer}. The default value is a single {@link * #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source - * with {@link #setMediaSources(MediaSource...)} and renderers with {@link + * with {@link #setMediaSource(MediaSource)} and renderers with {@link * #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. * * @param supportedFormats A list of supported {@link Format}s. @@ -231,7 +225,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be - * executed immediately before {@link SimpleExoPlayer#prepare()}. + * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. * * @param actionSchedule An {@link ActionSchedule} to be used by the test runner. * @return This builder. @@ -312,11 +306,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (clock == null) { clock = new AutoAdvancingFakeClock(); } - if (mediaSources.isEmpty()) { + if (mediaSource == null) { if (timeline == null) { timeline = new FakeTimeline(/* windowCount= */ 1, manifest); } - mediaSources.add(new FakeMediaSource(timeline, supportedFormats)); + mediaSource = new FakeMediaSource(timeline, supportedFormats); } if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; @@ -324,7 +318,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc return new ExoPlayerTestRunner( context, clock, - mediaSources, + mediaSource, renderersFactory, trackSelector, loadControl, @@ -338,7 +332,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final Context context; private final Clock clock; - private final List mediaSources; + private final MediaSource mediaSource; private final RenderersFactory renderersFactory; private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; @@ -355,7 +349,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final ArrayList timelineChangeReasons; private final ArrayList periodIndices; private final ArrayList discontinuityReasons; - private final ArrayList playbackStates; private SimpleExoPlayer player; private Exception exception; @@ -365,7 +358,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private ExoPlayerTestRunner( Context context, Clock clock, - List mediaSources, + MediaSource mediaSource, RenderersFactory renderersFactory, DefaultTrackSelector trackSelector, LoadControl loadControl, @@ -376,7 +369,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc int expectedPlayerEndedCount) { this.context = context; this.clock = clock; - this.mediaSources = mediaSources; + this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; this.loadControl = loadControl; @@ -388,7 +381,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); - this.playbackStates = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); @@ -434,10 +426,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.setMediaItems(mediaSources, /* resetPosition= */ false); - if (doPrepare) { - player.prepare(); - } + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } @@ -489,16 +478,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, - * int)} are the same to the provided timelines. This assert differs from testing equality by not - * comparing period ids which may be different due to id mapping of child source period ids. + * int)} are equal to the provided timelines. * * @param timelines A list of expected {@link Timeline}s. */ - public void assertTimelinesSame(Timeline... timelines) { - assertThat(this.timelines).hasSize(timelines.length); - for (int i = 0; i < timelines.length; i++) { - assertTrue(TestUtil.areTimelinesSame(timelines[i], this.timelines.get(i))); - } + public void assertTimelinesEqual(Timeline... timelines) { + assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); } /** @@ -510,15 +495,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder(); } - /** - * Asserts that the playback states reported by {@link - * Player.EventListener#onPlayerStateChanged(boolean, int)} are equal to the provided playback - * states. - */ - public void assertPlaybackStatesEqual(Integer... states) { - assertThat(playbackStates).containsExactlyElementsIn(Arrays.asList(states)).inOrder(); - } - /** * Asserts that the last track group array reported by {@link * Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the @@ -594,12 +570,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Override public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - timelineChangeReasons.add(reason); timelines.add(timeline); - int currentIndex = player.getCurrentPeriodIndex(); - if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) { - // Ignore timeline changes that do not change the period index. - periodIndices.add(currentIndex); + timelineChangeReasons.add(reason); + if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { + periodIndices.add(player.getCurrentPeriodIndex()); } } @@ -610,7 +584,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - playbackStates.add(playbackState); playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { @@ -657,9 +630,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc renderersFactory, trackSelector, loadControl, + /* drmSessionManager= */ null, bandwidthMeter, new AnalyticsCollector(clock), - /* useLazyPreparation= */ false, clock, Looper.myLooper()); } 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 a826e73e16..18eaec2cd7 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 @@ -25,10 +25,8 @@ import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.util.List; /** * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} @@ -98,11 +96,6 @@ 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(); @@ -113,77 +106,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItems(List mediaItems) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItems( - List mediaItems, int startWindowIndex, long startPositionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItem(MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItem(int index, MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItems(List mediaSources) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItems(int index, List mediaSources) { - throw new UnsupportedOperationException(); - } - - @Override - public void moveMediaItem(int currentIndex, int newIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public MediaSource removeMediaItem(int index) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeMediaItems(int fromIndex, int toIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void clearMediaItems() { - throw new UnsupportedOperationException(); - } - @Override public void setPlayWhenReady(boolean playWhenReady) { throw new UnsupportedOperationException(); @@ -204,11 +126,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - throw new UnsupportedOperationException(); - } - @Override public void setShuffleModeEnabled(boolean shuffleModeEnabled) { throw new UnsupportedOperationException(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 52c12f78b2..facfa0d7e4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -26,7 +26,6 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.DefaultDatabaseProvider; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; @@ -399,61 +398,4 @@ public class TestUtil { } return new DefaultExtractorInput(dataSource, position, length); } - - /** - * Checks whether the timelines are the same (does not compare {@link Timeline.Window#uid} and - * {@link Timeline.Period#uid}). - * - * @param firstTimeline The first {@link Timeline}. - * @param secondTimeline The second {@link Timeline} to compare with. - * @return {@code true} if both timelines are the same. - */ - public static boolean areTimelinesSame(Timeline firstTimeline, Timeline secondTimeline) { - if (firstTimeline == secondTimeline) { - return true; - } - if (secondTimeline.getWindowCount() != firstTimeline.getWindowCount() - || secondTimeline.getPeriodCount() != firstTimeline.getPeriodCount()) { - return false; - } - Timeline.Window firstWindow = new Timeline.Window(); - Timeline.Period firstPeriod = new Timeline.Period(); - Timeline.Window secondWindow = new Timeline.Window(); - Timeline.Period secondPeriod = new Timeline.Period(); - for (int i = 0; i < firstTimeline.getWindowCount(); i++) { - if (!areWindowsSame( - firstTimeline.getWindow(i, firstWindow), secondTimeline.getWindow(i, secondWindow))) { - return false; - } - } - for (int i = 0; i < firstTimeline.getPeriodCount(); i++) { - if (!firstTimeline - .getPeriod(i, firstPeriod, /* setIds= */ false) - .equals(secondTimeline.getPeriod(i, secondPeriod, /* setIds= */ false))) { - return false; - } - } - return true; - } - - /** - * Checks whether the windows are the same. This comparison does not compare the uid. - * - * @param first The first {@link Timeline.Window}. - * @param second The second {@link Timeline.Window}. - * @return true if both windows are the same. - */ - private static boolean areWindowsSame(Timeline.Window first, Timeline.Window second) { - return Util.areEqual(first.tag, second.tag) - && Util.areEqual(first.manifest, second.manifest) - && first.presentationStartTimeMs == second.presentationStartTimeMs - && first.windowStartTimeMs == second.windowStartTimeMs - && first.isSeekable == second.isSeekable - && first.isDynamic == second.isDynamic - && first.defaultPositionUs == second.defaultPositionUs - && first.durationUs == second.durationUs - && first.firstPeriodIndex == second.firstPeriodIndex - && first.lastPeriodIndex == second.lastPeriodIndex - && first.positionInFirstPeriodUs == second.positionInFirstPeriodUs; - } }