Commit 4ad4e3e4 by bachinger Committed by Oliver Woodman

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
parent cc8b774b
Showing with 721 additions and 3821 deletions
...@@ -49,6 +49,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep ...@@ -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.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.source.BehindLiveWindowException; 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.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
...@@ -78,7 +79,6 @@ import java.net.CookieHandler; ...@@ -78,7 +79,6 @@ import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
/** An activity that plays media using {@link SimpleExoPlayer}. */ /** An activity that plays media using {@link SimpleExoPlayer}. */
...@@ -141,7 +141,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -141,7 +141,7 @@ public class PlayerActivity extends AppCompatActivity
private DataSource.Factory dataSourceFactory; private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private List<MediaSource> mediaSources; private MediaSource mediaSource;
private DefaultTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters; private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
...@@ -343,8 +343,8 @@ public class PlayerActivity extends AppCompatActivity ...@@ -343,8 +343,8 @@ public class PlayerActivity extends AppCompatActivity
Intent intent = getIntent(); Intent intent = getIntent();
releaseMediaDrms(); releaseMediaDrms();
mediaSources = createTopLevelMediaSources(intent); mediaSource = createTopLevelMediaSource(intent);
if (mediaSources.isEmpty()) { if (mediaSource == null) {
return; return;
} }
...@@ -388,12 +388,12 @@ public class PlayerActivity extends AppCompatActivity ...@@ -388,12 +388,12 @@ public class PlayerActivity extends AppCompatActivity
if (haveStartPosition) { if (haveStartPosition) {
player.seekTo(startWindow, startPosition); player.seekTo(startWindow, startPosition);
} }
player.setMediaItems(mediaSources, /* resetPosition= */ !haveStartPosition); player.prepare(mediaSource, !haveStartPosition, false);
player.prepare();
updateButtonVisibility(); updateButtonVisibility();
} }
private List<MediaSource> createTopLevelMediaSources(Intent intent) { @Nullable
private MediaSource createTopLevelMediaSource(Intent intent) {
String action = intent.getAction(); String action = intent.getAction();
boolean actionIsListView = ACTION_VIEW_LIST.equals(action); boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
if (!actionIsListView && !ACTION_VIEW.equals(action)) { if (!actionIsListView && !ACTION_VIEW.equals(action)) {
...@@ -421,30 +421,34 @@ public class PlayerActivity extends AppCompatActivity ...@@ -421,30 +421,34 @@ public class PlayerActivity extends AppCompatActivity
} }
} }
List<MediaSource> mediaSources = new ArrayList<>(); MediaSource[] mediaSources = new MediaSource[samples.length];
for (UriSample sample : samples) { for (int i = 0; i < samples.length; i++) {
mediaSources.add(createLeafMediaSource(sample)); 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; Uri adTagUri = samples[0].adTagUri;
if (actionIsListView) {
showToast(R.string.unsupported_ads_in_concatenation);
} else {
if (!adTagUri.equals(loadedAdTagUri)) { if (!adTagUri.equals(loadedAdTagUri)) {
releaseAdsLoader(); releaseAdsLoader();
loadedAdTagUri = adTagUri; loadedAdTagUri = adTagUri;
} }
MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri); MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
if (adsMediaSource != null) { if (adsMediaSource != null) {
mediaSources.set(0, adsMediaSource); mediaSource = adsMediaSource;
} else { } else {
showToast(R.string.ima_not_loaded); showToast(R.string.ima_not_loaded);
} }
} else if (seenAdsTagUri && mediaSources.size() > 1) { }
showToast(R.string.unsupported_ads_in_concatenation);
releaseAdsLoader();
} else { } else {
releaseAdsLoader(); releaseAdsLoader();
} }
return mediaSources; return mediaSource;
} }
private MediaSource createLeafMediaSource(UriSample parameters) { private MediaSource createLeafMediaSource(UriSample parameters) {
...@@ -544,7 +548,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -544,7 +548,7 @@ public class PlayerActivity extends AppCompatActivity
debugViewHelper = null; debugViewHelper = null;
player.release(); player.release();
player = null; player = null;
mediaSources = null; mediaSource = null;
trackSelector = null; trackSelector = null;
} }
if (adsLoader != null) { if (adsLoader != null) {
......
...@@ -106,6 +106,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -106,6 +106,7 @@ public final class CastPlayer extends BasePlayer {
private int pendingSeekCount; private int pendingSeekCount;
private int pendingSeekWindowIndex; private int pendingSeekWindowIndex;
private long pendingSeekPositionMs; private long pendingSeekPositionMs;
private boolean waitingForInitialTimeline;
/** /**
* @param castContext The context from which the cast session is obtained. * @param castContext The context from which the cast session is obtained.
...@@ -167,6 +168,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -167,6 +168,7 @@ public final class CastPlayer extends BasePlayer {
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
positionMs, null); positionMs, null);
} }
...@@ -592,13 +594,15 @@ public final class CastPlayer extends BasePlayer { ...@@ -592,13 +594,15 @@ public final class CastPlayer extends BasePlayer {
private void maybeUpdateTimelineAndNotify() { private void maybeUpdateTimelineAndNotify() {
if (updateTimeline()) { if (updateTimeline()) {
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and @Player.TimelineChangeReason
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. int reason =
waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
notificationsBatch.add( notificationsBatch.add(
new ListenerNotificationTask( new ListenerNotificationTask(
listener -> listener -> listener.onTimelineChanged(currentTimeline, reason)));
listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
} }
} }
......
...@@ -30,6 +30,7 @@ import java.util.ArrayList; ...@@ -30,6 +30,7 @@ import java.util.ArrayList;
private final Timeline.Period period; private final Timeline.Period period;
private final Timeline timeline; private final Timeline timeline;
private boolean prepared;
@Player.State private int state; @Player.State private int state;
private boolean playWhenReady; private boolean playWhenReady;
private long position; private long position;
...@@ -46,17 +47,13 @@ import java.util.ArrayList; ...@@ -46,17 +47,13 @@ import java.util.ArrayList;
timeline = Timeline.EMPTY; timeline = Timeline.EMPTY;
} }
/** /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
* Sets the timeline on this fake player, which notifies listeners with the changed timeline and public void updateTimeline(Timeline timeline) {
* 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) {
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(timeline, timelineChangeReason); listener.onTimelineChanged(
timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
} }
prepared = true;
} }
/** /**
......
...@@ -285,9 +285,7 @@ public class ImaAdsLoaderTest { ...@@ -285,9 +285,7 @@ public class ImaAdsLoaderTest {
public void onAdPlaybackState(AdPlaybackState adPlaybackState) { public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
this.adPlaybackState = adPlaybackState; this.adPlaybackState = adPlaybackState;
fakeExoPlayer.updateTimeline( fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
new SinglePeriodAdTimeline(contentTimeline, adPlaybackState),
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
} }
@Override @Override
......
...@@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.LoopingMediaSource; ...@@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; 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.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
...@@ -40,7 +39,6 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -40,7 +39,6 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; 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 * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link
...@@ -141,7 +139,7 @@ public interface ExoPlayer extends Player { ...@@ -141,7 +139,7 @@ public interface ExoPlayer extends Player {
private LoadControl loadControl; private LoadControl loadControl;
private BandwidthMeter bandwidthMeter; private BandwidthMeter bandwidthMeter;
private Looper looper; private Looper looper;
@Nullable private AnalyticsCollector analyticsCollector; private AnalyticsCollector analyticsCollector;
private boolean useLazyPreparation; private boolean useLazyPreparation;
private boolean buildCalled; private boolean buildCalled;
...@@ -172,7 +170,7 @@ public interface ExoPlayer extends Player { ...@@ -172,7 +170,7 @@ public interface ExoPlayer extends Player {
new DefaultLoadControl(), new DefaultLoadControl(),
DefaultBandwidthMeter.getSingletonInstance(context), DefaultBandwidthMeter.getSingletonInstance(context),
Util.getLooper(), Util.getLooper(),
/* analyticsCollector= */ null, new AnalyticsCollector(Clock.DEFAULT),
/* useLazyPreparation= */ true, /* useLazyPreparation= */ true,
Clock.DEFAULT); Clock.DEFAULT);
} }
...@@ -199,7 +197,7 @@ public interface ExoPlayer extends Player { ...@@ -199,7 +197,7 @@ public interface ExoPlayer extends Player {
LoadControl loadControl, LoadControl loadControl,
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
Looper looper, Looper looper,
@Nullable AnalyticsCollector analyticsCollector, AnalyticsCollector analyticsCollector,
boolean useLazyPreparation, boolean useLazyPreparation,
Clock clock) { Clock clock) {
Assertions.checkArgument(renderers.length > 0); Assertions.checkArgument(renderers.length > 0);
...@@ -320,156 +318,38 @@ public interface ExoPlayer extends Player { ...@@ -320,156 +318,38 @@ public interface ExoPlayer extends Player {
Assertions.checkState(!buildCalled); Assertions.checkState(!buildCalled);
buildCalled = true; buildCalled = true;
return new ExoPlayerImpl( return new ExoPlayerImpl(
renderers, renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
trackSelector,
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
} }
} }
/** Returns the {@link Looper} associated with the playback thread. */ /** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper(); 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(); void retry();
/** @deprecated Use {@link #setMediaItem(MediaSource)} and {@link #prepare()} instead. */
@Deprecated
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}. Equivalent to {@code
* * prepare(mediaSource, true, true)}.
* @param mediaItems The new {@link MediaSource MediaSources}.
*/ */
void setMediaItems(List<MediaSource> mediaItems); void prepare(MediaSource mediaSource);
/** /**
* 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}. * @param mediaSource The {@link MediaSource} to play.
* @param resetPosition Whether the playback position should be reset to the default position in * @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 * the first {@link Timeline.Window}. If false, playback will start from the position defined
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. * 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<MediaSource> mediaItems, boolean resetPosition); void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
/**
* 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<MediaSource> 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<MediaSource> 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<MediaSource> 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);
/** /**
* Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message
......
...@@ -296,7 +296,6 @@ public final class ExoPlayerFactory { ...@@ -296,7 +296,6 @@ public final class ExoPlayerFactory {
drmSessionManager, drmSessionManager,
bandwidthMeter, bandwidthMeter,
analyticsCollector, analyticsCollector,
/* useLazyPreparation= */ true,
Clock.DEFAULT, Clock.DEFAULT,
looper); looper);
} }
...@@ -345,13 +344,6 @@ public final class ExoPlayerFactory { ...@@ -345,13 +344,6 @@ public final class ExoPlayerFactory {
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
Looper looper) { Looper looper) {
return new ExoPlayerImpl( return new ExoPlayerImpl(
renderers, renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);
trackSelector,
loadControl,
bandwidthMeter,
/* analyticsCollector= */ null,
/* useLazyPreparation= */ true,
Clock.DEFAULT,
looper);
} }
} }
...@@ -22,10 +22,8 @@ import android.os.Message; ...@@ -22,10 +22,8 @@ import android.os.Message;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlayerMessage.Target; 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;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
...@@ -37,9 +35,6 @@ import com.google.android.exoplayer2.util.Clock; ...@@ -37,9 +35,6 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
/** /**
...@@ -66,20 +61,19 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -66,20 +61,19 @@ import java.util.concurrent.CopyOnWriteArrayList;
private final CopyOnWriteArrayList<ListenerHolder> listeners; private final CopyOnWriteArrayList<ListenerHolder> listeners;
private final Timeline.Period period; private final Timeline.Period period;
private final ArrayDeque<Runnable> pendingListenerNotifications; private final ArrayDeque<Runnable> pendingListenerNotifications;
private final List<Playlist.MediaSourceHolder> mediaSourceHolders;
private final boolean useLazyPreparation;
private MediaSource mediaSource;
private boolean playWhenReady; private boolean playWhenReady;
@PlaybackSuppressionReason private int playbackSuppressionReason; @PlaybackSuppressionReason private int playbackSuppressionReason;
@RepeatMode private int repeatMode; @RepeatMode private int repeatMode;
private boolean shuffleModeEnabled; private boolean shuffleModeEnabled;
private int pendingOperationAcks; private int pendingOperationAcks;
private boolean hasPendingPrepare;
private boolean hasPendingSeek; private boolean hasPendingSeek;
private boolean foregroundMode; private boolean foregroundMode;
private int pendingSetPlaybackParametersAcks; private int pendingSetPlaybackParametersAcks;
private PlaybackParameters playbackParameters; private PlaybackParameters playbackParameters;
private SeekParameters seekParameters; private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder;
// Playback information when there is no pending seek/set source operation. // Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo; private PlaybackInfo playbackInfo;
...@@ -96,10 +90,6 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -96,10 +90,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
* @param trackSelector The {@link TrackSelector} that will 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 loadControl The {@link LoadControl} that will be used by the instance.
* @param bandwidthMeter The {@link BandwidthMeter} 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 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 * @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on. * used to call listeners on.
...@@ -110,8 +100,6 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -110,8 +100,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
TrackSelector trackSelector, TrackSelector trackSelector,
LoadControl loadControl, LoadControl loadControl,
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
@Nullable AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock, Clock clock,
Looper looper) { Looper looper) {
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
...@@ -119,13 +107,10 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -119,13 +107,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
Assertions.checkState(renderers.length > 0); Assertions.checkState(renderers.length > 0);
this.renderers = Assertions.checkNotNull(renderers); this.renderers = Assertions.checkNotNull(renderers);
this.trackSelector = Assertions.checkNotNull(trackSelector); this.trackSelector = Assertions.checkNotNull(trackSelector);
this.useLazyPreparation = useLazyPreparation; this.playWhenReady = false;
playWhenReady = false; this.repeatMode = Player.REPEAT_MODE_OFF;
repeatMode = Player.REPEAT_MODE_OFF; this.shuffleModeEnabled = false;
shuffleModeEnabled = false; this.listeners = new CopyOnWriteArrayList<>();
listeners = new CopyOnWriteArrayList<>();
mediaSourceHolders = new ArrayList<>();
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
emptyTrackSelectorResult = emptyTrackSelectorResult =
new TrackSelectorResult( new TrackSelectorResult(
new RendererConfiguration[renderers.length], new RendererConfiguration[renderers.length],
...@@ -144,9 +129,6 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -144,9 +129,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
}; };
playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
pendingListenerNotifications = new ArrayDeque<>(); pendingListenerNotifications = new ArrayDeque<>();
if (analyticsCollector != null) {
analyticsCollector.setPlayer(this);
}
internalPlayer = internalPlayer =
new ExoPlayerImplInternal( new ExoPlayerImplInternal(
renderers, renderers,
...@@ -157,7 +139,6 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -157,7 +139,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
playWhenReady, playWhenReady,
repeatMode, repeatMode,
shuffleModeEnabled, shuffleModeEnabled,
analyticsCollector,
eventHandler, eventHandler,
clock); clock);
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
...@@ -231,168 +212,41 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -231,168 +212,41 @@ import java.util.concurrent.CopyOnWriteArrayList;
} }
@Override @Override
@Deprecated
public void retry() { public void retry() {
prepare(); if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) {
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
}
} }
@Override @Override
public void prepare() { public void prepare(MediaSource mediaSource) {
if (playbackInfo.playbackState != Player.STATE_IDLE) { prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
return;
} }
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
this.mediaSource = mediaSource;
PlaybackInfo playbackInfo = PlaybackInfo playbackInfo =
getResetPlaybackInfo( getResetPlaybackInfo(
/* clearPlaylist= */ false, resetPosition,
resetState,
/* resetError= */ true, /* resetError= */ true,
/* playbackState= */ Player.STATE_BUFFERING); /* playbackState= */ Player.STATE_BUFFERING);
// Trigger internal prepare first before updating the playback info and notifying external // 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 // 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 // player after this prepare. The internal player can't change the playback info immediately
// because it uses a callback. // because it uses a callback.
hasPendingPrepare = true;
pendingOperationAcks++; pendingOperationAcks++;
internalPlayer.prepare(); internalPlayer.prepare(mediaSource, resetPosition, resetState);
updatePlaybackInfo( updatePlaybackInfo(
playbackInfo, playbackInfo,
/* positionDiscontinuity= */ false, /* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL, /* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false); /* 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<MediaSource> mediaItems) {
setMediaItems(
mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs */ C.TIME_UNSET);
}
@Override
public void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition) {
setMediaItems(
mediaItems,
/* startWindowIndex= */ resetPosition ? C.INDEX_UNSET : getCurrentWindowIndex(),
/* startPositionMs= */ resetPosition ? C.TIME_UNSET : getCurrentPosition());
}
@Override
public void setMediaItems(
List<MediaSource> mediaItems, int startWindowIndex, long startPositionMs) {
pendingOperationAcks++;
if (!mediaSourceHolders.isEmpty()) {
removeMediaSourceHolders(
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
}
List<Playlist.MediaSourceHolder> 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<MediaSource> mediaSources) {
addMediaItems(/* index= */ mediaSourceHolders.size(), mediaSources);
}
@Override
public void addMediaItems(int index, List<MediaSource> mediaSources) {
Assertions.checkArgument(index >= 0);
pendingOperationAcks++;
List<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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 @Override
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
...@@ -550,9 +404,13 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -550,9 +404,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Override @Override
public void stop(boolean reset) { public void stop(boolean reset) {
if (reset) {
mediaSource = null;
}
PlaybackInfo playbackInfo = PlaybackInfo playbackInfo =
getResetPlaybackInfo( getResetPlaybackInfo(
/* clearPlaylist= */ reset, /* resetPosition= */ reset,
/* resetState= */ reset,
/* resetError= */ reset, /* resetError= */ reset,
/* playbackState= */ Player.STATE_IDLE); /* playbackState= */ Player.STATE_IDLE);
// Trigger internal stop first before updating the playback info and notifying external // Trigger internal stop first before updating the playback info and notifying external
...@@ -565,7 +423,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -565,7 +423,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
playbackInfo, playbackInfo,
/* positionDiscontinuity= */ false, /* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL, /* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false); /* seekProcessed= */ false);
} }
...@@ -574,11 +432,13 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -574,11 +432,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
+ ExoPlayerLibraryInfo.registeredModules() + "]"); + ExoPlayerLibraryInfo.registeredModules() + "]");
mediaSource = null;
internalPlayer.release(); internalPlayer.release();
eventHandler.removeCallbacksAndMessages(null); eventHandler.removeCallbacksAndMessages(null);
playbackInfo = playbackInfo =
getResetPlaybackInfo( getResetPlaybackInfo(
/* clearPlaylist= */ false, /* resetPosition= */ false,
/* resetState= */ false,
/* resetError= */ false, /* resetError= */ false,
/* playbackState= */ Player.STATE_IDLE); /* playbackState= */ Player.STATE_IDLE);
} }
...@@ -726,11 +586,10 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -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. // Not private so it can be called from an inner class without going through a thunk method.
/* package */ void handleEvent(Message msg) { /* package */ void handleEvent(Message msg) {
switch (msg.what) { switch (msg.what) {
case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED:
handlePlaybackInfo( handlePlaybackInfo(
/* playbackInfo= */ (PlaybackInfo) msg.obj, (PlaybackInfo) msg.obj,
/* operationAcks= */ msg.arg1, /* operationAcks= */ msg.arg1,
/* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
/* positionDiscontinuityReason= */ msg.arg2); /* positionDiscontinuityReason= */ msg.arg2);
...@@ -778,23 +637,29 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -778,23 +637,29 @@ import java.util.concurrent.CopyOnWriteArrayList;
maskingWindowIndex = 0; maskingWindowIndex = 0;
maskingWindowPositionMs = 0; maskingWindowPositionMs = 0;
} }
@Player.TimelineChangeReason
int timelineChangeReason =
hasPendingPrepare
? Player.TIMELINE_CHANGE_REASON_PREPARED
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
boolean seekProcessed = hasPendingSeek; boolean seekProcessed = hasPendingSeek;
hasPendingPrepare = false;
hasPendingSeek = false; hasPendingSeek = false;
updatePlaybackInfo( updatePlaybackInfo(
playbackInfo, playbackInfo,
positionDiscontinuity, positionDiscontinuity,
positionDiscontinuityReason, positionDiscontinuityReason,
TIMELINE_CHANGE_REASON_SOURCE_UPDATE, timelineChangeReason,
seekProcessed); seekProcessed);
} }
} }
private PlaybackInfo getResetPlaybackInfo( private PlaybackInfo getResetPlaybackInfo(
boolean clearPlaylist, boolean resetError, @Player.State int playbackState) { boolean resetPosition,
if (clearPlaylist) { boolean resetState,
// Reset list of media source holders which are used for creating the masking timeline. boolean resetError,
removeMediaSourceHolders( @Player.State int playbackState) {
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); if (resetPosition) {
maskingWindowIndex = 0; maskingWindowIndex = 0;
maskingPeriodIndex = 0; maskingPeriodIndex = 0;
maskingWindowPositionMs = 0; maskingWindowPositionMs = 0;
...@@ -803,22 +668,24 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -803,22 +668,24 @@ import java.util.concurrent.CopyOnWriteArrayList;
maskingPeriodIndex = getCurrentPeriodIndex(); maskingPeriodIndex = getCurrentPeriodIndex();
maskingWindowPositionMs = getCurrentPosition(); maskingWindowPositionMs = getCurrentPosition();
} }
// Also reset period-based PlaybackInfo positions if resetting the state.
resetPosition = resetPosition || resetState;
MediaPeriodId mediaPeriodId = MediaPeriodId mediaPeriodId =
clearPlaylist resetPosition
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
: playbackInfo.periodId; : playbackInfo.periodId;
long startPositionUs = clearPlaylist ? 0 : playbackInfo.positionUs; long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;
long contentPositionUs = clearPlaylist ? C.TIME_UNSET : playbackInfo.contentPositionUs; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
return new PlaybackInfo( return new PlaybackInfo(
clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline, resetState ? Timeline.EMPTY : playbackInfo.timeline,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
contentPositionUs, contentPositionUs,
playbackState, playbackState,
resetError ? null : playbackInfo.playbackError, resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false, /* isLoading= */ false,
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
...@@ -828,8 +695,8 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -828,8 +695,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
private void updatePlaybackInfo( private void updatePlaybackInfo(
PlaybackInfo playbackInfo, PlaybackInfo playbackInfo,
boolean positionDiscontinuity, boolean positionDiscontinuity,
@DiscontinuityReason int positionDiscontinuityReason, @Player.DiscontinuityReason int positionDiscontinuityReason,
@TimelineChangeReason int timelineChangeReason, @Player.TimelineChangeReason int timelineChangeReason,
boolean seekProcessed) { boolean seekProcessed) {
boolean previousIsPlaying = isPlaying(); boolean previousIsPlaying = isPlaying();
// Assign playback info immediately such that all getters return the right values. // Assign playback info immediately such that all getters return the right values.
...@@ -850,53 +717,6 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -850,53 +717,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
/* isPlayingChanged= */ previousIsPlaying != isPlaying)); /* isPlayingChanged= */ previousIsPlaying != isPlaying));
} }
private List<Playlist.MediaSourceHolder> addMediaSourceHolders(
int index, List<MediaSource> mediaSources) {
List<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> removeMediaItemsInternal(int fromIndex, int toIndex) {
Assertions.checkArgument(
fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size());
pendingOperationAcks++;
List<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> removeMediaSourceHolders(
int fromIndex, int toIndexExclusive) {
List<Playlist.MediaSourceHolder> 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) { private void notifyListeners(ListenerInvocation listenerInvocation) {
CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners); CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation)); notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
...@@ -932,7 +752,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -932,7 +752,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
private final TrackSelector trackSelector; private final TrackSelector trackSelector;
private final boolean positionDiscontinuity; private final boolean positionDiscontinuity;
private final @Player.DiscontinuityReason int positionDiscontinuityReason; private final @Player.DiscontinuityReason int positionDiscontinuityReason;
private final int timelineChangeReason; private final @Player.TimelineChangeReason int timelineChangeReason;
private final boolean seekProcessed; private final boolean seekProcessed;
private final boolean playbackStateChanged; private final boolean playbackStateChanged;
private final boolean playbackErrorChanged; private final boolean playbackErrorChanged;
...@@ -966,16 +786,15 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -966,16 +786,15 @@ import java.util.concurrent.CopyOnWriteArrayList;
playbackErrorChanged = playbackErrorChanged =
previousPlaybackInfo.playbackError != playbackInfo.playbackError previousPlaybackInfo.playbackError != playbackInfo.playbackError
&& playbackInfo.playbackError != null; && playbackInfo.playbackError != null;
timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
timelineChanged =
!Util.areTimelinesSame(previousPlaybackInfo.timeline, playbackInfo.timeline);
trackSelectorResultChanged = trackSelectorResultChanged =
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
} }
@Override @Override
public void run() { public void run() {
if (timelineChanged) { if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
invokeAll( invokeAll(
listenerSnapshot, listenerSnapshot,
listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
......
...@@ -26,11 +26,11 @@ import androidx.annotation.NonNull; ...@@ -26,11 +26,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason; 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.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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.SampleStream;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
...@@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util; ...@@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** Implements the internal behavior of {@link ExoPlayerImpl}. */ /** Implements the internal behavior of {@link ExoPlayerImpl}. */
...@@ -53,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -53,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
implements Handler.Callback, implements Handler.Callback,
MediaPeriod.Callback, MediaPeriod.Callback,
TrackSelector.InvalidationListener, TrackSelector.InvalidationListener,
Playlist.PlaylistInfoRefreshListener, MediaSourceCaller,
PlaybackParameterListener, PlaybackParameterListener,
PlayerMessage.Sender { PlayerMessage.Sender {
...@@ -72,21 +71,16 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -72,21 +71,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int MSG_SET_SEEK_PARAMETERS = 5; private static final int MSG_SET_SEEK_PARAMETERS = 5;
private static final int MSG_STOP = 6; private static final int MSG_STOP = 6;
private static final int MSG_RELEASE = 7; private static final int MSG_RELEASE = 7;
private static final int MSG_PERIOD_PREPARED = 8; private static final int MSG_REFRESH_SOURCE_INFO = 8;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; private static final int MSG_PERIOD_PREPARED = 9;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10;
private static final int MSG_SET_REPEAT_MODE = 11; private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
private static final int MSG_SET_SHUFFLE_ENABLED = 12; private static final int MSG_SET_REPEAT_MODE = 12;
private static final int MSG_SET_FOREGROUND_MODE = 13; private static final int MSG_SET_SHUFFLE_ENABLED = 13;
private static final int MSG_SEND_MESSAGE = 14; private static final int MSG_SET_FOREGROUND_MODE = 14;
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; private static final int MSG_SEND_MESSAGE = 15;
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16; private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
private static final int MSG_SET_MEDIA_ITEMS = 17; private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 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 ACTIVE_INTERVAL_MS = 10; private static final int ACTIVE_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000; private static final int IDLE_INTERVAL_MS = 1000;
...@@ -109,12 +103,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -109,12 +103,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final ArrayList<PendingMessageInfo> pendingMessages; private final ArrayList<PendingMessageInfo> pendingMessages;
private final Clock clock; private final Clock clock;
private final MediaPeriodQueue queue; private final MediaPeriodQueue queue;
private final Playlist playlist;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private SeekParameters seekParameters; private SeekParameters seekParameters;
private PlaybackInfo playbackInfo; private PlaybackInfo playbackInfo;
private MediaSource mediaSource;
private Renderer[] enabledRenderers; private Renderer[] enabledRenderers;
private boolean released; private boolean released;
private boolean playWhenReady; private boolean playWhenReady;
...@@ -123,7 +117,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -123,7 +117,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
private boolean shuffleModeEnabled; private boolean shuffleModeEnabled;
private boolean foregroundMode; private boolean foregroundMode;
@Nullable private SeekPosition pendingInitialSeekPosition; private int pendingPrepareCount;
private SeekPosition pendingInitialSeekPosition;
private long rendererPositionUs; private long rendererPositionUs;
private int nextPendingMessageIndex; private int nextPendingMessageIndex;
...@@ -136,7 +131,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -136,7 +131,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean playWhenReady, boolean playWhenReady,
@Player.RepeatMode int repeatMode, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled, boolean shuffleModeEnabled,
@Nullable AnalyticsCollector analyticsCollector,
Handler eventHandler, Handler eventHandler,
Clock clock) { Clock clock) {
this.renderers = renderers; this.renderers = renderers;
...@@ -176,14 +170,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -176,14 +170,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start(); internalPlaybackThread.start();
handler = clock.createHandler(internalPlaybackThread.getLooper(), this); handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
playlist = new Playlist(this);
if (analyticsCollector != null) {
playlist.setAnalyticsCollector(eventHandler, analyticsCollector);
}
} }
public void prepare() { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
handler.obtainMessage(MSG_PREPARE).sendToTarget(); handler
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
.sendToTarget();
} }
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
...@@ -216,62 +208,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -216,62 +208,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
} }
public void setMediaItems(
List<Playlist.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder) {
setMediaItems(
mediaSources,
/* windowIndex= */ C.INDEX_UNSET,
/* positionUs= */ C.TIME_UNSET,
shuffleOrder);
}
public void setMediaItems(
List<Playlist.MediaSourceHolder> mediaSources,
int windowIndex,
long positionUs,
ShuffleOrder shuffleOrder) {
handler
.obtainMessage(
MSG_SET_MEDIA_ITEMS,
new PlaylistUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs))
.sendToTarget();
}
public void addMediaItems(
List<Playlist.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder) {
addMediaItems(C.INDEX_UNSET, mediaSources, shuffleOrder);
}
public void addMediaItems(
int index, List<Playlist.MediaSourceHolder> 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 @Override
public synchronized void sendMessage(PlayerMessage message) { public synchronized void sendMessage(PlayerMessage message) {
if (released) { if (released) {
...@@ -328,11 +264,13 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -328,11 +264,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
return internalPlaybackThread.getLooper(); return internalPlaybackThread.getLooper();
} }
// Playlist.PlaylistInfoRefreshListener implementation. // MediaSource.MediaSourceCaller implementation.
@Override @Override
public void onPlaylistUpdateRequested() { public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED); handler
.obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
.sendToTarget();
} }
// MediaPeriod.Callback implementation. // MediaPeriod.Callback implementation.
...@@ -364,12 +302,14 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -364,12 +302,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Handler.Callback implementation. // Handler.Callback implementation.
@Override @Override
@SuppressWarnings("unchecked")
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
try { try {
switch (msg.what) { switch (msg.what) {
case MSG_PREPARE: case MSG_PREPARE:
prepareInternal(); prepareInternal(
(MediaSource) msg.obj,
/* resetPosition= */ msg.arg1 != 0,
/* resetState= */ msg.arg2 != 0);
break; break;
case MSG_SET_PLAY_WHEN_READY: case MSG_SET_PLAY_WHEN_READY:
setPlayWhenReadyInternal(msg.arg1 != 0); setPlayWhenReadyInternal(msg.arg1 != 0);
...@@ -405,6 +345,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -405,6 +345,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
case MSG_PERIOD_PREPARED: case MSG_PERIOD_PREPARED:
handlePeriodPrepared((MediaPeriod) msg.obj); handlePeriodPrepared((MediaPeriod) msg.obj);
break; break;
case MSG_REFRESH_SOURCE_INFO:
handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);
break;
case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
handleContinueLoadingRequested((MediaPeriod) msg.obj); handleContinueLoadingRequested((MediaPeriod) msg.obj);
break; break;
...@@ -421,24 +364,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -421,24 +364,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
case MSG_SEND_MESSAGE_TO_TARGET_THREAD: case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
sendMessageToTargetThread((PlayerMessage) msg.obj); sendMessageToTargetThread((PlayerMessage) msg.obj);
break; 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: case MSG_RELEASE:
releaseInternal(); releaseInternal();
// Return immediately to not send playback info updates after release. // Return immediately to not send playback info updates after release.
...@@ -508,77 +433,21 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -508,77 +433,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private void prepareInternal() { private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); pendingPrepareCount++;
resetInternal( resetInternal(
/* resetRenderers= */ false, /* resetRenderers= */ false,
/* resetPosition= */ false, /* releaseMediaSource= */ true,
/* releasePlaylist= */ false, resetPosition,
/* clearPlaylist= */ false, resetState,
/* resetError= */ true); /* resetError= */ true);
loadControl.onPrepared(); loadControl.onPrepared();
setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING); this.mediaSource = mediaSource;
playlist.prepare(bandwidthMeter.getTransferListener()); setState(Player.STATE_BUFFERING);
mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK); 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 { private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
rebuffering = false; rebuffering = false;
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
...@@ -796,7 +665,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -796,7 +665,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
long periodPositionUs; long periodPositionUs;
long contentPositionUs; long contentPositionUs;
boolean seekPositionAdjusted; boolean seekPositionAdjusted;
@Nullable
Pair<Object, Long> resolvedSeekPosition = Pair<Object, Long> resolvedSeekPosition =
resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
if (resolvedSeekPosition == null) { if (resolvedSeekPosition == null) {
...@@ -821,7 +689,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -821,7 +689,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
try { 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. // Save seek position for later, as we are still waiting for a prepared source.
pendingInitialSeekPosition = seekPosition; pendingInitialSeekPosition = seekPosition;
} else if (periodPositionUs == C.TIME_UNSET) { } else if (periodPositionUs == C.TIME_UNSET) {
...@@ -829,9 +697,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -829,9 +697,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
setState(Player.STATE_ENDED); setState(Player.STATE_ENDED);
resetInternal( resetInternal(
/* resetRenderers= */ false, /* resetRenderers= */ false,
/* releaseMediaSource= */ false,
/* resetPosition= */ true, /* resetPosition= */ true,
/* releasePlaylist= */ false, /* resetState= */ false,
/* clearPlaylist= */ false,
/* resetError= */ true); /* resetError= */ true);
} else { } else {
// Execute the seek in the current media periods. // Execute the seek in the current media periods.
...@@ -978,11 +846,13 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -978,11 +846,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
resetInternal( resetInternal(
/* resetRenderers= */ forceResetRenderers || !foregroundMode, /* resetRenderers= */ forceResetRenderers || !foregroundMode,
/* releaseMediaSource= */ true,
/* resetPosition= */ resetPositionAndState, /* resetPosition= */ resetPositionAndState,
/* releasePlaylist= */ true, /* resetState= */ resetPositionAndState,
/* clearPlaylist= */ resetPositionAndState,
/* resetError= */ resetPositionAndState); /* resetError= */ resetPositionAndState);
playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0); playbackInfoUpdate.incrementPendingOperationAcks(
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
pendingPrepareCount = 0;
loadControl.onStopped(); loadControl.onStopped();
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
} }
...@@ -990,9 +860,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -990,9 +860,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void releaseInternal() { private void releaseInternal() {
resetInternal( resetInternal(
/* resetRenderers= */ true, /* resetRenderers= */ true,
/* releaseMediaSource= */ true,
/* resetPosition= */ true, /* resetPosition= */ true,
/* releasePlaylist= */ true, /* resetState= */ true,
/* clearPlaylist= */ true,
/* resetError= */ false); /* resetError= */ false);
loadControl.onReleased(); loadControl.onReleased();
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
...@@ -1005,9 +875,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1005,9 +875,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void resetInternal( private void resetInternal(
boolean resetRenderers, boolean resetRenderers,
boolean releaseMediaSource,
boolean resetPosition, boolean resetPosition,
boolean releasePlaylist, boolean resetState,
boolean clearPlaylist,
boolean resetError) { boolean resetError) {
handler.removeMessages(MSG_DO_SOME_WORK); handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false; rebuffering = false;
...@@ -1035,8 +905,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1035,8 +905,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (resetPosition) { if (resetPosition) {
pendingInitialSeekPosition = null; pendingInitialSeekPosition = null;
} else if (clearPlaylist) { } else if (resetState) {
// When clearing the playlist, also reset the period-based PlaybackInfo position and convert // When resetting the state, also reset the period-based PlaybackInfo position and convert
// existing position to initial seek instead. // existing position to initial seek instead.
resetPosition = true; resetPosition = true;
if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) { if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
...@@ -1047,10 +917,10 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1047,10 +917,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist); queue.clear(/* keepFrontPeriodUid= */ !resetState);
setIsLoading(false); setIsLoading(false);
if (clearPlaylist) { if (resetState) {
queue.setTimeline(playlist.clear(/* shuffleOrder= */ null)); queue.setTimeline(Timeline.EMPTY);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) { for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
} }
...@@ -1066,21 +936,24 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1066,21 +936,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
playbackInfo = playbackInfo =
new PlaybackInfo( new PlaybackInfo(
clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline, resetState ? Timeline.EMPTY : playbackInfo.timeline,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
contentPositionUs, contentPositionUs,
playbackInfo.playbackState, playbackInfo.playbackState,
resetError ? null : playbackInfo.playbackError, resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false, /* isLoading= */ false,
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
startPositionUs); startPositionUs);
if (releasePlaylist) { if (releaseMediaSource) {
playlist.release(); if (mediaSource != null) {
mediaSource.releaseSource(/* caller= */ this);
mediaSource = null;
}
} }
} }
...@@ -1088,7 +961,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1088,7 +961,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (message.getPositionMs() == C.TIME_UNSET) { if (message.getPositionMs() == C.TIME_UNSET) {
// If no delivery time is specified, trigger immediate message delivery. // If no delivery time is specified, trigger immediate message delivery.
sendMessageToTarget(message); sendMessageToTarget(message);
} else if (playbackInfo.timeline.isEmpty()) { } else if (mediaSource == null || pendingPrepareCount > 0) {
// Still waiting for initial timeline to resolve position. // Still waiting for initial timeline to resolve position.
pendingMessages.add(new PendingMessageInfo(message)); pendingMessages.add(new PendingMessageInfo(message));
} else { } else {
...@@ -1403,11 +1276,20 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1403,11 +1276,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
} }
playlist.maybeThrowSourceInfoRefreshError(); mediaSource.maybeThrowSourceInfoRefreshError();
}
private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
throws ExoPlaybackException {
if (sourceRefreshInfo.source != mediaSource) {
// Stale event.
return;
} }
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
pendingPrepareCount = 0;
private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException {
Timeline oldTimeline = playbackInfo.timeline; Timeline oldTimeline = playbackInfo.timeline;
Timeline timeline = sourceRefreshInfo.timeline;
queue.setTimeline(timeline); queue.setTimeline(timeline);
playbackInfo = playbackInfo.copyWithTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline);
resolvePendingMessagePositions(); resolvePendingMessagePositions();
...@@ -1418,7 +1300,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1418,7 +1300,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
long newContentPositionUs = oldContentPositionUs; long newContentPositionUs = oldContentPositionUs;
if (pendingInitialSeekPosition != null) { if (pendingInitialSeekPosition != null) {
// Resolve initial seek position. // Resolve initial seek position.
@Nullable
Pair<Object, Long> periodPosition = Pair<Object, Long> periodPosition =
resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
pendingInitialSeekPosition = null; pendingInitialSeekPosition = null;
...@@ -1523,12 +1404,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1523,12 +1404,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (playbackInfo.playbackState != Player.STATE_IDLE) { if (playbackInfo.playbackState != Player.STATE_IDLE) {
setState(Player.STATE_ENDED); 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( resetInternal(
/* resetRenderers= */ false, /* resetRenderers= */ false,
/* releaseMediaSource= */ false,
/* resetPosition= */ true, /* resetPosition= */ true,
/* releasePlaylist= */ false, /* resetState= */ false,
/* clearPlaylist= */ false,
/* resetError= */ true); /* resetError= */ true);
} }
...@@ -1571,7 +1452,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1571,7 +1452,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @throws IllegalSeekPositionException If the window index of the seek position is outside the * @throws IllegalSeekPositionException If the window index of the seek position is outside the
* bounds of the timeline. * bounds of the timeline.
*/ */
@Nullable
private Pair<Object, Long> resolveSeekPosition( private Pair<Object, Long> resolveSeekPosition(
SeekPosition seekPosition, boolean trySubsequentPeriods) { SeekPosition seekPosition, boolean trySubsequentPeriods) {
Timeline timeline = playbackInfo.timeline; Timeline timeline = playbackInfo.timeline;
...@@ -1628,9 +1508,13 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1628,9 +1508,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void updatePeriods() throws ExoPlaybackException, IOException { 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. // We're waiting to get information about periods.
playlist.maybeThrowSourceInfoRefreshError(); mediaSource.maybeThrowSourceInfoRefreshError();
return; return;
} }
maybeUpdateLoadingPeriod(); maybeUpdateLoadingPeriod();
...@@ -1650,7 +1534,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1650,7 +1534,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
rendererCapabilities, rendererCapabilities,
trackSelector, trackSelector,
loadControl.getAllocator(), loadControl.getAllocator(),
playlist, mediaSource,
info, info,
emptyTrackSelectorResult); emptyTrackSelectorResult);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
...@@ -1661,7 +1545,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1661,7 +1545,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
} }
} }
@Nullable MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false); setIsLoading(false);
} else if (!playbackInfo.isLoading) { } else if (!playbackInfo.isLoading) {
...@@ -1670,7 +1554,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1670,7 +1554,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void maybeUpdateReadingPeriod() throws ExoPlaybackException { private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
@Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (readingPeriodHolder == null) { if (readingPeriodHolder == null) {
return; return;
} }
...@@ -2079,38 +1963,14 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2079,38 +1963,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private static final class PlaylistUpdateMessage { private static final class MediaSourceRefreshInfo {
private final List<Playlist.MediaSourceHolder> mediaSourceHolders; public final MediaSource source;
private final ShuffleOrder shuffleOrder; public final Timeline timeline;
private final int windowIndex;
private final long positionUs;
private PlaylistUpdateMessage(
List<Playlist.MediaSourceHolder> 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( public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) {
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { this.source = source;
this.fromIndex = fromIndex; this.timeline = timeline;
this.toIndex = toIndex;
this.newFromIndex = newFromIndex;
this.shuffleOrder = shuffleOrder;
} }
} }
...@@ -2119,7 +1979,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2119,7 +1979,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private PlaybackInfo lastPlaybackInfo; private PlaybackInfo lastPlaybackInfo;
private int operationAcks; private int operationAcks;
private boolean positionDiscontinuity; private boolean positionDiscontinuity;
@DiscontinuityReason private int discontinuityReason; private @DiscontinuityReason int discontinuityReason;
public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
...@@ -2147,4 +2007,5 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2147,4 +2007,5 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.discontinuityReason = discontinuityReason; this.discontinuityReason = discontinuityReason;
} }
} }
} }
...@@ -19,6 +19,7 @@ import androidx.annotation.Nullable; ...@@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.ClippingMediaPeriod;
import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.EmptySampleStream;
import com.google.android.exoplayer2.source.MediaPeriod; 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.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
...@@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final boolean[] mayRetainStreamFlags; private final boolean[] mayRetainStreamFlags;
private final RendererCapabilities[] rendererCapabilities; private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector; private final TrackSelector trackSelector;
private final Playlist playlist; private final MediaSource mediaSource;
@Nullable private MediaPeriodHolder next; @Nullable private MediaPeriodHolder next;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
...@@ -69,7 +70,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -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 rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
* @param trackSelector The track selector. * @param trackSelector The track selector.
* @param allocator The allocator. * @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 info Information used to identify this media period in its timeline period.
* @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
* renderer. * renderer.
...@@ -79,13 +80,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -79,13 +80,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
long rendererPositionOffsetUs, long rendererPositionOffsetUs,
TrackSelector trackSelector, TrackSelector trackSelector,
Allocator allocator, Allocator allocator,
Playlist playlist, MediaSource mediaSource,
MediaPeriodInfo info, MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) { TrackSelectorResult emptyTrackSelectorResult) {
this.rendererCapabilities = rendererCapabilities; this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.rendererPositionOffsetUs = rendererPositionOffsetUs;
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
this.playlist = playlist; this.mediaSource = mediaSource;
this.uid = info.id.periodUid; this.uid = info.id.periodUid;
this.info = info; this.info = info;
this.trackGroups = TrackGroupArray.EMPTY; this.trackGroups = TrackGroupArray.EMPTY;
...@@ -93,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -93,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
sampleStreams = new SampleStream[rendererCapabilities.length]; sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length];
mediaPeriod = 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; ...@@ -303,7 +305,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Releases the media period. No other method should be called after the release. */ /** Releases the media period. No other method should be called after the release. */
public void release() { public void release() {
disableTrackSelectionsInResult(); disableTrackSelectionsInResult();
releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod); releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);
} }
/** /**
...@@ -400,11 +402,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -400,11 +402,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Returns a media period corresponding to the given {@code id}. */ /** Returns a media period corresponding to the given {@code id}. */
private static MediaPeriod createMediaPeriod( private static MediaPeriod createMediaPeriod(
MediaPeriodId id, MediaPeriodId id,
Playlist playlist, MediaSource mediaSource,
Allocator allocator, Allocator allocator,
long startPositionUs, long startPositionUs,
long endPositionUs) { 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) { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
mediaPeriod = mediaPeriod =
new ClippingMediaPeriod( new ClippingMediaPeriod(
...@@ -415,12 +417,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -415,12 +417,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
private static void releaseMediaPeriod( private static void releaseMediaPeriod(
long endPositionUs, Playlist playlist, MediaPeriod mediaPeriod) { long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
try { try {
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} else { } else {
playlist.releasePeriod(mediaPeriod); mediaSource.releasePeriod(mediaPeriod);
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
// There's nothing we can do. // There's nothing we can do.
......
...@@ -19,6 +19,7 @@ import android.util.Pair; ...@@ -19,6 +19,7 @@ import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.source.MediaPeriod; 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.MediaPeriodId;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
...@@ -133,7 +134,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -133,7 +134,7 @@ import com.google.android.exoplayer2.util.Assertions;
* @param rendererCapabilities The renderer capabilities. * @param rendererCapabilities The renderer capabilities.
* @param trackSelector The track selector. * @param trackSelector The track selector.
* @param allocator The allocator. * @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 info Information used to identify this media period in its timeline period.
* @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
* renderer. * renderer.
...@@ -142,7 +143,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -142,7 +143,7 @@ import com.google.android.exoplayer2.util.Assertions;
RendererCapabilities[] rendererCapabilities, RendererCapabilities[] rendererCapabilities,
TrackSelector trackSelector, TrackSelector trackSelector,
Allocator allocator, Allocator allocator,
Playlist playlist, MediaSource mediaSource,
MediaPeriodInfo info, MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) { TrackSelectorResult emptyTrackSelectorResult) {
long rendererPositionOffsetUs = long rendererPositionOffsetUs =
...@@ -157,7 +158,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -157,7 +158,7 @@ import com.google.android.exoplayer2.util.Assertions;
rendererPositionOffsetUs, rendererPositionOffsetUs,
trackSelector, trackSelector,
allocator, allocator,
playlist, mediaSource,
info, info,
emptyTrackSelectorResult); emptyTrackSelectorResult);
if (loading != null) { if (loading != null) {
......
...@@ -356,8 +356,7 @@ public interface Player { ...@@ -356,8 +356,7 @@ public interface Player {
* {@link #onPositionDiscontinuity(int)}. * {@link #onPositionDiscontinuity(int)}.
* *
* @param timeline The latest timeline. Never null, but may be empty. * @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 * @param manifest The latest manifest. May be null.
* null if the timeline has more than a single window.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
...@@ -585,17 +584,25 @@ public interface Player { ...@@ -585,17 +584,25 @@ public interface Player {
int DISCONTINUITY_REASON_INTERNAL = 4; int DISCONTINUITY_REASON_INTERNAL = 4;
/** /**
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link
* #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}. * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @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 {} @interface TimelineChangeReason {}
/** Timeline changed as a result of a change of the playlist items or the order of the items. */ /** Timeline and manifest changed as a result of a player initialization with new media. */
int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0; int TIMELINE_CHANGE_REASON_PREPARED = 0;
/** Timeline changed as a result of a dynamic update introduced by the played media. */ /** Timeline and manifest changed as a result of a player reset. */
int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; 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. */ /** Returns the component of this player for audio output, or null if audio is not supported. */
@Nullable @Nullable
......
/*
* 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.
*
* <p>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()}.
*
* <p>Called on the playback thread.
*/
void onPlaylistUpdateRequested();
}
private final List<MediaSourceHolder> mediaSourceHolders;
private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
private final PlaylistInfoRefreshListener playlistInfoListener;
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
private final HashMap<Playlist.MediaSourceHolder, MediaSourceAndListener> childSources;
private final Set<MediaSourceHolder> 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<MediaSourceHolder> 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 &lt;= index &lt;= {@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<MediaSourceHolder> 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).
*
* <p>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 &lt;= index &lt;= {@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 &lt;= index &lt;= {@link #getSize()}.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
* {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@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 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} &lt; 0,
* {@code currentIndex} &gt;= {@link #getSize()}, {@code newIndex} &lt; 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.
*
* <p>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 &lt;= index &lt;= {@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} &lt; 0,
* {@code toIndex} &lt; {@code fromIndex}, {@code fromIndex} &gt; {@code toIndex}, {@code
* newFromIndex} &lt; 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<MediaSourceHolder> 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<MediaSourceHolder> 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<MediaSource.MediaPeriodId> 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<Object, Integer> childIndexByUid;
public PlaylistTimeline(
Collection<MediaSourceHolder> 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;
}
}
}
...@@ -43,7 +43,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; ...@@ -43,7 +43,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource; 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.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextOutput;
...@@ -164,9 +163,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -164,9 +163,7 @@ public class SimpleExoPlayer extends BasePlayer
* @param bandwidthMeter A {@link BandwidthMeter}. * @param bandwidthMeter A {@link BandwidthMeter}.
* @param looper A {@link Looper} that must be used for all calls to the player. * @param looper A {@link Looper} that must be used for all calls to the player.
* @param analyticsCollector An {@link AnalyticsCollector}. * @param analyticsCollector An {@link AnalyticsCollector}.
* @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all * @param useLazyPreparation Whether media sources should be initialized lazily.
* 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 clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}.
*/ */
public Builder( public Builder(
...@@ -303,7 +300,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -303,7 +300,6 @@ public class SimpleExoPlayer extends BasePlayer
loadControl, loadControl,
bandwidthMeter, bandwidthMeter,
analyticsCollector, analyticsCollector,
useLazyPreparation,
clock, clock,
looper); looper);
} }
...@@ -343,6 +339,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -343,6 +339,7 @@ public class SimpleExoPlayer extends BasePlayer
private int audioSessionId; private int audioSessionId;
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
private float audioVolume; private float audioVolume;
@Nullable private MediaSource mediaSource;
private List<Cue> currentCues; private List<Cue> currentCues;
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
@Nullable private CameraMotionListener cameraMotionListener; @Nullable private CameraMotionListener cameraMotionListener;
...@@ -358,9 +355,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -358,9 +355,6 @@ public class SimpleExoPlayer extends BasePlayer
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will
* collect and forward all player events. * 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 * @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. * 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 * @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 ...@@ -374,7 +368,6 @@ public class SimpleExoPlayer extends BasePlayer
LoadControl loadControl, LoadControl loadControl,
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
AnalyticsCollector analyticsCollector, AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock, Clock clock,
Looper looper) { Looper looper) {
this( this(
...@@ -385,14 +378,26 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -385,14 +378,26 @@ public class SimpleExoPlayer extends BasePlayer
DrmSessionManager.getDummyDrmSessionManager(), DrmSessionManager.getDummyDrmSessionManager(),
bandwidthMeter, bandwidthMeter,
analyticsCollector, analyticsCollector,
useLazyPreparation,
clock, clock,
looper); 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, * @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. * DrmSessionManager} to the {@link MediaSource} factories.
*/ */
@Deprecated @Deprecated
...@@ -404,7 +409,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -404,7 +409,6 @@ public class SimpleExoPlayer extends BasePlayer
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
AnalyticsCollector analyticsCollector, AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock, Clock clock,
Looper looper) { Looper looper) {
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
...@@ -435,15 +439,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -435,15 +439,7 @@ public class SimpleExoPlayer extends BasePlayer
// Build the player and associated objects. // Build the player and associated objects.
player = player =
new ExoPlayerImpl( new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
renderers,
trackSelector,
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
analyticsCollector.setPlayer(player); analyticsCollector.setPlayer(player);
addListener(analyticsCollector); addListener(analyticsCollector);
addListener(componentListener); addListener(componentListener);
...@@ -1103,133 +1099,32 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1103,133 +1099,32 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
@Deprecated
public void retry() { public void retry() {
verifyApplicationThread(); 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 @Override
@Deprecated
@SuppressWarnings("deprecation")
public void prepare(MediaSource mediaSource) { public void prepare(MediaSource mediaSource) {
prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
} }
@Override @Override
@Deprecated
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
verifyApplicationThread(); verifyApplicationThread();
setMediaItems( if (this.mediaSource != null) {
Collections.singletonList(mediaSource), this.mediaSource.removeEventListener(analyticsCollector);
/* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET, analyticsCollector.resetForNewMediaSource();
/* startPositionMs= */ C.TIME_UNSET);
prepare();
}
@Override
public void setMediaItems(List<MediaSource> mediaItems) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItems(mediaItems);
}
@Override
public void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItems(mediaItems, resetPosition);
}
@Override
public void setMediaItems(
List<MediaSource> 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);
} }
this.mediaSource = mediaSource;
@Override mediaSource.addEventListener(eventHandler, analyticsCollector);
public void addMediaItem(MediaSource mediaSource) { @AudioFocusManager.PlayerCommand
verifyApplicationThread(); int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
player.addMediaItem(mediaSource); updatePlayWhenReady(getPlayWhenReady(), playerCommand);
} player.prepare(mediaSource, resetPosition, resetState);
@Override
public void addMediaItem(int index, MediaSource mediaSource) {
verifyApplicationThread();
player.addMediaItem(index, mediaSource);
}
@Override
public void addMediaItems(List<MediaSource> mediaSources) {
verifyApplicationThread();
player.addMediaItems(mediaSources);
}
@Override
public void addMediaItems(int index, List<MediaSource> 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);
} }
@Override @Override
...@@ -1309,7 +1204,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1309,7 +1204,6 @@ public class SimpleExoPlayer extends BasePlayer
@Override @Override
public void setForegroundMode(boolean foregroundMode) { public void setForegroundMode(boolean foregroundMode) {
verifyApplicationThread();
player.setForegroundMode(foregroundMode); player.setForegroundMode(foregroundMode);
} }
...@@ -1317,6 +1211,13 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1317,6 +1211,13 @@ public class SimpleExoPlayer extends BasePlayer
public void stop(boolean reset) { public void stop(boolean reset) {
verifyApplicationThread(); verifyApplicationThread();
player.stop(reset); player.stop(reset);
if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector);
analyticsCollector.resetForNewMediaSource();
if (reset) {
mediaSource = null;
}
}
audioFocusManager.handleStop(); audioFocusManager.handleStop();
currentCues = Collections.emptyList(); currentCues = Collections.emptyList();
} }
...@@ -1333,6 +1234,10 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1333,6 +1234,10 @@ public class SimpleExoPlayer extends BasePlayer
} }
surface = null; surface = null;
} }
if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector);
mediaSource = null;
}
if (isPriorityTaskManagerRegistered) { if (isPriorityTaskManagerRegistered) {
Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);
isPriorityTaskManagerRegistered = false; isPriorityTaskManagerRegistered = false;
......
...@@ -19,7 +19,6 @@ import android.util.Pair; ...@@ -19,7 +19,6 @@ import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions; 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 * A flexible representation of the structure of media. A timeline is able to represent the
...@@ -271,46 +270,6 @@ public abstract class Timeline { ...@@ -271,46 +270,6 @@ public abstract class Timeline {
return positionInFirstPeriodUs; 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 { ...@@ -567,34 +526,6 @@ public abstract class Timeline {
return adPlaybackState.adResumePositionUs; 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. */ /** An empty timeline. */
......
...@@ -132,8 +132,11 @@ public class AnalyticsCollector ...@@ -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. // Copying the list is needed because onMediaPeriodReleased will modify the list.
List<MediaPeriodInfo> mediaPeriodInfos = List<MediaPeriodInfo> mediaPeriodInfos =
new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
...@@ -783,13 +786,9 @@ public class AnalyticsCollector ...@@ -783,13 +786,9 @@ public class AnalyticsCollector
/** Updates the queue with a newly created media period. */ /** Updates the queue with a newly created media period. */
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET;
boolean isInTimeline = periodIndex != C.INDEX_UNSET;
MediaPeriodInfo mediaPeriodInfo = MediaPeriodInfo mediaPeriodInfo =
new MediaPeriodInfo( new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex);
mediaPeriodId,
isInTimeline ? timeline : Timeline.EMPTY,
isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodInfoQueue.add(mediaPeriodInfo);
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
...@@ -805,7 +804,7 @@ public class AnalyticsCollector ...@@ -805,7 +804,7 @@ public class AnalyticsCollector
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
if (mediaPeriodInfo == null) { 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; return false;
} }
mediaPeriodInfoQueue.remove(mediaPeriodInfo); mediaPeriodInfoQueue.remove(mediaPeriodInfo);
......
...@@ -13,14 +13,16 @@ ...@@ -13,14 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2.source;
import android.util.Pair; 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; import com.google.android.exoplayer2.util.Assertions;
/** Abstract base class for the concatenation of one or more {@link Timeline}s. */ /** 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 int childCount;
private final ShuffleOrder shuffleOrder; private final ShuffleOrder shuffleOrder;
...@@ -74,8 +76,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -74,8 +76,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
} }
@Override @Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, public int getNextWindowIndex(
boolean shuffleModeEnabled) { int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
if (isAtomic) { if (isAtomic) {
// Adapt repeat and shuffle mode to atomic concatenation. // Adapt repeat and shuffle mode to atomic concatenation.
repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
...@@ -84,7 +86,9 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -84,7 +86,9 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
// Find next window within current child. // Find next window within current child.
int childIndex = getChildIndexByWindowIndex(windowIndex); int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( int nextWindowIndexInChild =
getTimelineByChildIndex(childIndex)
.getNextWindowIndex(
windowIndex - firstWindowIndexInChild, windowIndex - firstWindowIndexInChild,
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
shuffleModeEnabled); shuffleModeEnabled);
...@@ -108,8 +112,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -108,8 +112,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
} }
@Override @Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, public int getPreviousWindowIndex(
boolean shuffleModeEnabled) { int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
if (isAtomic) { if (isAtomic) {
// Adapt repeat and shuffle mode to atomic concatenation. // Adapt repeat and shuffle mode to atomic concatenation.
repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
...@@ -118,7 +122,9 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -118,7 +122,9 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
// Find previous window within current child. // Find previous window within current child.
int childIndex = getChildIndexByWindowIndex(windowIndex); int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( int previousWindowIndexInChild =
getTimelineByChildIndex(childIndex)
.getPreviousWindowIndex(
windowIndex - firstWindowIndexInChild, windowIndex - firstWindowIndexInChild,
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
shuffleModeEnabled); shuffleModeEnabled);
...@@ -219,8 +225,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -219,8 +225,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
int childIndex = getChildIndexByPeriodIndex(periodIndex); int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, getTimelineByChildIndex(childIndex)
setIds); .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);
period.windowIndex += firstWindowIndexInChild; period.windowIndex += firstWindowIndexInChild;
if (setIds) { if (setIds) {
period.uid = period.uid =
...@@ -242,7 +248,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -242,7 +248,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
return C.INDEX_UNSET; return C.INDEX_UNSET;
} }
int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); 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; : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild;
} }
...@@ -307,13 +314,14 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { ...@@ -307,13 +314,14 @@ public abstract class AbstractConcatenatedTimeline extends Timeline {
protected abstract Object getChildUidByChildIndex(int childIndex); protected abstract Object getChildUidByChildIndex(int childIndex);
private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { 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; : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET;
} }
private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) {
return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) return shuffleModeEnabled
? shuffleOrder.getPreviousIndex(childIndex)
: childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET;
} }
} }
...@@ -19,7 +19,6 @@ import android.os.Handler; ...@@ -19,7 +19,6 @@ import android.os.Handler;
import android.os.Message; import android.os.Message;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.AbstractConcatenatedTimeline;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.AbstractConcatenatedTimeline;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
......
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
...@@ -62,7 +61,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -62,7 +61,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
} }
/** Returns the {@link Timeline}. */ /** Returns the {@link Timeline}. */
public synchronized Timeline getTimeline() { public Timeline getTimeline() {
return timeline; return timeline;
} }
...@@ -130,7 +129,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -130,7 +129,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
protected synchronized void onChildSourceInfoRefreshed( protected void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline newTimeline) { Void id, MediaSource mediaSource, Timeline newTimeline) {
if (isPrepared) { if (isPrepared) {
timeline = timeline.cloneWithUpdatedTimeline(newTimeline); timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
...@@ -294,8 +293,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -294,8 +293,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
} }
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
@VisibleForTesting private static final class DummyTimeline extends Timeline {
public static final class DummyTimeline extends Timeline {
@Nullable private final Object tag; @Nullable private final Object tag;
...@@ -334,8 +332,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -334,8 +332,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Override @Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
return period.set( return period.set(
/* id= */ setIds ? 0 : null, /* id= */ 0,
/* uid= */ setIds ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : null, /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID,
/* windowIndex= */ 0, /* windowIndex= */ 0,
/* durationUs = */ C.TIME_UNSET, /* durationUs = */ C.TIME_UNSET,
/* positionInWindowUs= */ 0); /* positionInWindowUs= */ 0);
......
...@@ -594,10 +594,12 @@ public class EventLogger implements AnalyticsListener { ...@@ -594,10 +594,12 @@ public class EventLogger implements AnalyticsListener {
private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) {
switch (reason) { switch (reason) {
case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE: case Player.TIMELINE_CHANGE_REASON_PREPARED:
return "SOURCE_UPDATE"; return "PREPARED";
case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED: case Player.TIMELINE_CHANGE_REASON_RESET:
return "PLAYLIST_CHANGED"; return "RESET";
case Player.TIMELINE_CHANGE_REASON_DYNAMIC:
return "DYNAMIC";
default: default:
return "?"; return "?";
} }
......
...@@ -53,7 +53,6 @@ import com.google.android.exoplayer2.Renderer; ...@@ -53,7 +53,6 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
...@@ -2025,42 +2024,6 @@ public final class Util { ...@@ -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<String, String> createIso3ToIso2Map() { private static HashMap<String, String> createIso3ToIso2Map() {
String[] iso2Languages = Locale.getISOLanguages(); String[] iso2Languages = Locale.getISOLanguages();
HashMap<String, String> iso3ToIso2 = HashMap<String, String> iso3ToIso2 =
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.content.Context; import android.content.Context;
...@@ -32,7 +31,6 @@ import com.google.android.exoplayer2.Timeline.Window; ...@@ -32,7 +31,6 @@ import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; 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;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
...@@ -87,12 +85,10 @@ public final class ExoPlayerTest { ...@@ -87,12 +85,10 @@ public final class ExoPlayerTest {
private static final int TIMEOUT_MS = 10000; private static final int TIMEOUT_MS = 10000;
private Context context; private Context context;
private Timeline dummyTimeline;
@Before @Before
public void setUp() { public void setUp() {
context = ApplicationProvider.getApplicationContext(); context = ApplicationProvider.getApplicationContext();
dummyTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ 0);
} }
/** /**
...@@ -102,7 +98,6 @@ public final class ExoPlayerTest { ...@@ -102,7 +98,6 @@ public final class ExoPlayerTest {
@Test @Test
public void testPlayEmptyTimeline() throws Exception { public void testPlayEmptyTimeline() throws Exception {
Timeline timeline = Timeline.EMPTY; Timeline timeline = Timeline.EMPTY;
Timeline expectedMaskingTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null);
FakeRenderer renderer = new FakeRenderer(); FakeRenderer renderer = new FakeRenderer();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new Builder() new Builder()
...@@ -112,10 +107,7 @@ public final class ExoPlayerTest { ...@@ -112,10 +107,7 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertNoPositionDiscontinuities(); testRunner.assertNoPositionDiscontinuities();
testRunner.assertTimelinesSame(expectedMaskingTimeline, Timeline.EMPTY); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(renderer.formatReadCount).isEqualTo(0); assertThat(renderer.formatReadCount).isEqualTo(0);
assertThat(renderer.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.sampleBufferReadCount).isEqualTo(0);
assertThat(renderer.isEnded).isFalse(); assertThat(renderer.isEnded).isFalse();
...@@ -136,10 +128,8 @@ public final class ExoPlayerTest { ...@@ -136,10 +128,8 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertNoPositionDiscontinuities(); testRunner.assertNoPositionDiscontinuities();
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
assertThat(renderer.formatReadCount).isEqualTo(1); assertThat(renderer.formatReadCount).isEqualTo(1);
assertThat(renderer.sampleBufferReadCount).isEqualTo(1); assertThat(renderer.sampleBufferReadCount).isEqualTo(1);
...@@ -161,10 +151,8 @@ public final class ExoPlayerTest { ...@@ -161,10 +151,8 @@ public final class ExoPlayerTest {
testRunner.assertPositionDiscontinuityReasonsEqual( testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(renderer.formatReadCount).isEqualTo(3); assertThat(renderer.formatReadCount).isEqualTo(3);
assertThat(renderer.sampleBufferReadCount).isEqualTo(3); assertThat(renderer.sampleBufferReadCount).isEqualTo(3);
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
...@@ -187,10 +175,8 @@ public final class ExoPlayerTest { ...@@ -187,10 +175,8 @@ public final class ExoPlayerTest {
Integer[] expectedReasons = new Integer[99]; Integer[] expectedReasons = new Integer[99];
Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(renderer.formatReadCount).isEqualTo(100); assertThat(renderer.formatReadCount).isEqualTo(100);
assertThat(renderer.sampleBufferReadCount).isEqualTo(100); assertThat(renderer.sampleBufferReadCount).isEqualTo(100);
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
...@@ -262,17 +248,14 @@ public final class ExoPlayerTest { ...@@ -262,17 +248,14 @@ public final class ExoPlayerTest {
testRunner.assertPositionDiscontinuityReasonsEqual( testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(audioRenderer.positionResetCount).isEqualTo(1); assertThat(audioRenderer.positionResetCount).isEqualTo(1);
assertThat(videoRenderer.isEnded).isTrue(); assertThat(videoRenderer.isEnded).isTrue();
assertThat(audioRenderer.isEnded).isTrue(); assertThat(audioRenderer.isEnded).isTrue();
} }
@Test @Test
public void testResettingMediaItemsGivesFreshSourceInfo() throws Exception { public void testRepreparationGivesFreshSourceInfo() throws Exception {
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
Object firstSourceManifest = new Object(); Object firstSourceManifest = new Object();
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest);
...@@ -288,8 +271,8 @@ public final class ExoPlayerTest { ...@@ -288,8 +271,8 @@ public final class ExoPlayerTest {
@Nullable TransferListener mediaTransferListener) { @Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener); super.prepareSourceInternal(mediaTransferListener);
// We've queued a source info refresh on the playback thread's event queue. Allow the // 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 // test thread to prepare the player with the third source, and block this thread (the
// playback thread) until the test thread's call to setMediaItems() has returned. // playback thread) until the test thread's call to prepare() has returned.
queuedSourceInfoCountDownLatch.countDown(); queuedSourceInfoCountDownLatch.countDown();
try { try {
completePreparationCountDownLatch.await(); completePreparationCountDownLatch.await();
...@@ -304,13 +287,12 @@ public final class ExoPlayerTest { ...@@ -304,13 +287,12 @@ public final class ExoPlayerTest {
// Prepare the player with a source with the first manifest and a non-empty timeline. Prepare // 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 // 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 // test thread to prepare the player with a third source, and block the playback thread until
// to setMediaItems() has returned. // the test thread's call to prepare() has returned.
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testResettingMediaItemsGivesFreshSourceInfo") new ActionSchedule.Builder("testRepreparation")
.waitForTimelineChanged( .waitForTimelineChanged(firstTimeline)
firstTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .prepareSource(secondSource)
.setMediaItems(secondSource)
.executeRunnable( .executeRunnable(
() -> { () -> {
try { try {
...@@ -319,32 +301,26 @@ public final class ExoPlayerTest { ...@@ -319,32 +301,26 @@ public final class ExoPlayerTest {
// Ignore. // Ignore.
} }
}) })
.setMediaItems(thirdSource) .prepareSource(thirdSource)
.executeRunnable(completePreparationCountDownLatch::countDown) .executeRunnable(completePreparationCountDownLatch::countDown)
.waitForPlaybackState(Player.STATE_READY)
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new Builder() new Builder()
.setMediaSources(firstSource) .setMediaSource(firstSource)
.setRenderers(renderer) .setRenderers(renderer)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertNoPositionDiscontinuities(); testRunner.assertNoPositionDiscontinuities();
// The first source's preparation completed with a real timeline. When the second source was // The first source's preparation completed with a non-empty timeline. When the player was
// prepared, it immediately exposed a dummy timeline, but the source info refresh from the // re-prepared with the second source, it immediately exposed an empty timeline, but the source
// second source was suppressed as we replace it with the third source before the update // info refresh from the second source was suppressed as we re-prepared with the third source.
// arrives. testRunner.assertTimelinesEqual(firstTimeline, Timeline.EMPTY, thirdTimeline);
testRunner.assertTimelinesSame(
dummyTimeline, firstTimeline, dummyTimeline, dummyTimeline, thirdTimeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, Player.TIMELINE_CHANGE_REASON_RESET,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
} }
...@@ -356,8 +332,7 @@ public final class ExoPlayerTest { ...@@ -356,8 +332,7 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testRepeatMode") new ActionSchedule.Builder("testRepeatMode")
.pause() .pause()
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.playUntilStartOfWindow(/* windowIndex= */ 1) .playUntilStartOfWindow(/* windowIndex= */ 1)
.setRepeatMode(Player.REPEAT_MODE_ONE) .setRepeatMode(Player.REPEAT_MODE_ONE)
.playUntilStartOfWindow(/* windowIndex= */ 1) .playUntilStartOfWindow(/* windowIndex= */ 1)
...@@ -392,10 +367,8 @@ public final class ExoPlayerTest { ...@@ -392,10 +367,8 @@ public final class ExoPlayerTest {
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
} }
...@@ -424,7 +397,7 @@ public final class ExoPlayerTest { ...@@ -424,7 +397,7 @@ public final class ExoPlayerTest {
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setRenderers(renderer) .setRenderers(renderer)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
...@@ -470,13 +443,12 @@ public final class ExoPlayerTest { ...@@ -470,13 +443,12 @@ public final class ExoPlayerTest {
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null)) .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null))
.waitForTimelineChanged( .waitForTimelineChanged(adErrorTimeline)
adErrorTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play() .play()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(fakeMediaSource) .setMediaSource(fakeMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -571,31 +543,26 @@ public final class ExoPlayerTest { ...@@ -571,31 +543,26 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testIllegalSeekPositionDoesThrow() throws Exception { public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception {
final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1];
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testIllegalSeekPositionDoesThrow") new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition")
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable( // The illegal seek position will end playback.
new PlayerRunnable() { .seek(/* windowIndex= */ 100, /* positionMs= */ 0)
@Override
public void run(SimpleExoPlayer player) {
try {
player.seekTo(/* windowIndex= */ 100, /* positionMs= */ 0);
} catch (IllegalSeekPositionException e) {
exception[0] = e;
}
}
})
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
.build(); .build();
new Builder() final boolean[] onSeekProcessedCalled = new boolean[1];
.setActionSchedule(actionSchedule) EventListener listener =
.build(context) new EventListener() {
.start() @Override
.blockUntilActionScheduleFinished(TIMEOUT_MS) public void onSeekProcessed() {
.blockUntilEnded(TIMEOUT_MS); onSeekProcessedCalled[0] = true;
assertThat(exception[0]).isNotNull(); }
};
ExoPlayerTestRunner testRunner =
new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context);
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
assertThat(onSeekProcessedCalled[0]).isTrue();
} }
@Test @Test
...@@ -639,7 +606,7 @@ public final class ExoPlayerTest { ...@@ -639,7 +606,7 @@ public final class ExoPlayerTest {
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -667,7 +634,7 @@ public final class ExoPlayerTest { ...@@ -667,7 +634,7 @@ public final class ExoPlayerTest {
}; };
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.build(context) .build(context)
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
...@@ -693,7 +660,7 @@ public final class ExoPlayerTest { ...@@ -693,7 +660,7 @@ public final class ExoPlayerTest {
}; };
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.build(context) .build(context)
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
...@@ -711,7 +678,7 @@ public final class ExoPlayerTest { ...@@ -711,7 +678,7 @@ public final class ExoPlayerTest {
FakeTrackSelector trackSelector = new FakeTrackSelector(); FakeTrackSelector trackSelector = new FakeTrackSelector();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer) .setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.build(context) .build(context)
...@@ -740,7 +707,7 @@ public final class ExoPlayerTest { ...@@ -740,7 +707,7 @@ public final class ExoPlayerTest {
FakeTrackSelector trackSelector = new FakeTrackSelector(); FakeTrackSelector trackSelector = new FakeTrackSelector();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer) .setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.build(context) .build(context)
...@@ -777,7 +744,7 @@ public final class ExoPlayerTest { ...@@ -777,7 +744,7 @@ public final class ExoPlayerTest {
.build(); .build();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer) .setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setActionSchedule(disableTrackAction) .setActionSchedule(disableTrackAction)
...@@ -816,7 +783,7 @@ public final class ExoPlayerTest { ...@@ -816,7 +783,7 @@ public final class ExoPlayerTest {
.build(); .build();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer) .setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setActionSchedule(disableTrackAction) .setActionSchedule(disableTrackAction)
...@@ -840,35 +807,31 @@ public final class ExoPlayerTest { ...@@ -840,35 +807,31 @@ public final class ExoPlayerTest {
@Test @Test
public void testDynamicTimelineChangeReason() throws Exception { 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 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 = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testDynamicTimelineChangeReason") new ActionSchedule.Builder("testDynamicTimelineChangeReason")
.pause() .pause()
.waitForTimelineChanged( .waitForTimelineChanged(timeline1)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null))
.waitForTimelineChanged( .waitForTimelineChanged(timeline2)
timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play() .play()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(dummyTimeline, timeline, timeline2); testRunner.assertTimelinesEqual(timeline1, timeline2);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
} }
@Test @Test
public void testResetMediaItemsWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception {
Timeline fakeTimeline = Timeline fakeTimeline =
new FakeTimeline( new FakeTimeline(
new TimelineWindowDefinition( new TimelineWindowDefinition(
...@@ -891,19 +854,17 @@ public final class ExoPlayerTest { ...@@ -891,19 +854,17 @@ public final class ExoPlayerTest {
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.setShuffleModeEnabled(true) .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. // Plays period 1 and 0 because of the reversed fake shuffle order.
.setMediaItems(/* resetPosition= */ true, secondMediaSource) .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false)
.play() .play()
.waitForPositionDiscontinuity()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(firstMediaSource) .setMediaSource(firstMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0); testRunner.assertPlayedPeriodIndices(0, 1, 0);
} }
...@@ -948,7 +909,7 @@ public final class ExoPlayerTest { ...@@ -948,7 +909,7 @@ public final class ExoPlayerTest {
.executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -981,10 +942,8 @@ public final class ExoPlayerTest { ...@@ -981,10 +942,8 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertNoPositionDiscontinuities(); testRunner.assertNoPositionDiscontinuities();
assertThat(positionHolder[0]).isAtLeast(50L); assertThat(positionHolder[0]).isAtLeast(50L);
} }
...@@ -1015,10 +974,8 @@ public final class ExoPlayerTest { ...@@ -1015,10 +974,8 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertNoPositionDiscontinuities(); testRunner.assertNoPositionDiscontinuities();
assertThat(positionHolder[0]).isAtLeast(50L); assertThat(positionHolder[0]).isAtLeast(50L);
} }
...@@ -1049,11 +1006,9 @@ public final class ExoPlayerTest { ...@@ -1049,11 +1006,9 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
testRunner.assertNoPositionDiscontinuities(); testRunner.assertNoPositionDiscontinuities();
assertThat(positionHolder[0]).isEqualTo(0); assertThat(positionHolder[0]).isEqualTo(0);
} }
...@@ -1099,29 +1054,15 @@ public final class ExoPlayerTest { ...@@ -1099,29 +1054,15 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testSettingNewStartPositionPossibleAfterStopWithReset() throws Exception { public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT);
MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT);
AtomicInteger windowIndexAfterStop = new AtomicInteger();
AtomicLong positionAfterStop = new AtomicLong();
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSettingNewStartPositionPossibleAfterStopWithReset") new ActionSchedule.Builder("testRepreparationAfterStop")
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ true) .stop(/* reset= */ true)
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.seek(/* windowIndex= */ 1, /* positionMs= */ 1000) .prepareSource(secondSource)
.setMediaItems(secondSource)
.prepare()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
windowIndexAfterStop.set(player.getCurrentWindowIndex());
positionAfterStop.set(player.getCurrentPosition());
}
})
.waitForPlaybackState(Player.STATE_READY)
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
...@@ -1131,103 +1072,62 @@ public final class ExoPlayerTest { ...@@ -1131,103 +1072,62 @@ public final class ExoPlayerTest {
.build(context) .build(context)
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlaybackStatesEqual( testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);
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.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, Player.TIMELINE_CHANGE_REASON_RESET,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // stop(true) Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, testRunner.assertNoPositionDiscontinuities();
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(windowIndexAfterStop.get()).isEqualTo(1);
assertThat(positionAfterStop.get()).isAtLeast(1000L);
testRunner.assertPlayedPeriodIndices(0, 1);
} }
@Test @Test
public void testResetPlaylistWithPreviousPosition() throws Exception { public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception {
Object firstWindowId = new Object(); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
Timeline timeline = Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2);
new FakeTimeline( MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT);
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId));
Timeline firstExpectedMaskingTimeline =
new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId);
Object secondWindowId = new Object();
Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId));
Timeline secondExpectedMaskingTimeline =
new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId);
MediaSource secondSource = new FakeMediaSource(secondTimeline);
AtomicLong positionAfterReprepare = new AtomicLong();
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testResetPlaylistWithPreviousPosition") new ActionSchedule.Builder("testSeekAfterStopWithReset")
.pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) .stop(/* reset= */ true)
.setMediaItems(/* windowIndex= */ 0, /* positionMs= */ 2000, secondSource) .waitForPlaybackState(Player.STATE_IDLE)
.waitForTimelineChanged( // If we were still using the first timeline, this would throw.
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .seek(/* windowIndex= */ 1, /* positionMs= */ 0)
.executeRunnable( .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true)
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
positionAfterReprepare.set(player.getCurrentPosition());
}
})
.play()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setTimeline(timeline) .setTimeline(timeline)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.setExpectedPlayerEndedCount(2)
.build(context) .build(context)
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline);
testRunner.assertTimelinesSame(
firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, Player.TIMELINE_CHANGE_REASON_RESET,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
assertThat(positionAfterReprepare.get()).isAtLeast(2000L); testRunner.assertPlayedPeriodIndices(0, 1);
} }
@Test @Test
public void testResetPlaylistStartsFromDefaultPosition() throws Exception { public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception {
Object firstWindowId = new Object();
Timeline timeline = Timeline timeline =
new FakeTimeline( new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object()));
Timeline firstExpectedDummyTimeline =
new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId);
Object secondWindowId = new Object();
Timeline secondTimeline = Timeline secondTimeline =
new FakeTimeline( new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object()));
Timeline secondExpectedDummyTimeline =
new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId);
MediaSource secondSource = new FakeMediaSource(secondTimeline); MediaSource secondSource = new FakeMediaSource(secondTimeline);
AtomicLong positionAfterReprepare = new AtomicLong(); AtomicLong positionAfterReprepare = new AtomicLong();
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testResetPlaylistStartsFromDefaultPosition") new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource")
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000)
.setMediaItems(secondSource) .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true)
.waitForTimelineChanged( .waitForTimelineChanged(secondTimeline)
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
...@@ -1246,14 +1146,8 @@ public final class ExoPlayerTest { ...@@ -1246,14 +1146,8 @@ public final class ExoPlayerTest {
.blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame( testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline);
firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); assertThat(positionAfterReprepare.get()).isAtLeast(2000L);
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 @Test
...@@ -1274,10 +1168,8 @@ public final class ExoPlayerTest { ...@@ -1274,10 +1168,8 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(dummyTimeline, Timeline.EMPTY); testRunner.assertTimelinesEqual(Timeline.EMPTY);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
} }
...@@ -1302,10 +1194,8 @@ public final class ExoPlayerTest { ...@@ -1302,10 +1194,8 @@ public final class ExoPlayerTest {
.start() .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
} }
...@@ -1317,7 +1207,8 @@ public final class ExoPlayerTest { ...@@ -1317,7 +1207,8 @@ public final class ExoPlayerTest {
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.prepare() .prepareSource(
new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false)
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
...@@ -1331,10 +1222,9 @@ public final class ExoPlayerTest { ...@@ -1331,10 +1222,9 @@ public final class ExoPlayerTest {
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
// Expected exception. // Expected exception.
} }
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline, timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
} }
@Test @Test
...@@ -1356,7 +1246,8 @@ public final class ExoPlayerTest { ...@@ -1356,7 +1246,8 @@ public final class ExoPlayerTest {
positionHolder[0] = player.getCurrentPosition(); positionHolder[0] = player.getCurrentPosition();
} }
}) })
.prepare() .prepareSource(
new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false)
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
...@@ -1378,29 +1269,52 @@ public final class ExoPlayerTest { ...@@ -1378,29 +1269,52 @@ public final class ExoPlayerTest {
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
// Expected exception. // Expected exception.
} }
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline, timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
assertThat(positionHolder[0]).isEqualTo(50); assertThat(positionHolder[0]).isEqualTo(50);
assertThat(positionHolder[1]).isEqualTo(50); assertThat(positionHolder[1]).isEqualTo(50);
} }
@Test @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 public void
testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod()
throws Exception { 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(); AtomicInteger windowIndexAfterUpdate = new AtomicInteger();
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod") new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod")
.setShuffleOrder(new FakeShuffleOrder(/* length= */ 0))
.setShuffleModeEnabled(true) .setShuffleModeEnabled(true)
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
// Seeking to an invalid position will end playback. // Seeking to an invalid position will end playback.
.seek( .seek(/* windowIndex= */ 100, /* positionMs= */ 0)
/* windowIndex= */ 100, /* positionMs= */ 0, /* catchIllegalSeekException= */ true)
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
...@@ -1410,13 +1324,12 @@ public final class ExoPlayerTest { ...@@ -1410,13 +1324,12 @@ public final class ExoPlayerTest {
} }
}) })
.build(); .build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(concatenatingMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context);
.start() testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(windowIndexAfterUpdate.get()).isEqualTo(1); assertThat(windowIndexAfterUpdate.get()).isEqualTo(1);
} }
...@@ -1450,7 +1363,7 @@ public final class ExoPlayerTest { ...@@ -1450,7 +1363,7 @@ public final class ExoPlayerTest {
}) })
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(concatenatingMediaSource) .setMediaSource(concatenatingMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -1464,7 +1377,7 @@ public final class ExoPlayerTest { ...@@ -1464,7 +1377,7 @@ public final class ExoPlayerTest {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); final Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
final long[] positionHolder = new long[3]; final long[] positionHolder = new long[3];
final int[] windowIndexHolder = new int[3]; final int[] windowIndexHolder = new int[3];
final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline); final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition")
.pause() .pause()
...@@ -1481,7 +1394,8 @@ public final class ExoPlayerTest { ...@@ -1481,7 +1394,8 @@ public final class ExoPlayerTest {
windowIndexHolder[0] = player.getCurrentWindowIndex(); windowIndexHolder[0] = player.getCurrentWindowIndex();
} }
}) })
.prepare() .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
...@@ -1489,6 +1403,7 @@ public final class ExoPlayerTest { ...@@ -1489,6 +1403,7 @@ public final class ExoPlayerTest {
// Position while repreparing. // Position while repreparing.
positionHolder[1] = player.getCurrentPosition(); positionHolder[1] = player.getCurrentPosition();
windowIndexHolder[1] = player.getCurrentWindowIndex(); windowIndexHolder[1] = player.getCurrentWindowIndex();
secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null);
} }
}) })
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
...@@ -1505,7 +1420,7 @@ public final class ExoPlayerTest { ...@@ -1505,7 +1420,7 @@ public final class ExoPlayerTest {
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(firstMediaSource) .setTimeline(timeline)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context); .build(context);
try { try {
...@@ -1532,8 +1447,7 @@ public final class ExoPlayerTest { ...@@ -1532,8 +1447,7 @@ public final class ExoPlayerTest {
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.seek(0, C.TIME_UNSET) .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false)
.prepare()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.play() .play()
.build(); .build();
...@@ -1550,7 +1464,7 @@ public final class ExoPlayerTest { ...@@ -1550,7 +1464,7 @@ public final class ExoPlayerTest {
}; };
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.setAnalyticsListener(listener) .setAnalyticsListener(listener)
.build(context); .build(context);
...@@ -1566,15 +1480,14 @@ public final class ExoPlayerTest { ...@@ -1566,15 +1480,14 @@ public final class ExoPlayerTest {
@Test @Test
public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); final Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final FakeMediaSource mediaSource2 = new FakeMediaSource(timeline); final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition")
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.setMediaItems(/* resetPosition= */ false, mediaSource2) .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false)
.prepare()
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
...@@ -1590,12 +1503,9 @@ public final class ExoPlayerTest { ...@@ -1590,12 +1503,9 @@ public final class ExoPlayerTest {
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
// Expected exception. // Expected exception.
} }
testRunner.assertTimelinesSame(dummyTimeline, timeline, dummyTimeline, timeline); testRunner.assertTimelinesEqual(timeline, timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
} }
@Test @Test
...@@ -1625,8 +1535,7 @@ public final class ExoPlayerTest { ...@@ -1625,8 +1535,7 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* positionMs= */ 50) .sendMessage(target, /* positionMs= */ 50)
.play() .play()
.build(); .build();
...@@ -1720,12 +1629,17 @@ public final class ExoPlayerTest { ...@@ -1720,12 +1629,17 @@ public final class ExoPlayerTest {
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0)
.sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms)
.sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0)
.sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms)
.play() .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) .waitForPlaybackState(Player.STATE_ENDED)
.build(); .build();
new Builder() new Builder()
...@@ -1772,8 +1686,7 @@ public final class ExoPlayerTest { ...@@ -1772,8 +1686,7 @@ public final class ExoPlayerTest {
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50) .sendMessage(target, /* positionMs= */ 50)
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.seek(/* positionMs= */ 50) .seek(/* positionMs= */ 50)
.build(); .build();
new Builder() new Builder()
...@@ -1814,8 +1727,7 @@ public final class ExoPlayerTest { ...@@ -1814,8 +1727,7 @@ public final class ExoPlayerTest {
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.sendMessage(target, /* positionMs= */ 50) .sendMessage(target, /* positionMs= */ 50)
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.seek(/* positionMs= */ 51) .seek(/* positionMs= */ 51)
.play() .play()
.build(); .build();
...@@ -1894,16 +1806,14 @@ public final class ExoPlayerTest { ...@@ -1894,16 +1806,14 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* positionMs= */ 50) .sendMessage(target, /* positionMs= */ 50)
.executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null))
.waitForTimelineChanged( .waitForTimelineChanged(secondTimeline)
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play() .play()
.build(); .build();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -1919,7 +1829,7 @@ public final class ExoPlayerTest { ...@@ -1919,7 +1829,7 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50)
.play() .play()
.build(); .build();
...@@ -1940,8 +1850,7 @@ public final class ExoPlayerTest { ...@@ -1940,8 +1850,7 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50)
.play() .play()
.build(); .build();
...@@ -1970,17 +1879,15 @@ public final class ExoPlayerTest { ...@@ -1970,17 +1879,15 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSendMessages") new ActionSchedule.Builder("testSendMessages")
.pause() .pause()
.waitForTimelineChanged( .waitForTimelineChanged(timeline)
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50)
.executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null))
.waitForTimelineChanged( .waitForTimelineChanged(secondTimeline)
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.seek(/* windowIndex= */ 0, /* positionMs= */ 0) .seek(/* windowIndex= */ 0, /* positionMs= */ 0)
.play() .play()
.build(); .build();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2014,7 +1921,7 @@ public final class ExoPlayerTest { ...@@ -2014,7 +1921,7 @@ public final class ExoPlayerTest {
.play() .play()
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2152,21 +2059,16 @@ public final class ExoPlayerTest { ...@@ -2152,21 +2059,16 @@ public final class ExoPlayerTest {
/* windowIndex= */ 0, /* windowIndex= */ 0,
/* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US))
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null))
.waitForTimelineChanged( .waitForTimelineChanged(timeline2)
timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play() .play()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .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); testRunner.assertPlayedPeriodIndices(0, 1);
// Assert that the second period was re-created from the new timeline. // Assert that the second period was re-created from the new timeline.
assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3); assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3);
...@@ -2208,7 +2110,7 @@ public final class ExoPlayerTest { ...@@ -2208,7 +2110,7 @@ public final class ExoPlayerTest {
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2327,56 +2229,6 @@ public final class ExoPlayerTest { ...@@ -2327,56 +2229,6 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testRecursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 3);
final AtomicReference<ExoPlayer> 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 { public void testClippedLoopedPeriodsArePlayedFully() throws Exception {
long startPositionUs = 300_000; long startPositionUs = 300_000;
long expectedDurationUs = 700_000; long expectedDurationUs = 700_000;
...@@ -2428,7 +2280,7 @@ public final class ExoPlayerTest { ...@@ -2428,7 +2280,7 @@ public final class ExoPlayerTest {
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setClock(clock) .setClock(clock)
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2462,7 +2314,7 @@ public final class ExoPlayerTest { ...@@ -2462,7 +2314,7 @@ public final class ExoPlayerTest {
List<TrackGroupArray> trackGroupsList = new ArrayList<>(); List<TrackGroupArray> trackGroupsList = new ArrayList<>();
List<TrackSelectionArray> trackSelectionsList = new ArrayList<>(); List<TrackSelectionArray> trackSelectionsList = new ArrayList<>();
new Builder() new Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.setEventListener( .setEventListener(
...@@ -2510,7 +2362,7 @@ public final class ExoPlayerTest { ...@@ -2510,7 +2362,7 @@ public final class ExoPlayerTest {
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new Builder() new Builder()
.setMediaSources(concatenatingMediaSource) .setMediaSource(concatenatingMediaSource)
.setRenderers(renderer) .setRenderers(renderer)
.build(context); .build(context);
try { try {
...@@ -2553,7 +2405,49 @@ public final class ExoPlayerTest { ...@@ -2553,7 +2405,49 @@ public final class ExoPlayerTest {
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new Builder() 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<Boolean> 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) .setActionSchedule(actionSchedule)
.setRenderers(renderer) .setRenderers(renderer)
.build(context); .build(context);
...@@ -2587,7 +2481,7 @@ public final class ExoPlayerTest { ...@@ -2587,7 +2481,7 @@ public final class ExoPlayerTest {
.executeRunnable(concatenatingMediaSource::clear) .executeRunnable(concatenatingMediaSource::clear)
.build(); .build();
new Builder() new Builder()
.setMediaSources(concatenatingMediaSource) .setMediaSource(concatenatingMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2611,7 +2505,7 @@ public final class ExoPlayerTest { ...@@ -2611,7 +2505,7 @@ public final class ExoPlayerTest {
.pause() .pause()
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.seek(/* positionMs= */ 10) .seek(/* positionMs= */ 10)
.waitForSeekProcessed() .waitForTimelineChanged()
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null))
.waitForTimelineChanged() .waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
...@@ -2625,7 +2519,7 @@ public final class ExoPlayerTest { ...@@ -2625,7 +2519,7 @@ public final class ExoPlayerTest {
.play() .play()
.build(); .build();
new Builder() new Builder()
.setMediaSources(concatenatedMediaSource) .setMediaSource(concatenatedMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2656,7 +2550,7 @@ public final class ExoPlayerTest { ...@@ -2656,7 +2550,7 @@ public final class ExoPlayerTest {
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
// Seek 10ms into the second period. // Seek 10ms into the second period.
.seek(/* positionMs= */ periodDurationMs + 10) .seek(/* positionMs= */ periodDurationMs + 10)
.waitForSeekProcessed() .waitForTimelineChanged()
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null))
.waitForTimelineChanged() .waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
...@@ -2671,7 +2565,7 @@ public final class ExoPlayerTest { ...@@ -2671,7 +2565,7 @@ public final class ExoPlayerTest {
.play() .play()
.build(); .build();
new Builder() new Builder()
.setMediaSources(concatenatedMediaSource) .setMediaSource(concatenatedMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2772,10 +2666,10 @@ public final class ExoPlayerTest { ...@@ -2772,10 +2666,10 @@ public final class ExoPlayerTest {
player.addListener(eventListener); player.addListener(eventListener);
} }
}) })
.seek(/* positionMs= */ 5_000) .seek(5_000)
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(fakeMediaSource) .setMediaSource(fakeMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context) .build(context)
.start() .start()
...@@ -2873,506 +2767,6 @@ public final class ExoPlayerTest { ...@@ -2873,506 +2767,6 @@ public final class ExoPlayerTest {
.inOrder(); .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. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
...@@ -21,17 +21,15 @@ import static org.mockito.Mockito.mock; ...@@ -21,17 +21,15 @@ import static org.mockito.Mockito.mock;
import android.net.Uri; import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; 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.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; 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.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import java.util.Collections;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -51,20 +49,19 @@ public final class MediaPeriodQueueTest { ...@@ -51,20 +49,19 @@ public final class MediaPeriodQueueTest {
private MediaPeriodQueue mediaPeriodQueue; private MediaPeriodQueue mediaPeriodQueue;
private AdPlaybackState adPlaybackState; private AdPlaybackState adPlaybackState;
private Timeline timeline;
private Object periodUid; private Object periodUid;
private PlaybackInfo playbackInfo; private PlaybackInfo playbackInfo;
private RendererCapabilities[] rendererCapabilities; private RendererCapabilities[] rendererCapabilities;
private TrackSelector trackSelector; private TrackSelector trackSelector;
private Allocator allocator; private Allocator allocator;
private Playlist playlist; private MediaSource mediaSource;
private FakeMediaSource fakeMediaSource;
private Playlist.MediaSourceHolder mediaSourceHolder;
@Before @Before
public void setUp() { public void setUp() {
mediaPeriodQueue = new MediaPeriodQueue(); mediaPeriodQueue = new MediaPeriodQueue();
playlist = mock(Playlist.class); mediaSource = mock(MediaSource.class);
rendererCapabilities = new RendererCapabilities[0]; rendererCapabilities = new RendererCapabilities[0];
trackSelector = mock(TrackSelector.class); trackSelector = mock(TrackSelector.class);
allocator = mock(Allocator.class); allocator = mock(Allocator.class);
...@@ -72,7 +69,7 @@ public final class MediaPeriodQueueTest { ...@@ -72,7 +69,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {
setupTimeline(); setupTimeline(/* initialPositionUs= */ 0);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* endPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_UNSET,
...@@ -83,7 +80,7 @@ public final class MediaPeriodQueueTest { ...@@ -83,7 +80,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
setupTimeline(/* adGroupTimesUs= */ 0); setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);
advance(); advance();
...@@ -97,7 +94,10 @@ public final class MediaPeriodQueueTest { ...@@ -97,7 +94,10 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { 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( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* endPositionUs= */ FIRST_AD_START_TIME_US, /* endPositionUs= */ FIRST_AD_START_TIME_US,
...@@ -132,7 +132,10 @@ public final class MediaPeriodQueueTest { ...@@ -132,7 +132,10 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { 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( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* endPositionUs= */ FIRST_AD_START_TIME_US, /* endPositionUs= */ FIRST_AD_START_TIME_US,
...@@ -165,7 +168,7 @@ public final class MediaPeriodQueueTest { ...@@ -165,7 +168,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {
setupTimeline(/* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* endPositionUs= */ C.TIME_END_OF_SOURCE, /* endPositionUs= */ C.TIME_END_OF_SOURCE,
...@@ -185,7 +188,10 @@ public final class MediaPeriodQueueTest { ...@@ -185,7 +188,10 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { 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= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad. enqueueNext(); // Content before first ad.
...@@ -195,8 +201,10 @@ public final class MediaPeriodQueueTest { ...@@ -195,8 +201,10 @@ public final class MediaPeriodQueueTest {
enqueueNext(); // Second ad. enqueueNext(); // Second ad.
// Change position of second ad (= change duration of content between ads). // Change position of second ad (= change duration of content between ads).
updateAdPlaybackStateAndTimeline( setupTimeline(
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1); /* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US + 1);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled = boolean changeHandled =
...@@ -210,7 +218,10 @@ public final class MediaPeriodQueueTest { ...@@ -210,7 +218,10 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { 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= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad. enqueueNext(); // Content before first ad.
...@@ -221,8 +232,10 @@ public final class MediaPeriodQueueTest { ...@@ -221,8 +232,10 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading first ad. advanceReading(); // Reading first ad.
// Change position of first ad (= change duration of content before first ad). // Change position of first ad (= change duration of content before first ad).
updateAdPlaybackStateAndTimeline( setupTimeline(
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US); /* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1,
SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled = boolean changeHandled =
...@@ -237,6 +250,7 @@ public final class MediaPeriodQueueTest { ...@@ -237,6 +250,7 @@ public final class MediaPeriodQueueTest {
public void public void
updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US); SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
...@@ -250,8 +264,10 @@ public final class MediaPeriodQueueTest { ...@@ -250,8 +264,10 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading content between ads. advanceReading(); // Reading content between ads.
// Change position of second ad (= change duration of content between ads). // Change position of second ad (= change duration of content between ads).
updateAdPlaybackStateAndTimeline( setupTimeline(
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); /* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US;
...@@ -268,6 +284,7 @@ public final class MediaPeriodQueueTest { ...@@ -268,6 +284,7 @@ public final class MediaPeriodQueueTest {
public void public void
updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US); SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
...@@ -281,8 +298,10 @@ public final class MediaPeriodQueueTest { ...@@ -281,8 +298,10 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading content between ads. advanceReading(); // Reading content between ads.
// Change position of second ad (= change duration of content between ads). // Change position of second ad (= change duration of content between ads).
updateAdPlaybackStateAndTimeline( setupTimeline(
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); /* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US;
...@@ -299,6 +318,7 @@ public final class MediaPeriodQueueTest { ...@@ -299,6 +318,7 @@ public final class MediaPeriodQueueTest {
public void public void
updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US); SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
...@@ -312,8 +332,10 @@ public final class MediaPeriodQueueTest { ...@@ -312,8 +332,10 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading content between ads. advanceReading(); // Reading content between ads.
// Change position of second ad (= change duration of content between ads). // Change position of second ad (= change duration of content between ads).
updateAdPlaybackStateAndTimeline( setupTimeline(
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); /* initialPositionUs= */ 0,
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled = boolean changeHandled =
...@@ -324,25 +346,16 @@ public final class MediaPeriodQueueTest { ...@@ -324,25 +346,16 @@ public final class MediaPeriodQueueTest {
assertThat(getQueueLength()).isEqualTo(3); assertThat(getQueueLength()).isEqualTo(3);
} }
private void setupTimeline(long... adGroupTimesUs) { private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) {
adPlaybackState = adPlaybackState =
new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);
timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
// 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();
periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
mediaPeriodQueue.setTimeline(timeline); mediaPeriodQueue.setTimeline(timeline);
playbackInfo = playbackInfo =
new PlaybackInfo( new PlaybackInfo(
timeline, timeline,
mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0), mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs),
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* contentPositionUs= */ 0, /* contentPositionUs= */ 0,
Player.STATE_READY, Player.STATE_READY,
...@@ -356,25 +369,6 @@ public final class MediaPeriodQueueTest { ...@@ -356,25 +369,6 @@ public final class MediaPeriodQueueTest {
/* positionUs= */ 0); /* 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() { private void advance() {
enqueueNext(); enqueueNext();
if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) {
...@@ -395,7 +389,7 @@ public final class MediaPeriodQueueTest { ...@@ -395,7 +389,7 @@ public final class MediaPeriodQueueTest {
rendererCapabilities, rendererCapabilities,
trackSelector, trackSelector,
allocator, allocator,
playlist, mediaSource,
getNextMediaPeriodInfo(), getNextMediaPeriodInfo(),
new TrackSelectorResult( new TrackSelectorResult(
new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); new RendererConfiguration[0], new TrackSelection[0], /* info= */ null));
...@@ -427,6 +421,11 @@ public final class MediaPeriodQueueTest { ...@@ -427,6 +421,11 @@ public final class MediaPeriodQueueTest {
updateTimeline(); updateTimeline();
} }
private void updateTimeline() {
timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
mediaPeriodQueue.setTimeline(timeline);
}
private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
long startPositionUs, long startPositionUs,
long endPositionUs, long endPositionUs,
......
/*
* 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> 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<Playlist.MediaSourceHolder> createFakeHolders() {
MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1));
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
for (int i = 0; i < PLAYLIST_SIZE; i++) {
holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true));
}
return holders;
}
private static List<Playlist.MediaSourceHolder> createFakeHoldersWithSources(
boolean useLazyPreparation, MediaSource... sources) {
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
for (MediaSource mediaSource : sources) {
holders.add(
new Playlist.MediaSourceHolder(
mediaSource, /* useLazyPreparation= */ useLazyPreparation));
}
return holders;
}
}
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
...@@ -60,142 +58,4 @@ public class TimelineTest { ...@@ -60,142 +58,4 @@ public class TimelineTest {
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 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());
}
} }
...@@ -44,6 +44,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; ...@@ -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;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; 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.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
...@@ -132,29 +133,24 @@ public final class AnalyticsCollectorTest { ...@@ -132,29 +133,24 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly( .containsExactly(
WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
listener.assertNoMoreEvents(); listener.assertNoMoreEvents();
} }
@Test @Test
public void testSinglePeriod() throws Exception { public void testSinglePeriod() throws Exception {
FakeMediaSource mediaSource = FakeMediaSource mediaSource =
new FakeMediaSource( new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource); TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline); populateEventIds(SINGLE_PERIOD_TIMELINE);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly( .containsExactly(
WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* setPlayWhenReady */,
WINDOW_0 /* BUFFERING */, WINDOW_0 /* BUFFERING */,
period0 /* READY */, period0 /* READY */,
period0 /* ENDED */); period0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0 /* started */, period0 /* stopped */); .containsExactly(period0 /* started */, period0 /* stopped */);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0);
...@@ -183,14 +179,9 @@ public final class AnalyticsCollectorTest { ...@@ -183,14 +179,9 @@ public final class AnalyticsCollectorTest {
public void testAutomaticPeriodTransition() throws Exception { public void testAutomaticPeriodTransition() throws Exception {
MediaSource mediaSource = MediaSource mediaSource =
new ConcatenatingMediaSource( new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT),
new FakeMediaSource( new FakeMediaSource(
SINGLE_PERIOD_TIMELINE, SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT));
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT),
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource); TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline); populateEventIds(listener.lastReportedTimeline);
...@@ -200,8 +191,7 @@ public final class AnalyticsCollectorTest { ...@@ -200,8 +191,7 @@ public final class AnalyticsCollectorTest {
WINDOW_0 /* BUFFERING */, WINDOW_0 /* BUFFERING */,
period0 /* READY */, period0 /* READY */,
period1 /* ENDED */); period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0, period0, period0, period0); .containsExactly(period0, period0, period0, period0);
...@@ -243,8 +233,8 @@ public final class AnalyticsCollectorTest { ...@@ -243,8 +233,8 @@ public final class AnalyticsCollectorTest {
public void testPeriodTransitionWithRendererChange() throws Exception { public void testPeriodTransitionWithRendererChange() throws Exception {
MediaSource mediaSource = MediaSource mediaSource =
new ConcatenatingMediaSource( 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.AUDIO_FORMAT)); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource); TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline); populateEventIds(listener.lastReportedTimeline);
...@@ -256,8 +246,7 @@ public final class AnalyticsCollectorTest { ...@@ -256,8 +246,7 @@ public final class AnalyticsCollectorTest {
period1 /* BUFFERING */, period1 /* BUFFERING */,
period1 /* READY */, period1 /* READY */,
period1 /* ENDED */); period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0, period0, period0, period0); .containsExactly(period0, period0, period0, period0);
...@@ -297,8 +286,8 @@ public final class AnalyticsCollectorTest { ...@@ -297,8 +286,8 @@ public final class AnalyticsCollectorTest {
public void testSeekToOtherPeriod() throws Exception { public void testSeekToOtherPeriod() throws Exception {
MediaSource mediaSource = MediaSource mediaSource =
new ConcatenatingMediaSource( 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.AUDIO_FORMAT)); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT));
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("AnalyticsCollectorTest") new ActionSchedule.Builder("AnalyticsCollectorTest")
.pause() .pause()
...@@ -319,8 +308,7 @@ public final class AnalyticsCollectorTest { ...@@ -319,8 +308,7 @@ public final class AnalyticsCollectorTest {
period1 /* READY */, period1 /* READY */,
period1 /* setPlayWhenReady=true */, period1 /* setPlayWhenReady=true */,
period1 /* ENDED */); period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1);
...@@ -362,11 +350,9 @@ public final class AnalyticsCollectorTest { ...@@ -362,11 +350,9 @@ public final class AnalyticsCollectorTest {
public void testSeekBackAfterReadingAhead() throws Exception { public void testSeekBackAfterReadingAhead() throws Exception {
MediaSource mediaSource = MediaSource mediaSource =
new ConcatenatingMediaSource( new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT),
new FakeMediaSource( new FakeMediaSource(
SINGLE_PERIOD_TIMELINE, SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT));
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
long periodDurationMs = long periodDurationMs =
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
...@@ -394,8 +380,7 @@ public final class AnalyticsCollectorTest { ...@@ -394,8 +380,7 @@ public final class AnalyticsCollectorTest {
period1Seq2 /* BUFFERING */, period1Seq2 /* BUFFERING */,
period1Seq2 /* READY */, period1Seq2 /* READY */,
period1Seq2 /* ENDED */); period1Seq2 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(period0, period1Seq2); .containsExactly(period0, period1Seq2);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
...@@ -443,28 +428,18 @@ public final class AnalyticsCollectorTest { ...@@ -443,28 +428,18 @@ public final class AnalyticsCollectorTest {
@Test @Test
public void testPrepareNewSource() throws Exception { public void testPrepareNewSource() throws Exception {
MediaSource mediaSource1 = MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
MediaSource mediaSource2 =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("AnalyticsCollectorTest") new ActionSchedule.Builder("AnalyticsCollectorTest")
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.setMediaItems(/* resetPosition= */ false, mediaSource2) .prepareSource(mediaSource2)
.play() .play()
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);
// Populate all event ids with last timeline (after second prepare). populateEventIds(SINGLE_PERIOD_TIMELINE);
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));
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly( .containsExactly(
WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* setPlayWhenReady=true */,
...@@ -476,16 +451,12 @@ public final class AnalyticsCollectorTest { ...@@ -476,16 +451,12 @@ public final class AnalyticsCollectorTest {
period0Seq1 /* READY */, period0Seq1 /* READY */,
period0Seq1 /* ENDED */); period0Seq1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly( .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */);
WINDOW_0 /* PLAYLIST_CHANGE */,
WINDOW_0 /* DYNAMIC */,
WINDOW_0 /* PLAYLIST_CHANGE */,
WINDOW_0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))
.containsExactly( .containsExactly(
period0Seq0 /* prepared */, WINDOW_0 /* setMediaItems */, period0Seq1 /* prepared */); period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */);
assertThat(listener.getEvents(EVENT_LOAD_STARTED)) assertThat(listener.getEvents(EVENT_LOAD_STARTED))
.containsExactly( .containsExactly(
WINDOW_0 /* manifest */, WINDOW_0 /* manifest */,
...@@ -519,20 +490,19 @@ public final class AnalyticsCollectorTest { ...@@ -519,20 +490,19 @@ public final class AnalyticsCollectorTest {
@Test @Test
public void testReprepareAfterError() throws Exception { public void testReprepareAfterError() throws Exception {
MediaSource mediaSource = MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("AnalyticsCollectorTest") new ActionSchedule.Builder("AnalyticsCollectorTest")
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 0) .seek(/* positionMs= */ 0)
.prepare() .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false)
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
populateEventIds(listener.lastReportedTimeline); populateEventIds(SINGLE_PERIOD_TIMELINE);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly( .containsExactly(
WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* setPlayWhenReady=true */,
...@@ -586,7 +556,7 @@ public final class AnalyticsCollectorTest { ...@@ -586,7 +556,7 @@ public final class AnalyticsCollectorTest {
@Test @Test
public void testDynamicTimelineChange() throws Exception { public void testDynamicTimelineChange() throws Exception {
MediaSource childMediaSource = MediaSource childMediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
final ConcatenatingMediaSource concatenatedMediaSource = final ConcatenatingMediaSource concatenatedMediaSource =
new ConcatenatingMediaSource(childMediaSource, childMediaSource); new ConcatenatingMediaSource(childMediaSource, childMediaSource);
long periodDurationMs = long periodDurationMs =
...@@ -618,11 +588,7 @@ public final class AnalyticsCollectorTest { ...@@ -618,11 +588,7 @@ public final class AnalyticsCollectorTest {
period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* setPlayWhenReady=true */,
period1Seq0 /* BUFFERING */, period1Seq0 /* BUFFERING */,
period1Seq0 /* ENDED */); period1Seq0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0);
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */,
period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly( .containsExactly(
window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);
...@@ -676,7 +642,7 @@ public final class AnalyticsCollectorTest { ...@@ -676,7 +642,7 @@ public final class AnalyticsCollectorTest {
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); 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_STARTED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0);
} }
...@@ -743,7 +709,7 @@ public final class AnalyticsCollectorTest { ...@@ -743,7 +709,7 @@ public final class AnalyticsCollectorTest {
TestAnalyticsListener listener = new TestAnalyticsListener(); TestAnalyticsListener listener = new TestAnalyticsListener();
try { try {
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(mediaSource) .setMediaSource(mediaSource)
.setRenderersFactory(renderersFactory) .setRenderersFactory(renderersFactory)
.setAnalyticsListener(listener) .setAnalyticsListener(listener)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
...@@ -765,7 +731,7 @@ public final class AnalyticsCollectorTest { ...@@ -765,7 +731,7 @@ public final class AnalyticsCollectorTest {
private boolean renderedFirstFrame; private boolean renderedFirstFrame;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT); super(Builder.VIDEO_FORMAT);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
} }
...@@ -823,7 +789,7 @@ public final class AnalyticsCollectorTest { ...@@ -823,7 +789,7 @@ public final class AnalyticsCollectorTest {
private boolean notifiedAudioSessionId; private boolean notifiedAudioSessionId;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT); super(Builder.AUDIO_FORMAT);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
} }
...@@ -907,12 +873,10 @@ public final class AnalyticsCollectorTest { ...@@ -907,12 +873,10 @@ public final class AnalyticsCollectorTest {
public Timeline lastReportedTimeline; public Timeline lastReportedTimeline;
private final List<Timeline> reportedTimelines;
private final ArrayList<ReportedEvent> reportedEvents; private final ArrayList<ReportedEvent> reportedEvents;
public TestAnalyticsListener() { public TestAnalyticsListener() {
reportedEvents = new ArrayList<>(); reportedEvents = new ArrayList<>();
reportedTimelines = new ArrayList<>();
lastReportedTimeline = Timeline.EMPTY; lastReportedTimeline = Timeline.EMPTY;
} }
...@@ -942,7 +906,6 @@ public final class AnalyticsCollectorTest { ...@@ -942,7 +906,6 @@ public final class AnalyticsCollectorTest {
@Override @Override
public void onTimelineChanged(EventTime eventTime, int reason) { public void onTimelineChanged(EventTime eventTime, int reason) {
lastReportedTimeline = eventTime.timeline; lastReportedTimeline = eventTime.timeline;
reportedTimelines.add(eventTime.timeline);
reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime));
} }
......
...@@ -21,7 +21,6 @@ import androidx.annotation.Nullable; ...@@ -21,7 +21,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.IllegalSeekPositionException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.PlayerMessage;
...@@ -29,7 +28,6 @@ import com.google.android.exoplayer2.PlayerMessage.Target; ...@@ -29,7 +28,6 @@ import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource; 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.ActionNode;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
...@@ -38,8 +36,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet ...@@ -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.ConditionVariable;
import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import java.util.Arrays;
import java.util.List;
/** Base class for actions to perform during playback tests. */ /** Base class for actions to perform during playback tests. */
public abstract class Action { public abstract class Action {
...@@ -116,7 +112,6 @@ public abstract class Action { ...@@ -116,7 +112,6 @@ public abstract class Action {
private final Integer windowIndex; private final Integer windowIndex;
private final long positionMs; private final long positionMs;
private final boolean catchIllegalSeekException;
/** /**
* Action calls {@link Player#seekTo(long)}. * Action calls {@link Player#seekTo(long)}.
...@@ -128,7 +123,6 @@ public abstract class Action { ...@@ -128,7 +123,6 @@ public abstract class Action {
super(tag, "Seek:" + positionMs); super(tag, "Seek:" + positionMs);
this.windowIndex = null; this.windowIndex = null;
this.positionMs = positionMs; this.positionMs = positionMs;
catchIllegalSeekException = false;
} }
/** /**
...@@ -137,188 +131,21 @@ public abstract class Action { ...@@ -137,188 +131,21 @@ public abstract class Action {
* @param tag A tag to use for logging. * @param tag A tag to use for logging.
* @param windowIndex The window to seek to. * @param windowIndex The window to seek to.
* @param positionMs The seek position. * @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); super(tag, "Seek:" + positionMs);
this.windowIndex = windowIndex; this.windowIndex = windowIndex;
this.positionMs = positionMs; this.positionMs = positionMs;
this.catchIllegalSeekException = catchIllegalSeekException;
} }
@Override @Override
protected void doActionImpl( protected void doActionImpl(
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
try {
if (windowIndex == null) { if (windowIndex == null) {
player.seekTo(positionMs); player.seekTo(positionMs);
} else { } else {
player.seekTo(windowIndex, positionMs); player.seekTo(windowIndex, positionMs);
} }
} catch (IllegalSeekPositionException e) {
if (!catchIllegalSeekException) {
throw e;
}
}
}
}
/** 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();
} }
} }
...@@ -380,6 +207,7 @@ public abstract class Action { ...@@ -380,6 +207,7 @@ public abstract class Action {
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.setPlayWhenReady(playWhenReady); player.setPlayWhenReady(playWhenReady);
} }
} }
/** /**
...@@ -440,28 +268,42 @@ public abstract class Action { ...@@ -440,28 +268,42 @@ public abstract class Action {
} }
} }
/** Calls {@link ExoPlayer#prepare()}. */ /** Calls {@link ExoPlayer#prepare(MediaSource)}. */
public static final class Prepare extends Action { 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 PrepareSource(String tag, MediaSource mediaSource) {
this(tag, mediaSource, true, true);
}
/** @param tag A tag to use for logging. */ /** @param tag A tag to use for logging. */
public Prepare(String tag) { public PrepareSource(
super(tag, "Prepare"); String tag, MediaSource mediaSource, boolean resetPosition, boolean resetState) {
super(tag, "PrepareSource");
this.mediaSource = mediaSource;
this.resetPosition = resetPosition;
this.resetState = resetState;
} }
@Override @Override
protected void doActionImpl( protected void doActionImpl(
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.prepare(); player.prepare(mediaSource, resetPosition, resetState);
} }
} }
/** Calls {@link Player#setRepeatMode(int)}. */ /** Calls {@link Player#setRepeatMode(int)}. */
public static final class SetRepeatMode extends Action { 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. */ /** @param tag A tag to use for logging. */
public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) {
super(tag, "SetRepeatMode: " + repeatMode); super(tag, "SetRepeatMode:" + repeatMode);
this.repeatMode = repeatMode; this.repeatMode = repeatMode;
} }
...@@ -472,27 +314,6 @@ public abstract class Action { ...@@ -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)}. */ /** Calls {@link Player#setShuffleModeEnabled(boolean)}. */
public static final class SetShuffleModeEnabled extends Action { public static final class SetShuffleModeEnabled extends Action {
...@@ -500,7 +321,7 @@ public abstract class Action { ...@@ -500,7 +321,7 @@ public abstract class Action {
/** @param tag A tag to use for logging. */ /** @param tag A tag to use for logging. */
public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) {
super(tag, "SetShuffleModeEnabled: " + shuffleModeEnabled); super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled);
this.shuffleModeEnabled = shuffleModeEnabled; this.shuffleModeEnabled = shuffleModeEnabled;
} }
...@@ -587,6 +408,7 @@ public abstract class Action { ...@@ -587,6 +408,7 @@ public abstract class Action {
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.setPlaybackParameters(playbackParameters); player.setPlaybackParameters(playbackParameters);
} }
} }
/** Throws a playback exception on the playback thread. */ /** Throws a playback exception on the playback thread. */
...@@ -683,35 +505,18 @@ public abstract class Action { ...@@ -683,35 +505,18 @@ public abstract class Action {
/** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */ /** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */
public static final class WaitForTimelineChanged extends Action { public static final class WaitForTimelineChanged extends Action {
private final Timeline expectedTimeline; @Nullable private final Timeline expectedTimeline;
private final boolean ignoreExpectedReason;
@Player.TimelineChangeReason private final int expectedReason;
/** /**
* 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 tag A tag to use for logging.
* @param expectedTimeline The expected timeline or null if any timeline change is relevant. * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline
* @param expectedReason The expected timeline change reason. * change.
*/ */
public WaitForTimelineChanged( public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) {
String tag, Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
super(tag, "WaitForTimelineChanged"); super(tag, "WaitForTimelineChanged");
this.expectedTimeline = expectedTimeline; 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 @Override
...@@ -729,9 +534,7 @@ public abstract class Action { ...@@ -729,9 +534,7 @@ public abstract class Action {
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) { Timeline timeline, @Player.TimelineChangeReason int reason) {
if ((expectedTimeline == null if (expectedTimeline == null || timeline.equals(expectedTimeline)) {
|| TestUtil.areTimelinesSame(expectedTimeline, timeline))
&& (ignoreExpectedReason || expectedReason == reason)) {
player.removeListener(this); player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler); nextAction.schedule(player, trackSelector, surface, handler);
} }
...@@ -919,7 +722,7 @@ public abstract class Action { ...@@ -919,7 +722,7 @@ public abstract class Action {
} }
} }
/** Calls {@code Runnable.run()}. */ /** Calls {@link Runnable#run()}. */
public static final class ExecuteRunnable extends Action { public static final class ExecuteRunnable extends Action {
private final Runnable runnable; private final Runnable runnable;
......
...@@ -27,10 +27,10 @@ import com.google.android.exoplayer2.PlayerMessage.Target; ...@@ -27,10 +27,10 @@ import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource; 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.ClearVideoSurface;
import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable;
import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition; 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.Seek;
import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SendMessages;
import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady;
...@@ -38,7 +38,6 @@ import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; ...@@ -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.SetRendererDisabled;
import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; 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.SetVideoSurface;
import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.Stop;
import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
...@@ -170,19 +169,7 @@ public final class ActionSchedule { ...@@ -170,19 +169,7 @@ public final class ActionSchedule {
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder seek(int windowIndex, long positionMs) { public Builder seek(int windowIndex, long positionMs) {
return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false)); return apply(new Seek(tag, windowIndex, positionMs));
}
/**
* 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));
} }
/** /**
...@@ -314,100 +301,23 @@ public final class ActionSchedule { ...@@ -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));
}
/**
* Schedules a set media items 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. * @return The builder, for convenience.
*/ */
public Builder moveMediaItem(int currentIndex, int newIndex) { public Builder prepareSource(MediaSource mediaSource) {
return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex)); return apply(new PrepareSource(tag, mediaSource));
} }
/** /**
* Schedules a remove media item action to be executed. * Schedules a new source preparation 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) * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean)
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder removeMediaItem(int index) { public Builder prepareSource(
return apply(new Action.RemoveMediaItem(tag, index)); MediaSource mediaSource, boolean resetPosition, boolean resetState) {
} return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState));
/**
* 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));
} }
/** /**
...@@ -420,16 +330,6 @@ public final class ActionSchedule { ...@@ -420,16 +330,6 @@ public final class ActionSchedule {
} }
/** /**
* 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. * Schedules a shuffle setting action to be executed.
* *
* @return The builder, for convenience. * @return The builder, for convenience.
...@@ -482,19 +382,18 @@ public final class ActionSchedule { ...@@ -482,19 +382,18 @@ public final class ActionSchedule {
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder waitForTimelineChanged() { 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. * Schedules a delay until the timeline changed to a specified expected timeline.
* *
* @param expectedTimeline The expected timeline. * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline
* @param expectedReason The expected reason of the timeline change. * change.
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder waitForTimelineChanged( public Builder waitForTimelineChanged(Timeline expectedTimeline) {
Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { return apply(new WaitForTimelineChanged(tag, expectedTimeline));
return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason));
} }
/** /**
......
...@@ -141,8 +141,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { ...@@ -141,8 +141,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest {
pendingSchedule = null; pendingSchedule = null;
} }
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = buildDrmSessionManager(userAgent); DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = buildDrmSessionManager(userAgent);
player.setMediaItem(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager));
player.prepare();
} }
@Override @Override
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.testutil; package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.assertTrue;
import android.content.Context; import android.content.Context;
import android.os.HandlerThread; import android.os.HandlerThread;
...@@ -45,7 +44,6 @@ import com.google.android.exoplayer2.util.HandlerWrapper; ...@@ -45,7 +44,6 @@ import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
...@@ -74,8 +72,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -74,8 +72,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private Clock clock; private Clock clock;
private Timeline timeline; private Timeline timeline;
private List<MediaSource> mediaSources;
private Object manifest; private Object manifest;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private LoadControl loadControl; private LoadControl loadControl;
private BandwidthMeter bandwidthMeter; private BandwidthMeter bandwidthMeter;
...@@ -87,22 +85,18 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -87,22 +85,18 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private AnalyticsListener analyticsListener; private AnalyticsListener analyticsListener;
private Integer expectedPlayerEndedCount; private Integer expectedPlayerEndedCount;
public Builder() {
mediaSources = new ArrayList<>();
}
/** /**
* Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The * 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 * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link
* FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is * 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 * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test
* runner. * runner.
* @return This builder. * @return This builder.
*/ */
public Builder setTimeline(Timeline timeline) { public Builder setTimeline(Timeline timeline) {
assertThat(mediaSources).isEmpty(); assertThat(mediaSource).isNull();
this.timeline = timeline; this.timeline = timeline;
return this; return this;
} }
...@@ -110,30 +104,30 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -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 * 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 * 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. * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner.
* @return This builder. * @return This builder.
*/ */
public Builder setManifest(Object manifest) { public Builder setManifest(Object manifest) {
assertThat(mediaSources).isEmpty(); assertThat(mediaSource).isNull();
this.manifest = manifest; this.manifest = manifest;
return this; 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)} * 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 * and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to
* #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. * {@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. * @return This builder.
*/ */
public Builder setMediaSources(MediaSource... mediaSources) { public Builder setMediaSource(MediaSource mediaSource) {
assertThat(timeline).isNull(); assertThat(timeline).isNull();
assertThat(manifest).isNull(); assertThat(manifest).isNull();
this.mediaSources = Arrays.asList(mediaSources); this.mediaSource = mediaSource;
return this; return this;
} }
...@@ -177,7 +171,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -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 * 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 * 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 * #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. * #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set.
* *
* @param supportedFormats A list of supported {@link Format}s. * @param supportedFormats A list of supported {@link Format}s.
...@@ -231,7 +225,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -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 * 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. * @param actionSchedule An {@link ActionSchedule} to be used by the test runner.
* @return This builder. * @return This builder.
...@@ -312,11 +306,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -312,11 +306,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (clock == null) { if (clock == null) {
clock = new AutoAdvancingFakeClock(); clock = new AutoAdvancingFakeClock();
} }
if (mediaSources.isEmpty()) { if (mediaSource == null) {
if (timeline == null) { if (timeline == null) {
timeline = new FakeTimeline(/* windowCount= */ 1, manifest); timeline = new FakeTimeline(/* windowCount= */ 1, manifest);
} }
mediaSources.add(new FakeMediaSource(timeline, supportedFormats)); mediaSource = new FakeMediaSource(timeline, supportedFormats);
} }
if (expectedPlayerEndedCount == null) { if (expectedPlayerEndedCount == null) {
expectedPlayerEndedCount = 1; expectedPlayerEndedCount = 1;
...@@ -324,7 +318,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -324,7 +318,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
return new ExoPlayerTestRunner( return new ExoPlayerTestRunner(
context, context,
clock, clock,
mediaSources, mediaSource,
renderersFactory, renderersFactory,
trackSelector, trackSelector,
loadControl, loadControl,
...@@ -338,7 +332,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -338,7 +332,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final Context context; private final Context context;
private final Clock clock; private final Clock clock;
private final List<MediaSource> mediaSources; private final MediaSource mediaSource;
private final RenderersFactory renderersFactory; private final RenderersFactory renderersFactory;
private final DefaultTrackSelector trackSelector; private final DefaultTrackSelector trackSelector;
private final LoadControl loadControl; private final LoadControl loadControl;
...@@ -355,7 +349,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -355,7 +349,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final ArrayList<Integer> timelineChangeReasons; private final ArrayList<Integer> timelineChangeReasons;
private final ArrayList<Integer> periodIndices; private final ArrayList<Integer> periodIndices;
private final ArrayList<Integer> discontinuityReasons; private final ArrayList<Integer> discontinuityReasons;
private final ArrayList<Integer> playbackStates;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private Exception exception; private Exception exception;
...@@ -365,7 +358,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -365,7 +358,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private ExoPlayerTestRunner( private ExoPlayerTestRunner(
Context context, Context context,
Clock clock, Clock clock,
List<MediaSource> mediaSources, MediaSource mediaSource,
RenderersFactory renderersFactory, RenderersFactory renderersFactory,
DefaultTrackSelector trackSelector, DefaultTrackSelector trackSelector,
LoadControl loadControl, LoadControl loadControl,
...@@ -376,7 +369,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -376,7 +369,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
int expectedPlayerEndedCount) { int expectedPlayerEndedCount) {
this.context = context; this.context = context;
this.clock = clock; this.clock = clock;
this.mediaSources = mediaSources; this.mediaSource = mediaSource;
this.renderersFactory = renderersFactory; this.renderersFactory = renderersFactory;
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
this.loadControl = loadControl; this.loadControl = loadControl;
...@@ -388,7 +381,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -388,7 +381,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
this.timelineChangeReasons = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>();
this.periodIndices = new ArrayList<>(); this.periodIndices = new ArrayList<>();
this.discontinuityReasons = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>();
this.playbackStates = new ArrayList<>();
this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);
this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
this.playerThread = new HandlerThread("ExoPlayerTest thread"); this.playerThread = new HandlerThread("ExoPlayerTest thread");
...@@ -434,10 +426,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -434,10 +426,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (actionSchedule != null) { if (actionSchedule != null) {
actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);
} }
player.setMediaItems(mediaSources, /* resetPosition= */ false); player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
if (doPrepare) {
player.prepare();
}
} catch (Exception e) { } catch (Exception e) {
handleException(e); handleException(e);
} }
...@@ -489,16 +478,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -489,16 +478,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
/** /**
* Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, * 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 * int)} are equal to the provided timelines.
* comparing period ids which may be different due to id mapping of child source period ids.
* *
* @param timelines A list of expected {@link Timeline}s. * @param timelines A list of expected {@link Timeline}s.
*/ */
public void assertTimelinesSame(Timeline... timelines) { public void assertTimelinesEqual(Timeline... timelines) {
assertThat(this.timelines).hasSize(timelines.length); assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder();
for (int i = 0; i < timelines.length; i++) {
assertTrue(TestUtil.areTimelinesSame(timelines[i], this.timelines.get(i)));
}
} }
/** /**
...@@ -511,15 +496,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -511,15 +496,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
} }
/** /**
* 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 * Asserts that the last track group array reported by {@link
* Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the * Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the
* provided track group array. * provided track group array.
...@@ -594,12 +570,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -594,12 +570,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
@Override @Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineChangeReasons.add(reason);
timelines.add(timeline); timelines.add(timeline);
int currentIndex = player.getCurrentPeriodIndex(); timelineChangeReasons.add(reason);
if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) { if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) {
// Ignore timeline changes that do not change the period index. periodIndices.add(player.getCurrentPeriodIndex());
periodIndices.add(currentIndex);
} }
} }
...@@ -610,7 +584,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -610,7 +584,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
playbackStates.add(playbackState);
playerWasPrepared |= playbackState != Player.STATE_IDLE; playerWasPrepared |= playbackState != Player.STATE_IDLE;
if (playbackState == Player.STATE_ENDED if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playerWasPrepared)) { || (playbackState == Player.STATE_IDLE && playerWasPrepared)) {
...@@ -657,9 +630,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -657,9 +630,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
renderersFactory, renderersFactory,
trackSelector, trackSelector,
loadControl, loadControl,
/* drmSessionManager= */ null,
bandwidthMeter, bandwidthMeter,
new AnalyticsCollector(clock), new AnalyticsCollector(clock),
/* useLazyPreparation= */ false,
clock, clock,
Looper.myLooper()); Looper.myLooper());
} }
......
...@@ -25,10 +25,8 @@ import com.google.android.exoplayer2.PlayerMessage; ...@@ -25,10 +25,8 @@ import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource; 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.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import java.util.List;
/** /**
* An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException}
...@@ -99,11 +97,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -99,11 +97,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
} }
@Override @Override
public void prepare() {
throw new UnsupportedOperationException();
}
@Override
public void prepare(MediaSource mediaSource) { public void prepare(MediaSource mediaSource) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
...@@ -114,77 +107,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -114,77 +107,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
} }
@Override @Override
public void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition) {
throw new UnsupportedOperationException();
}
@Override
public void setMediaItems(List<MediaSource> mediaItems) {
throw new UnsupportedOperationException();
}
@Override
public void setMediaItems(
List<MediaSource> 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<MediaSource> mediaSources) {
throw new UnsupportedOperationException();
}
@Override
public void addMediaItems(int index, List<MediaSource> 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) { public void setPlayWhenReady(boolean playWhenReady) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
...@@ -205,11 +127,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -205,11 +127,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
} }
@Override @Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
throw new UnsupportedOperationException();
}
@Override
public void setShuffleModeEnabled(boolean shuffleModeEnabled) { public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
......
...@@ -26,7 +26,6 @@ import android.graphics.BitmapFactory; ...@@ -26,7 +26,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; 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.DatabaseProvider;
import com.google.android.exoplayer2.database.DefaultDatabaseProvider; import com.google.android.exoplayer2.database.DefaultDatabaseProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
...@@ -399,61 +398,4 @@ public class TestUtil { ...@@ -399,61 +398,4 @@ public class TestUtil {
} }
return new DefaultExtractorInput(dataSource, position, length); 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;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment