Commit 36fa9d5a by bachinger Committed by Oliver Woodman

add top-level playlist API

Design doc: https://docs.google.com/document/d/11h0S91KI5TB3NNZUtsCzg0S7r6nyTnF_tDZZAtmY93g/edit

Issue: #6161, #5155
PiperOrigin-RevId: 286020313
parent 43bbc172
Showing with 1979 additions and 641 deletions
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
([6773](https://github.com/google/ExoPlayer/issues/6773)). ([6773](https://github.com/google/ExoPlayer/issues/6773)).
* Suppress ProGuard warnings for compile-time `javax.annotation` package * Suppress ProGuard warnings for compile-time `javax.annotation` package
([#6771](https://github.com/google/ExoPlayer/issues/6771)). ([#6771](https://github.com/google/ExoPlayer/issues/6771)).
* Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)).
### 2.11.0 (2019-12-11) ### ### 2.11.0 (2019-12-11) ###
......
...@@ -51,7 +51,6 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep ...@@ -51,7 +51,6 @@ 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.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
...@@ -82,6 +81,8 @@ import java.lang.reflect.Constructor; ...@@ -82,6 +81,8 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler; 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.List;
/** An activity that plays media using {@link SimpleExoPlayer}. */ /** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends AppCompatActivity public class PlayerActivity extends AppCompatActivity
...@@ -147,7 +148,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -147,7 +148,7 @@ public class PlayerActivity extends AppCompatActivity
private DataSource.Factory dataSourceFactory; private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private MediaSource mediaSource; private List<MediaSource> mediaSources;
private DefaultTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters; private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
...@@ -349,12 +350,10 @@ public class PlayerActivity extends AppCompatActivity ...@@ -349,12 +350,10 @@ public class PlayerActivity extends AppCompatActivity
private void initializePlayer() { private void initializePlayer() {
if (player == null) { if (player == null) {
Intent intent = getIntent(); Intent intent = getIntent();
mediaSources = createTopLevelMediaSources(intent);
mediaSource = createTopLevelMediaSource(intent); if (mediaSources.isEmpty()) {
if (mediaSource == null) {
return; return;
} }
TrackSelection.Factory trackSelectionFactory; TrackSelection.Factory trackSelectionFactory;
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
...@@ -395,13 +394,12 @@ public class PlayerActivity extends AppCompatActivity ...@@ -395,13 +394,12 @@ public class PlayerActivity extends AppCompatActivity
if (haveStartPosition) { if (haveStartPosition) {
player.seekTo(startWindow, startPosition); player.seekTo(startWindow, startPosition);
} }
player.setMediaSource(mediaSource); player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition);
player.prepare(); player.prepare();
updateButtonVisibility(); updateButtonVisibility();
} }
@Nullable private List<MediaSource> createTopLevelMediaSources(Intent intent) {
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)) {
...@@ -429,10 +427,10 @@ public class PlayerActivity extends AppCompatActivity ...@@ -429,10 +427,10 @@ public class PlayerActivity extends AppCompatActivity
} }
} }
MediaSource[] mediaSources = new MediaSource[samples.length]; List<MediaSource> mediaSources = new ArrayList<>();
for (int i = 0; i < samples.length; i++) { for (UriSample sample : samples) {
mediaSources[i] = createLeafMediaSource(samples[i]); MediaSource mediaSource = createLeafMediaSource(sample);
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo; Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo;
if (subtitleInfo != null) { if (subtitleInfo != null) {
Format subtitleFormat = Format subtitleFormat =
Format.createTextSampleFormat( Format.createTextSampleFormat(
...@@ -443,33 +441,30 @@ public class PlayerActivity extends AppCompatActivity ...@@ -443,33 +441,30 @@ public class PlayerActivity extends AppCompatActivity
MediaSource subtitleMediaSource = MediaSource subtitleMediaSource =
new SingleSampleMediaSource.Factory(dataSourceFactory) new SingleSampleMediaSource.Factory(dataSourceFactory)
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource); mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource);
} }
mediaSources.add(mediaSource);
} }
MediaSource mediaSource = if (seenAdsTagUri && mediaSources.size() == 1) {
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(mediaSource, adTagUri); MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
if (adsMediaSource != null) { if (adsMediaSource != null) {
mediaSource = adsMediaSource; mediaSources.set(0, 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 mediaSource; return mediaSources;
} }
private MediaSource createLeafMediaSource(UriSample parameters) { private MediaSource createLeafMediaSource(UriSample parameters) {
...@@ -557,7 +552,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -557,7 +552,7 @@ public class PlayerActivity extends AppCompatActivity
debugViewHelper = null; debugViewHelper = null;
player.release(); player.release();
player = null; player = null;
mediaSource = null; mediaSources = null;
trackSelector = null; trackSelector = null;
} }
if (adsLoader != null) { if (adsLoader != null) {
......
...@@ -110,7 +110,6 @@ public final class CastPlayer extends BasePlayer { ...@@ -110,7 +110,6 @@ 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.
...@@ -173,7 +172,6 @@ public final class CastPlayer extends BasePlayer { ...@@ -173,7 +172,6 @@ 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);
} }
...@@ -641,15 +639,13 @@ public final class CastPlayer extends BasePlayer { ...@@ -641,15 +639,13 @@ public final class CastPlayer extends BasePlayer {
private void updateTimelineAndNotifyIfChanged() { private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) { if (updateTimeline()) {
@Player.TimelineChangeReason // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
int reason = // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
notificationsBatch.add( notificationsBatch.add(
new ListenerNotificationTask( new ListenerNotificationTask(
listener -> listener.onTimelineChanged(currentTimeline, reason))); listener ->
listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
} }
} }
......
...@@ -288,7 +288,7 @@ public class ImaAdsLoaderTest { ...@@ -288,7 +288,7 @@ public class ImaAdsLoaderTest {
this.adPlaybackState = adPlaybackState; this.adPlaybackState = adPlaybackState;
fakeExoPlayer.updateTimeline( fakeExoPlayer.updateTimeline(
new SinglePeriodAdTimeline(contentTimeline, adPlaybackState), new SinglePeriodAdTimeline(contentTimeline, adPlaybackState),
Player.TIMELINE_CHANGE_REASON_DYNAMIC); Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
} }
@Override @Override
......
...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.source.LoopingMediaSource; ...@@ -28,6 +28,7 @@ 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;
...@@ -39,6 +40,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -39,6 +40,7 @@ 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
...@@ -139,7 +141,7 @@ public interface ExoPlayer extends Player { ...@@ -139,7 +141,7 @@ public interface ExoPlayer extends Player {
private LoadControl loadControl; private LoadControl loadControl;
private BandwidthMeter bandwidthMeter; private BandwidthMeter bandwidthMeter;
private Looper looper; private Looper looper;
private AnalyticsCollector analyticsCollector; @Nullable private AnalyticsCollector analyticsCollector;
private boolean useLazyPreparation; private boolean useLazyPreparation;
private boolean buildCalled; private boolean buildCalled;
...@@ -172,7 +174,7 @@ public interface ExoPlayer extends Player { ...@@ -172,7 +174,7 @@ public interface ExoPlayer extends Player {
new DefaultLoadControl(), new DefaultLoadControl(),
DefaultBandwidthMeter.getSingletonInstance(context), DefaultBandwidthMeter.getSingletonInstance(context),
Util.getLooper(), Util.getLooper(),
new AnalyticsCollector(Clock.DEFAULT), /* analyticsCollector= */ null,
/* useLazyPreparation= */ true, /* useLazyPreparation= */ true,
Clock.DEFAULT); Clock.DEFAULT);
} }
...@@ -199,7 +201,7 @@ public interface ExoPlayer extends Player { ...@@ -199,7 +201,7 @@ public interface ExoPlayer extends Player {
LoadControl loadControl, LoadControl loadControl,
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
Looper looper, Looper looper,
AnalyticsCollector analyticsCollector, @Nullable AnalyticsCollector analyticsCollector,
boolean useLazyPreparation, boolean useLazyPreparation,
Clock clock) { Clock clock) {
Assertions.checkArgument(renderers.length > 0); Assertions.checkArgument(renderers.length > 0);
...@@ -335,7 +337,15 @@ public interface ExoPlayer extends Player { ...@@ -335,7 +337,15 @@ public interface ExoPlayer extends Player {
Assertions.checkState(!buildCalled); Assertions.checkState(!buildCalled);
buildCalled = true; buildCalled = true;
ExoPlayerImpl player = ExoPlayerImpl player =
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); new ExoPlayerImpl(
renderers,
trackSelector,
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
if (releaseTimeoutMs > 0) { if (releaseTimeoutMs > 0) {
player.experimental_setReleaseTimeoutMs(releaseTimeoutMs); player.experimental_setReleaseTimeoutMs(releaseTimeoutMs);
...@@ -348,52 +358,64 @@ public interface ExoPlayer extends Player { ...@@ -348,52 +358,64 @@ public interface ExoPlayer extends Player {
/** 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. */
* Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback @Deprecated
* has not failed or been stopped.
*/
void retry(); void retry();
/** Prepares the player. */ /** Prepares the player. */
void prepare(); void prepare();
/** /** @deprecated Use {@link #setMediaSource(MediaSource)} and {@link #prepare()} instead. */
* @deprecated Use {@code setMediaSource(mediaSource, C.TIME_UNSET)} and {@link #prepare()}
* instead.
*/
@Deprecated @Deprecated
void prepare(MediaSource mediaSource); void prepare(MediaSource mediaSource);
/** @deprecated Use {@link #setMediaSource(MediaSource, long)} and {@link #prepare()} instead. */ /**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link #prepare()} instead.
*/
@Deprecated @Deprecated
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
/** /**
* Sets the specified {@link MediaSource}. * Clears the playlist, adds the specified {@link MediaSource MediaSources} and resets the
* position to the default position.
* *
* <p>Note: This is an intermediate implementation towards a larger change. Until then {@link * @param mediaSources The new {@link MediaSource MediaSources}.
* #prepare()} has to be called immediately after calling this method. */
void setMediaSources(List<MediaSource> mediaSources);
/**
* Clears the playlist and adds the specified {@link MediaSource MediaSources}.
*
* @param mediaSources The new {@link MediaSource MediaSources}.
* @param resetPosition Whether the playback position should be reset to the default position in
* the first {@link Timeline.Window}. If false, playback will start from the position defined
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
*/
void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition);
/**
* Clears the playlist and adds the specified {@link MediaSource MediaSources}.
*
* @param mediaSources 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 setMediaSources(List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs);
/**
* Clears the playlist, adds the specified {@link MediaSource} and resets the position to the
* default position.
* *
* @param mediaSource The new {@link MediaSource}. * @param mediaSource The new {@link MediaSource}.
*/ */
void setMediaSource(MediaSource mediaSource); void setMediaSource(MediaSource mediaSource);
/** /**
* Sets the specified {@link MediaSource}. * Clears the playlist and adds the specified {@link MediaSource}.
*
* <p>Note: This is an intermediate implementation towards a larger change. Until then {@link
* #prepare()} has to be called immediately after calling this method.
*
* <p>This intermediate implementation calls {@code stop(true)} before seeking to avoid seeking in
* a media item that has been set previously. It is equivalent with calling
*
* <pre><code>
* if (!getCurrentTimeline().isEmpty()) {
* player.stop(true);
* }
* player.seekTo(0, startPositionMs);
* player.setMediaSource(mediaSource);
* </code></pre>
* *
* @param mediaSource The new {@link MediaSource}. * @param mediaSource The new {@link MediaSource}.
* @param startPositionMs The position in milliseconds to start playback from. * @param startPositionMs The position in milliseconds to start playback from.
...@@ -401,6 +423,93 @@ public interface ExoPlayer extends Player { ...@@ -401,6 +423,93 @@ public interface ExoPlayer extends Player {
void setMediaSource(MediaSource mediaSource, long startPositionMs); void setMediaSource(MediaSource mediaSource, long startPositionMs);
/** /**
* Clears the playlist and adds the specified {@link MediaSource}.
*
* @param mediaSource The new {@link MediaSource}.
* @param resetPosition Whether the playback position should be reset to the default position. If
* false, playback will start from the position defined by {@link #getCurrentWindowIndex()}
* and {@link #getCurrentPosition()}.
*/
void setMediaSource(MediaSource mediaSource, boolean resetPosition);
/**
* Adds a media source to the end of the playlist.
*
* @param mediaSource The {@link MediaSource} to add.
*/
void addMediaSource(MediaSource mediaSource);
/**
* Adds a media source at the given index of the playlist.
*
* @param index The index at which to add the source.
* @param mediaSource The {@link MediaSource} to add.
*/
void addMediaSource(int index, MediaSource mediaSource);
/**
* Adds a list of media sources to the end of the playlist.
*
* @param mediaSources The {@link MediaSource MediaSources} to add.
*/
void addMediaSources(List<MediaSource> mediaSources);
/**
* Adds a list of media sources at the given index of the playlist.
*
* @param index The index at which to add the media sources.
* @param mediaSources The {@link MediaSource MediaSources} to add.
*/
void addMediaSources(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
* will be delivered immediately without blocking on the playback thread. The default {@link * will be delivered immediately without blocking on the playback thread. The default {@link
* PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a
......
...@@ -297,6 +297,7 @@ public final class ExoPlayerFactory { ...@@ -297,6 +297,7 @@ public final class ExoPlayerFactory {
drmSessionManager, drmSessionManager,
bandwidthMeter, bandwidthMeter,
analyticsCollector, analyticsCollector,
/* useLazyPreparation= */ true,
Clock.DEFAULT, Clock.DEFAULT,
looper); looper);
} }
...@@ -345,6 +346,13 @@ public final class ExoPlayerFactory { ...@@ -345,6 +346,13 @@ public final class ExoPlayerFactory {
BandwidthMeter bandwidthMeter, BandwidthMeter bandwidthMeter,
Looper looper) { Looper looper) {
return new ExoPlayerImpl( return new ExoPlayerImpl(
renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); renderers,
trackSelector,
loadControl,
bandwidthMeter,
/* analyticsCollector= */ null,
/* useLazyPreparation= */ true,
Clock.DEFAULT,
looper);
} }
} }
...@@ -22,8 +22,10 @@ import android.os.Message; ...@@ -22,8 +22,10 @@ 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;
...@@ -35,6 +37,9 @@ import com.google.android.exoplayer2.util.Clock; ...@@ -35,6 +37,9 @@ 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;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
...@@ -62,19 +67,20 @@ import java.util.concurrent.TimeoutException; ...@@ -62,19 +67,20 @@ import java.util.concurrent.TimeoutException;
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;
@Nullable 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;
...@@ -91,6 +97,10 @@ import java.util.concurrent.TimeoutException; ...@@ -91,6 +97,10 @@ import java.util.concurrent.TimeoutException;
* @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.
...@@ -101,6 +111,8 @@ import java.util.concurrent.TimeoutException; ...@@ -101,6 +111,8 @@ import java.util.concurrent.TimeoutException;
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)) + " ["
...@@ -108,10 +120,13 @@ import java.util.concurrent.TimeoutException; ...@@ -108,10 +120,13 @@ import java.util.concurrent.TimeoutException;
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.playWhenReady = false; this.useLazyPreparation = useLazyPreparation;
this.repeatMode = Player.REPEAT_MODE_OFF; playWhenReady = false;
this.shuffleModeEnabled = false; repeatMode = Player.REPEAT_MODE_OFF;
this.listeners = new CopyOnWriteArrayList<>(); shuffleModeEnabled = false;
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],
...@@ -121,6 +136,7 @@ import java.util.concurrent.TimeoutException; ...@@ -121,6 +136,7 @@ import java.util.concurrent.TimeoutException;
playbackParameters = PlaybackParameters.DEFAULT; playbackParameters = PlaybackParameters.DEFAULT;
seekParameters = SeekParameters.DEFAULT; seekParameters = SeekParameters.DEFAULT;
playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
maskingWindowIndex = C.INDEX_UNSET;
eventHandler = eventHandler =
new Handler(looper) { new Handler(looper) {
@Override @Override
...@@ -130,6 +146,9 @@ import java.util.concurrent.TimeoutException; ...@@ -130,6 +146,9 @@ import java.util.concurrent.TimeoutException;
}; };
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,
...@@ -140,6 +159,7 @@ import java.util.concurrent.TimeoutException; ...@@ -140,6 +159,7 @@ import java.util.concurrent.TimeoutException;
playWhenReady, playWhenReady,
repeatMode, repeatMode,
shuffleModeEnabled, shuffleModeEnabled,
analyticsCollector,
eventHandler, eventHandler,
clock); clock);
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
...@@ -226,45 +246,182 @@ import java.util.concurrent.TimeoutException; ...@@ -226,45 +246,182 @@ import java.util.concurrent.TimeoutException;
return playbackInfo.playbackError; return playbackInfo.playbackError;
} }
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
@Override @Override
public void retry() { public void retry() {
if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { prepare();
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
}
} }
@Override @Override
public void prepare() {
if (playbackInfo.playbackState != Player.STATE_IDLE) {
return;
}
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
/* clearPlaylist= */ false,
/* resetError= */ true,
/* playbackState= */ this.playbackInfo.timeline.isEmpty()
? Player.STATE_ENDED
: Player.STATE_BUFFERING);
// Trigger internal prepare first before updating the playback info and notifying external
// listeners to ensure that new operations issued in the listener notifications reach the
// player after this prepare. The internal player can't change the playback info immediately
// because it uses a callback.
pendingOperationAcks++;
internalPlayer.prepare();
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
/* seekProcessed= */ false);
}
/**
* @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead.
*/
@Deprecated @Deprecated
@Override
public void prepare(MediaSource mediaSource) { public void prepare(MediaSource mediaSource) {
setMediaSource(mediaSource); setMediaSource(mediaSource);
prepareInternal(/* resetPosition= */ true, /* resetState= */ true); prepare();
} }
@Override /**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()}
* instead.
*/
@Deprecated @Deprecated
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
setMediaSource(mediaSource); setMediaSource(mediaSource, resetPosition);
prepareInternal(resetPosition, resetState); prepare();
} }
@Override @Override
public void prepare() { public void setMediaSource(MediaSource mediaSource) {
Assertions.checkNotNull(mediaSource); setMediaSources(Collections.singletonList(mediaSource));
prepareInternal(/* resetPosition= */ false, /* resetState= */ true);
} }
@Override @Override
public void setMediaSource(MediaSource mediaSource, long startPositionMs) { public void setMediaSource(MediaSource mediaSource, long startPositionMs) {
if (!getCurrentTimeline().isEmpty()) { setMediaSources(
stop(/* reset= */ true); Collections.singletonList(mediaSource), /* startWindowIndex= */ 0, startPositionMs);
} }
seekTo(/* windowIndex= */ 0, startPositionMs);
setMediaSource(mediaSource); @Override
public void setMediaSource(MediaSource mediaSource, boolean resetPosition) {
setMediaSources(Collections.singletonList(mediaSource), resetPosition);
} }
@Override @Override
public void setMediaSource(MediaSource mediaSource) { public void setMediaSources(List<MediaSource> mediaSources) {
this.mediaSource = mediaSource; setMediaSources(mediaSources, /* resetPosition= */ true);
}
@Override
public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
setMediaItemsInternal(
mediaSources,
/* startWindowIndex= */ C.INDEX_UNSET,
/* startPositionMs= */ C.TIME_UNSET,
/* resetToDefaultPosition= */ resetPosition);
}
@Override
public void setMediaSources(
List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs) {
setMediaItemsInternal(
mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false);
}
@Override
public void addMediaSource(MediaSource mediaSource) {
addMediaSources(Collections.singletonList(mediaSource));
}
@Override
public void addMediaSource(int index, MediaSource mediaSource) {
addMediaSources(index, Collections.singletonList(mediaSource));
}
@Override
public void addMediaSources(List<MediaSource> mediaSources) {
addMediaSources(/* index= */ mediaSourceHolders.size(), mediaSources);
}
@Override
public void addMediaSources(int index, List<MediaSource> mediaSources) {
Assertions.checkArgument(index >= 0);
int currentWindowIndex = getCurrentWindowIndex();
long currentPositionMs = getCurrentPosition();
Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++;
List<Playlist.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources);
Timeline timeline =
maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline);
internalPlayer.addMediaSources(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);
int currentWindowIndex = getCurrentWindowIndex();
long currentPositionMs = getCurrentPosition();
Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++;
newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex));
Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
Timeline timeline =
maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline);
internalPlayer.moveMediaSources(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
...@@ -365,18 +522,7 @@ import java.util.concurrent.TimeoutException; ...@@ -365,18 +522,7 @@ import java.util.concurrent.TimeoutException;
.sendToTarget(); .sendToTarget();
return; return;
} }
maskingWindowIndex = windowIndex; maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs);
if (timeline.isEmpty()) {
maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs;
maskingPeriodIndex = 0;
} else {
long windowPositionUs = positionMs == C.TIME_UNSET
? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs);
Pair<Object, Long> periodUidAndPosition =
timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
maskingWindowPositionMs = C.usToMs(windowPositionUs);
maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first);
}
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
} }
...@@ -427,13 +573,9 @@ import java.util.concurrent.TimeoutException; ...@@ -427,13 +573,9 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void stop(boolean reset) { public void stop(boolean reset) {
if (reset) {
mediaSource = null;
}
PlaybackInfo playbackInfo = PlaybackInfo playbackInfo =
getResetPlaybackInfo( getResetPlaybackInfo(
/* resetPosition= */ reset, /* clearPlaylist= */ 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
...@@ -446,7 +588,7 @@ import java.util.concurrent.TimeoutException; ...@@ -446,7 +588,7 @@ import java.util.concurrent.TimeoutException;
playbackInfo, playbackInfo,
/* positionDiscontinuity= */ false, /* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL, /* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* seekProcessed= */ false); /* seekProcessed= */ false);
} }
...@@ -455,7 +597,6 @@ import java.util.concurrent.TimeoutException; ...@@ -455,7 +597,6 @@ import java.util.concurrent.TimeoutException;
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;
if (!internalPlayer.release()) { if (!internalPlayer.release()) {
notifyListeners( notifyListeners(
listener -> listener ->
...@@ -466,8 +607,7 @@ import java.util.concurrent.TimeoutException; ...@@ -466,8 +607,7 @@ import java.util.concurrent.TimeoutException;
eventHandler.removeCallbacksAndMessages(null); eventHandler.removeCallbacksAndMessages(null);
playbackInfo = playbackInfo =
getResetPlaybackInfo( getResetPlaybackInfo(
/* resetPosition= */ false, /* clearPlaylist= */ false,
/* resetState= */ false,
/* resetError= */ false, /* resetError= */ false,
/* playbackState= */ Player.STATE_IDLE); /* playbackState= */ Player.STATE_IDLE);
} }
...@@ -493,12 +633,8 @@ import java.util.concurrent.TimeoutException; ...@@ -493,12 +633,8 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public int getCurrentWindowIndex() { public int getCurrentWindowIndex() {
if (shouldMaskPosition()) { int currentWindowIndex = getCurrentWindowIndexInternal();
return maskingWindowIndex; return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex;
} else {
return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
.windowIndex;
}
} }
@Override @Override
...@@ -615,10 +751,11 @@ import java.util.concurrent.TimeoutException; ...@@ -615,10 +751,11 @@ import java.util.concurrent.TimeoutException;
// 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) msg.obj, /* playbackInfo= */ (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);
...@@ -631,27 +768,13 @@ import java.util.concurrent.TimeoutException; ...@@ -631,27 +768,13 @@ import java.util.concurrent.TimeoutException;
} }
} }
/* package */ void prepareInternal(boolean resetPosition, boolean resetState) { private int getCurrentWindowIndexInternal() {
Assertions.checkNotNull(mediaSource); if (shouldMaskPosition()) {
PlaybackInfo playbackInfo = return maskingWindowIndex;
getResetPlaybackInfo( } else {
resetPosition, return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
resetState, .windowIndex;
/* resetError= */ true, }
/* playbackState= */ Player.STATE_BUFFERING);
// Trigger internal prepare first before updating the playback info and notifying external
// listeners to ensure that new operations issued in the listener notifications reach the
// player after this prepare. The internal player can't change the playback info immediately
// because it uses a callback.
hasPendingPrepare = true;
pendingOperationAcks++;
internalPlayer.prepare(mediaSource, resetPosition, resetState);
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false);
} }
private void handlePlaybackParameters( private void handlePlaybackParameters(
...@@ -685,59 +808,51 @@ import java.util.concurrent.TimeoutException; ...@@ -685,59 +808,51 @@ import java.util.concurrent.TimeoutException;
} }
if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
// Update the masking variables, which are used when the timeline becomes empty. // Update the masking variables, which are used when the timeline becomes empty.
maskingPeriodIndex = 0; resetMaskingPosition();
maskingWindowIndex = 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,
timelineChangeReason, TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
seekProcessed); seekProcessed);
} }
} }
private PlaybackInfo getResetPlaybackInfo( private PlaybackInfo getResetPlaybackInfo(
boolean resetPosition, boolean clearPlaylist, boolean resetError, @Player.State int playbackState) {
boolean resetState, if (clearPlaylist) {
boolean resetError, // Reset list of media source holders which are used for creating the masking timeline.
@Player.State int playbackState) { removeMediaSourceHolders(
if (resetPosition) { /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
maskingWindowIndex = 0; resetMaskingPosition();
maskingPeriodIndex = 0;
maskingWindowPositionMs = 0;
} else { } else {
maskingWindowIndex = getCurrentWindowIndex(); maskingWindowIndex = getCurrentWindowIndex();
maskingPeriodIndex = getCurrentPeriodIndex(); maskingPeriodIndex = getCurrentPeriodIndex();
maskingWindowPositionMs = getCurrentPosition(); maskingWindowPositionMs = getCurrentPosition();
} }
// Also reset period-based PlaybackInfo positions if resetting the state. Timeline timeline = playbackInfo.timeline;
resetPosition = resetPosition || resetState; MediaPeriodId mediaPeriodId = playbackInfo.periodId;
MediaPeriodId mediaPeriodId = long contentPositionUs = playbackInfo.contentPositionUs;
resetPosition long startPositionUs = playbackInfo.positionUs;
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) if (clearPlaylist) {
: playbackInfo.periodId; timeline = Timeline.EMPTY;
long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; mediaPeriodId = playbackInfo.getDummyPeriodForEmptyTimeline();
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; contentPositionUs = C.TIME_UNSET;
startPositionUs = C.TIME_UNSET;
}
return new PlaybackInfo( return new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline, timeline,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
contentPositionUs, contentPositionUs,
playbackState, playbackState,
resetError ? null : playbackInfo.playbackError, resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false, /* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
...@@ -747,8 +862,8 @@ import java.util.concurrent.TimeoutException; ...@@ -747,8 +862,8 @@ import java.util.concurrent.TimeoutException;
private void updatePlaybackInfo( private void updatePlaybackInfo(
PlaybackInfo playbackInfo, PlaybackInfo playbackInfo,
boolean positionDiscontinuity, boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason, @DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason, @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.
...@@ -769,6 +884,218 @@ import java.util.concurrent.TimeoutException; ...@@ -769,6 +884,218 @@ import java.util.concurrent.TimeoutException;
/* isPlayingChanged= */ previousIsPlaying != isPlaying)); /* isPlayingChanged= */ previousIsPlaying != isPlaying));
} }
private void setMediaItemsInternal(
List<MediaSource> mediaItems,
int startWindowIndex,
long startPositionMs,
boolean resetToDefaultPosition) {
int currentWindowIndex = getCurrentWindowIndexInternal();
long currentPositionMs = getCurrentPosition();
boolean currentPlayWhenReady = getPlayWhenReady();
pendingOperationAcks++;
if (!mediaSourceHolders.isEmpty()) {
removeMediaSourceHolders(
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
}
List<Playlist.MediaSourceHolder> holders = addMediaSourceHolders(/* index= */ 0, mediaItems);
Timeline timeline = maskTimeline();
if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) {
throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs);
}
// Evaluate the actual start position.
if (resetToDefaultPosition) {
startWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
startPositionMs = C.TIME_UNSET;
} else if (startWindowIndex == C.INDEX_UNSET) {
startWindowIndex = currentWindowIndex;
startPositionMs = currentPositionMs;
}
maskWindowIndexAndPositionForSeek(
timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs);
// mask the playback state
int maskingPlaybackState = playbackInfo.playbackState;
if (startWindowIndex != C.INDEX_UNSET) {
// Position reset to startWindowIndex (results in pending initial seek).
if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) {
// Setting an empty timeline or invalid seek transitions to ended.
maskingPlaybackState = STATE_ENDED;
} else {
maskingPlaybackState = STATE_BUFFERING;
}
}
boolean playbackStateChanged =
playbackInfo.playbackState != STATE_IDLE
&& playbackInfo.playbackState != maskingPlaybackState;
int finalMaskingPlaybackState = maskingPlaybackState;
if (playbackStateChanged) {
playbackInfo = playbackInfo.copyWithPlaybackState(finalMaskingPlaybackState);
}
internalPlayer.setMediaSources(
holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder);
notifyListeners(
listener -> {
listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
if (playbackStateChanged) {
listener.onPlayerStateChanged(currentPlayWhenReady, finalMaskingPlaybackState);
}
});
}
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());
int currentWindowIndex = getCurrentWindowIndex();
long currentPositionMs = getCurrentPosition();
boolean currentPlayWhenReady = getPlayWhenReady();
Timeline oldTimeline = getCurrentTimeline();
int currentMediaSourceCount = mediaSourceHolders.size();
pendingOperationAcks++;
List<Playlist.MediaSourceHolder> removedHolders =
removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex);
Timeline timeline =
maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline);
// Player transitions to STATE_ENDED if the current index is part of the removed tail.
final boolean transitionsToEnded =
playbackInfo.playbackState != STATE_IDLE
&& playbackInfo.playbackState != STATE_ENDED
&& fromIndex < toIndex
&& toIndex == currentMediaSourceCount
&& currentWindowIndex >= timeline.getWindowCount();
if (transitionsToEnded) {
playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED);
}
internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder);
notifyListeners(
listener -> {
listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
if (transitionsToEnded) {
listener.onPlayerStateChanged(currentPlayWhenReady, STATE_ENDED);
}
});
return removedHolders;
}
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 Timeline maskTimelineAndWindowIndex(
int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) {
Timeline maskingTimeline = maskTimeline();
if (oldTimeline.isEmpty()) {
// The index is the default index or was set by a seek in the empty old timeline.
maskingWindowIndex = currentWindowIndex;
if (!maskingTimeline.isEmpty() && currentWindowIndex >= maskingTimeline.getWindowCount()) {
// The seek is not valid in the new timeline.
maskWithDefaultPosition(maskingTimeline);
}
return maskingTimeline;
}
@Nullable
Pair<Object, Long> periodPosition =
oldTimeline.getPeriodPosition(
window,
period,
currentWindowIndex,
C.msToUs(currentPositionMs),
/* defaultPositionProjectionUs= */ 0);
Object periodUid = Util.castNonNull(periodPosition).first;
if (maskingTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) {
// Get the window index of the current period that exists in the new timeline also.
maskingWindowIndex = maskingTimeline.getPeriodByUid(periodUid, period).windowIndex;
} else {
// Period uid not found in new timeline. Try to get subsequent period.
@Nullable
Object nextPeriodUid =
ExoPlayerImplInternal.resolveSubsequentPeriod(
window,
period,
repeatMode,
shuffleModeEnabled,
periodUid,
oldTimeline,
maskingTimeline);
if (nextPeriodUid != null) {
// Set masking to the default position of the window of the subsequent period.
maskingWindowIndex = maskingTimeline.getPeriodByUid(nextPeriodUid, period).windowIndex;
maskingPeriodIndex = maskingTimeline.getWindow(maskingWindowIndex, window).firstPeriodIndex;
maskingWindowPositionMs = window.getDefaultPositionMs();
} else {
// Reset if no subsequent period is found.
maskWithDefaultPosition(maskingTimeline);
}
}
return maskingTimeline;
}
private void maskWindowIndexAndPositionForSeek(
Timeline timeline, int windowIndex, long positionMs) {
maskingWindowIndex = windowIndex;
if (timeline.isEmpty()) {
maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs;
maskingPeriodIndex = 0;
} else if (windowIndex >= timeline.getWindowCount()) {
// An initial seek now proves to be invalid in the actual timeline.
maskWithDefaultPosition(timeline);
} else {
long windowPositionUs =
positionMs == C.TIME_UNSET
? timeline.getWindow(windowIndex, window).getDefaultPositionUs()
: C.msToUs(positionMs);
Pair<Object, Long> periodUidAndPosition =
timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
maskingWindowPositionMs = C.usToMs(windowPositionUs);
maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first);
}
}
private void maskWithDefaultPosition(Timeline timeline) {
if (timeline.isEmpty()) {
resetMaskingPosition();
return;
}
maskingWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
timeline.getWindow(maskingWindowIndex, window);
maskingWindowPositionMs = window.getDefaultPositionMs();
maskingPeriodIndex = window.firstPeriodIndex;
}
private void resetMaskingPosition() {
maskingWindowIndex = C.INDEX_UNSET;
maskingWindowPositionMs = 0;
maskingPeriodIndex = 0;
}
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));
...@@ -804,7 +1131,7 @@ import java.util.concurrent.TimeoutException; ...@@ -804,7 +1131,7 @@ import java.util.concurrent.TimeoutException;
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 @Player.TimelineChangeReason int timelineChangeReason; private final 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;
...@@ -838,15 +1165,15 @@ import java.util.concurrent.TimeoutException; ...@@ -838,15 +1165,15 @@ import java.util.concurrent.TimeoutException;
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 = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline);
trackSelectorResultChanged = trackSelectorResultChanged =
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
} }
@Override @Override
public void run() { public void run() {
if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { if (timelineChanged) {
invokeAll( invokeAll(
listenerSnapshot, listenerSnapshot,
listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
......
...@@ -25,11 +25,11 @@ import android.util.Pair; ...@@ -25,11 +25,11 @@ import android.util.Pair;
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;
...@@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -44,6 +44,7 @@ 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}. */
...@@ -51,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -51,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
implements Handler.Callback, implements Handler.Callback,
MediaPeriod.Callback, MediaPeriod.Callback,
TrackSelector.InvalidationListener, TrackSelector.InvalidationListener,
MediaSourceCaller, Playlist.PlaylistInfoRefreshListener,
PlaybackParameterListener, PlaybackParameterListener,
PlayerMessage.Sender { PlayerMessage.Sender {
...@@ -70,16 +71,21 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -70,16 +71,21 @@ 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_REFRESH_SOURCE_INFO = 8; private static final int MSG_PERIOD_PREPARED = 8;
private static final int MSG_PERIOD_PREPARED = 9; private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; private static final int MSG_SET_REPEAT_MODE = 11;
private static final int MSG_SET_REPEAT_MODE = 12; private static final int MSG_SET_SHUFFLE_ENABLED = 12;
private static final int MSG_SET_SHUFFLE_ENABLED = 13; private static final int MSG_SET_FOREGROUND_MODE = 13;
private static final int MSG_SET_FOREGROUND_MODE = 14; private static final int MSG_SEND_MESSAGE = 14;
private static final int MSG_SEND_MESSAGE = 15; private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; private static final int MSG_SET_MEDIA_SOURCES = 17;
private static final int MSG_ADD_MEDIA_SOURCES = 18;
private static final int MSG_MOVE_MEDIA_SOURCES = 19;
private static final int MSG_REMOVE_MEDIA_SOURCES = 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;
...@@ -102,12 +108,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -102,12 +108,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;
...@@ -117,8 +123,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -117,8 +123,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private boolean shuffleModeEnabled; private boolean shuffleModeEnabled;
private boolean foregroundMode; private boolean foregroundMode;
private int pendingPrepareCount; @Nullable private SeekPosition pendingInitialSeekPosition;
private SeekPosition pendingInitialSeekPosition;
private long rendererPositionUs; private long rendererPositionUs;
private int nextPendingMessageIndex; private int nextPendingMessageIndex;
private boolean deliverPendingMessageAtStartPositionRequired; private boolean deliverPendingMessageAtStartPositionRequired;
...@@ -134,6 +139,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -134,6 +139,7 @@ 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;
...@@ -174,16 +180,18 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -174,16 +180,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
internalPlaybackThread.start(); internalPlaybackThread.start();
handler = clock.createHandler(internalPlaybackThread.getLooper(), this); handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
deliverPendingMessageAtStartPositionRequired = true; deliverPendingMessageAtStartPositionRequired = true;
playlist = new Playlist(this);
if (analyticsCollector != null) {
playlist.setAnalyticsCollector(eventHandler, analyticsCollector);
}
} }
public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) { public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) {
this.releaseTimeoutMs = releaseTimeoutMs; this.releaseTimeoutMs = releaseTimeoutMs;
} }
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { public void prepare() {
handler handler.obtainMessage(MSG_PREPARE).sendToTarget();
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
.sendToTarget();
} }
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
...@@ -216,6 +224,50 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -216,6 +224,50 @@ 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 setMediaSources(
List<Playlist.MediaSourceHolder> mediaSources,
int windowIndex,
long positionUs,
ShuffleOrder shuffleOrder) {
handler
.obtainMessage(
MSG_SET_MEDIA_SOURCES,
new PlaylistUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs))
.sendToTarget();
}
public void addMediaSources(
int index, List<Playlist.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder) {
handler
.obtainMessage(
MSG_ADD_MEDIA_SOURCES,
index,
/* ignored */ 0,
new PlaylistUpdateMessage(
mediaSources,
shuffleOrder,
/* windowIndex= */ C.INDEX_UNSET,
/* positionUs= */ C.TIME_UNSET))
.sendToTarget();
}
public void removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
handler
.obtainMessage(MSG_REMOVE_MEDIA_SOURCES, fromIndex, toIndex, shuffleOrder)
.sendToTarget();
}
public void moveMediaSources(
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
MoveMediaItemsMessage moveMediaItemsMessage =
new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder);
handler.obtainMessage(MSG_MOVE_MEDIA_SOURCES, 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 || !internalPlaybackThread.isAlive()) { if (released || !internalPlaybackThread.isAlive()) {
...@@ -275,13 +327,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -275,13 +327,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return internalPlaybackThread.getLooper(); return internalPlaybackThread.getLooper();
} }
// MediaSource.MediaSourceCaller implementation. // Playlist.PlaylistInfoRefreshListener implementation.
@Override @Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { public void onPlaylistUpdateRequested() {
handler handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED);
.obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
.sendToTarget();
} }
// MediaPeriod.Callback implementation. // MediaPeriod.Callback implementation.
...@@ -313,14 +363,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -313,14 +363,12 @@ 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);
...@@ -356,9 +404,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -356,9 +404,6 @@ 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;
...@@ -375,6 +420,24 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -375,6 +420,24 @@ 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_SOURCES:
setMediaItemsInternal((PlaylistUpdateMessage) msg.obj);
break;
case MSG_ADD_MEDIA_SOURCES:
addMediaItemsInternal((PlaylistUpdateMessage) msg.obj, msg.arg1);
break;
case MSG_MOVE_MEDIA_SOURCES:
moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj);
break;
case MSG_REMOVE_MEDIA_SOURCES:
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.
...@@ -509,21 +572,77 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -509,21 +572,77 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { private void prepareInternal() {
pendingPrepareCount++; playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
resetInternal( resetInternal(
/* resetRenderers= */ false, /* resetRenderers= */ false,
/* releaseMediaSource= */ true, /* resetPosition= */ false,
resetPosition, /* releasePlaylist= */ false,
resetState, /* clearPlaylist= */ false,
/* resetError= */ true); /* resetError= */ true);
loadControl.onPrepared(); loadControl.onPrepared();
this.mediaSource = mediaSource; setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
setState(Player.STATE_BUFFERING); playlist.prepare(bandwidthMeter.getTransferListener());
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;
...@@ -563,7 +682,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -563,7 +682,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
// position of the playing period to make sure none of the removed period is played. // position of the playing period to make sure none of the removed period is played.
MediaPeriodId periodId = queue.getPlayingPeriod().info.id; MediaPeriodId periodId = queue.getPlayingPeriod().info.id;
long newPositionUs = long newPositionUs =
seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); seekToPeriodPosition(
periodId,
playbackInfo.positionUs,
/* forceDisableRenderers= */ true,
/* forceBufferingState= */ false);
if (newPositionUs != playbackInfo.positionUs) { if (newPositionUs != playbackInfo.positionUs) {
playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);
if (sendDiscontinuity) { if (sendDiscontinuity) {
...@@ -741,14 +864,15 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -741,14 +864,15 @@ 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) {
// The seek position was valid for the timeline that it was performed into, but the // The seek position was valid for the timeline that it was performed into, but the
// timeline has changed or is not ready and a suitable seek position could not be resolved. // timeline has changed or is not ready and a suitable seek position could not be resolved.
periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); periodId = getDummyFirstMediaPeriodForAds();
periodPositionUs = C.TIME_UNSET;
contentPositionUs = C.TIME_UNSET; contentPositionUs = C.TIME_UNSET;
periodPositionUs = C.TIME_UNSET;
seekPositionAdjusted = true; seekPositionAdjusted = true;
} else { } else {
// Update the resolved seek position to take ads into account. // Update the resolved seek position to take ads into account.
...@@ -765,7 +889,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -765,7 +889,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
try { try {
if (mediaSource == null || pendingPrepareCount > 0) { if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) {
// 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) {
...@@ -773,9 +897,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -773,9 +897,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,
/* resetState= */ false, /* releasePlaylist= */ 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.
...@@ -795,7 +919,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -795,7 +919,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return; return;
} }
} }
newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); newPeriodPositionUs =
seekToPeriodPosition(
periodId,
newPeriodPositionUs,
/* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED);
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
periodPositionUs = newPeriodPositionUs; periodPositionUs = newPeriodPositionUs;
} }
...@@ -807,19 +935,26 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -807,19 +935,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) private long seekToPeriodPosition(
MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState)
throws ExoPlaybackException { throws ExoPlaybackException {
// Force disable renderers if they are reading from a period other than the one being played. // Force disable renderers if they are reading from a period other than the one being played.
return seekToPeriodPosition( return seekToPeriodPosition(
periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod()); periodId,
periodPositionUs,
queue.getPlayingPeriod() != queue.getReadingPeriod(),
forceBufferingState);
} }
private long seekToPeriodPosition( private long seekToPeriodPosition(
MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers) MediaPeriodId periodId,
long periodPositionUs,
boolean forceDisableRenderers,
boolean forceBufferingState)
throws ExoPlaybackException { throws ExoPlaybackException {
stopRenderers(); stopRenderers();
rebuffering = false; rebuffering = false;
if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) { if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) {
setState(Player.STATE_BUFFERING); setState(Player.STATE_BUFFERING);
} }
...@@ -920,13 +1055,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -920,13 +1055,11 @@ 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,
/* resetState= */ resetPositionAndState, /* releasePlaylist= */ true,
/* clearPlaylist= */ resetPositionAndState,
/* resetError= */ resetPositionAndState); /* resetError= */ resetPositionAndState);
playbackInfoUpdate.incrementPendingOperationAcks( playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0);
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
pendingPrepareCount = 0;
loadControl.onStopped(); loadControl.onStopped();
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
} }
...@@ -934,9 +1067,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -934,9 +1067,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,
/* resetState= */ true, /* releasePlaylist= */ true,
/* clearPlaylist= */ true,
/* resetError= */ false); /* resetError= */ false);
loadControl.onReleased(); loadControl.onReleased();
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
...@@ -949,9 +1082,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -949,9 +1082,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void resetInternal( private void resetInternal(
boolean resetRenderers, boolean resetRenderers,
boolean releaseMediaSource,
boolean resetPosition, boolean resetPosition,
boolean resetState, boolean releasePlaylist,
boolean clearPlaylist,
boolean resetError) { boolean resetError) {
handler.removeMessages(MSG_DO_SOME_WORK); handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false; rebuffering = false;
...@@ -979,8 +1112,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -979,8 +1112,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (resetPosition) { if (resetPosition) {
pendingInitialSeekPosition = null; pendingInitialSeekPosition = null;
} else if (resetState) { } else if (clearPlaylist) {
// When resetting the state, also reset the period-based PlaybackInfo position and convert // When clearing the playlist, 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()) {
...@@ -991,51 +1124,65 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -991,51 +1124,65 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
queue.clear(/* keepFrontPeriodUid= */ !resetState); queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist);
shouldContinueLoading = false; shouldContinueLoading = false;
if (resetState) { Timeline timeline = playbackInfo.timeline;
queue.setTimeline(Timeline.EMPTY); if (clearPlaylist) {
timeline = playlist.clear(/* shuffleOrder= */ null);
queue.setTimeline(timeline);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) { for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
} }
pendingMessages.clear(); pendingMessages.clear();
nextPendingMessageIndex = 0; nextPendingMessageIndex = 0;
} }
MediaPeriodId mediaPeriodId = MediaPeriodId mediaPeriodId = playbackInfo.periodId;
resetPosition long contentPositionUs = playbackInfo.contentPositionUs;
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) if (resetPosition) {
: playbackInfo.periodId; mediaPeriodId =
timeline.isEmpty()
? playbackInfo.getDummyPeriodForEmptyTimeline()
: getDummyFirstMediaPeriodForAds();
contentPositionUs = C.TIME_UNSET;
}
// Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
playbackInfo = playbackInfo =
new PlaybackInfo( new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline, timeline,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
contentPositionUs, contentPositionUs,
playbackInfo.playbackState, playbackInfo.playbackState,
resetError ? null : playbackInfo.playbackError, resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false, /* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId, mediaPeriodId,
startPositionUs, startPositionUs,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
startPositionUs); startPositionUs);
if (releaseMediaSource) { if (releasePlaylist) {
if (mediaSource != null) { playlist.release();
mediaSource.releaseSource(/* caller= */ this);
mediaSource = null;
} }
} }
private MediaPeriodId getDummyFirstMediaPeriodForAds() {
MediaPeriodId dummyFirstMediaPeriodId =
playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);
if (!playbackInfo.timeline.isEmpty()) {
// add ad metadata if any and propagate the window sequence number to new period id.
dummyFirstMediaPeriodId =
queue.resolveMediaPeriodIdForAds(dummyFirstMediaPeriodId.periodUid, /* positionUs= */ 0);
}
return dummyFirstMediaPeriodId;
} }
private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException { private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException {
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 (mediaSource == null || pendingPrepareCount > 0) { } else if (playbackInfo.timeline.isEmpty()) {
// 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 {
...@@ -1355,86 +1502,109 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1355,86 +1502,109 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
} }
mediaSource.maybeThrowSourceInfoRefreshError(); playlist.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();
if (timeline.isEmpty()) {
MediaPeriodId newPeriodId = playbackInfo.periodId; @Nullable SeekPosition pendingInitialSeekPosition = this.pendingInitialSeekPosition;
handleEndOfPlaylist();
// Retain seek position if any.
this.pendingInitialSeekPosition = pendingInitialSeekPosition;
return;
}
MediaPeriodId oldPeriodId = playbackInfo.periodId;
Object newPeriodUid = oldPeriodId.periodUid;
long oldContentPositionUs = long oldContentPositionUs =
playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
long newContentPositionUs = oldContentPositionUs; long newContentPositionUs = oldContentPositionUs;
boolean forceBufferingState = false;
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;
if (periodPosition == null) { if (periodPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the // The initial seek in the empty old timeline is invalid in the new timeline.
// timeline has changed and a suitable seek position could not be resolved in the new one. handleEndOfPlaylist();
handleSourceInfoRefreshEndedPlayback(); // Use the period resulting from the reset.
return; newPeriodUid = playbackInfo.periodId.periodUid;
newContentPositionUs = C.TIME_UNSET;
} else {
// The pending seek has been resolved successfully in the new timeline.
newPeriodUid = periodPosition.first;
newContentPositionUs =
pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET
? C.TIME_UNSET
: periodPosition.second;
forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED;
} }
newContentPositionUs = periodPosition.second; pendingInitialSeekPosition = null;
newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs); } else if (oldTimeline.isEmpty()) {
} else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) { // Resolve to default position if the old timeline is empty and no seek is requested above.
// Resolve unset start position to default position.
Pair<Object, Long> defaultPosition = Pair<Object, Long> defaultPosition =
getPeriodPosition( getPeriodPosition(
timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); timeline,
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); timeline.getFirstWindowIndex(shuffleModeEnabled),
if (!newPeriodId.isAd()) { /* windowPositionUs= */ C.TIME_UNSET);
// Keep unset start position if we need to play an ad first. newPeriodUid = defaultPosition.first;
newContentPositionUs = defaultPosition.second; newContentPositionUs = C.TIME_UNSET;
} } else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) {
} else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) {
// The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
// window we can restart from. // window we can restart from.
Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline); @Nullable
if (newPeriodUid == null) { Object subsequentPeriodUid =
// We failed to resolve a suitable restart position. resolveSubsequentPeriod(
handleSourceInfoRefreshEndedPlayback(); window, period, repeatMode, shuffleModeEnabled, newPeriodUid, oldTimeline, timeline);
return; if (subsequentPeriodUid == null) {
} // We failed to resolve a suitable restart position but the timeline is not empty.
// We resolved a subsequent period. Start at the default position in the corresponding window. handleEndOfPlaylist();
// Use period and position resulting from the reset.
newPeriodUid = playbackInfo.periodId.periodUid;
newContentPositionUs = C.TIME_UNSET;
} else {
// We resolved a subsequent period. Start at the default position in the corresponding
// window.
Pair<Object, Long> defaultPosition = Pair<Object, Long> defaultPosition =
getPeriodPosition( getPeriodPosition(
timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET); timeline,
newContentPositionUs = defaultPosition.second; timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex,
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); C.TIME_UNSET);
} else { newPeriodUid = defaultPosition.first;
// Recheck if the current ad still needs to be played or if we need to start playing an ad. newContentPositionUs = C.TIME_UNSET;
newPeriodId = }
queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs); }
if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) {
// Ensure ad insertion metadata is up to date.
long contentPositionForAdResolution = newContentPositionUs;
if (contentPositionForAdResolution == C.TIME_UNSET) {
contentPositionForAdResolution =
timeline.getWindow(timeline.getPeriodByUid(newPeriodUid, period).windowIndex, window)
.defaultPositionUs;
}
MediaPeriodId periodIdWithAds =
queue.resolveMediaPeriodIdForAds(newPeriodUid, contentPositionForAdResolution);
boolean oldAndNewPeriodIdAreSame =
oldPeriodId.periodUid.equals(newPeriodUid)
&& !oldPeriodId.isAd()
&& !periodIdWithAds.isAd();
// Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
// only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential
// discontinuity until we reach the former next ad group position. // discontinuity until we reach the former next ad group position.
newPeriodId = playbackInfo.periodId; MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds;
}
}
if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) { if (oldPeriodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) {
// We can keep the current playing period. Update the rest of the queued periods. // We can keep the current playing period. Update the rest of the queued periods.
if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
seekToCurrentPosition(/* sendDiscontinuity= */ false); seekToCurrentPosition(/* sendDiscontinuity= */ false);
} }
} else { } else {
// Something changed. Seek to new start position. // Something changed. Seek to new start position.
MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); @Nullable MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
if (periodHolder != null) { if (periodHolder != null) {
// Update the new playing media period info if it already exists. // Update the new playing media period info if it already exists.
while (periodHolder.getNext() != null) { while (periodHolder.getNext() != null) {
...@@ -1444,9 +1614,16 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1444,9 +1614,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
} }
// Actually do the seek.
long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;
long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs); if (!newPeriodId.isAd() && newContentPositionUs == C.TIME_UNSET) {
// Get the default position for the first new period that is not an ad.
int windowIndex = timeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex;
newContentPositionUs = timeline.getWindow(windowIndex, window).getDefaultPositionUs();
newPositionUs = newContentPositionUs;
}
// Actually do the seek.
long seekedToPositionUs =
seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState);
playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs); playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs);
} }
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
...@@ -1477,48 +1654,20 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1477,48 +1654,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
return maxReadPositionUs; return maxReadPositionUs;
} }
private void handleSourceInfoRefreshEndedPlayback() { private void handleEndOfPlaylist() {
if (playbackInfo.playbackState != Player.STATE_IDLE) { if (playbackInfo.playbackState != Player.STATE_IDLE) {
setState(Player.STATE_ENDED); setState(Player.STATE_ENDED);
} }
// Reset, but retain the source so that it can still be used should a seek occur. // Reset, but retain the playlist so that it can still resume after a seek or be modified.
resetInternal( resetInternal(
/* resetRenderers= */ false, /* resetRenderers= */ false,
/* releaseMediaSource= */ false,
/* resetPosition= */ true, /* resetPosition= */ true,
/* resetState= */ false, /* releasePlaylist= */ false,
/* clearPlaylist= */ false,
/* resetError= */ true); /* resetError= */ true);
} }
/** /**
* Given a period index into an old timeline, finds the first subsequent period that also exists
* in a new timeline. The uid of this period in the new timeline is returned.
*
* @param oldPeriodUid The index of the period in the old timeline.
* @param oldTimeline The old timeline.
* @param newTimeline The new timeline.
* @return The uid in the new timeline of the first subsequent period, or null if no such period
* was found.
*/
private @Nullable Object resolveSubsequentPeriod(
Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) {
int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid);
int newPeriodIndex = C.INDEX_UNSET;
int maxIterations = oldTimeline.getPeriodCount();
for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
oldPeriodIndex =
oldTimeline.getNextPeriodIndex(
oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
if (oldPeriodIndex == C.INDEX_UNSET) {
// We've reached the end of the old timeline.
break;
}
newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex));
}
return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);
}
/**
* Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the
* internal timeline. * internal timeline.
* *
...@@ -1566,7 +1715,15 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1566,7 +1715,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (trySubsequentPeriods) { if (trySubsequentPeriods) {
// Try and find a subsequent period from the seek timeline in the internal timeline. // Try and find a subsequent period from the seek timeline in the internal timeline.
@Nullable @Nullable
Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); Object periodUid =
resolveSubsequentPeriod(
window,
period,
repeatMode,
shuffleModeEnabled,
periodPosition.first,
seekTimeline,
timeline);
if (periodUid != null) { if (periodUid != null) {
// We found one. Use the default position of the corresponding window. // We found one. Use the default position of the corresponding window.
return getPeriodPosition( return getPeriodPosition(
...@@ -1587,13 +1744,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1587,13 +1744,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void updatePeriods() throws ExoPlaybackException, IOException { private void updatePeriods() throws ExoPlaybackException, IOException {
if (mediaSource == null) { if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) {
// 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.
mediaSource.maybeThrowSourceInfoRefreshError(); playlist.maybeThrowSourceInfoRefreshError();
return; return;
} }
maybeUpdateLoadingPeriod(); maybeUpdateLoadingPeriod();
...@@ -1613,7 +1766,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1613,7 +1766,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
rendererCapabilities, rendererCapabilities,
trackSelector, trackSelector,
loadControl.getAllocator(), loadControl.getAllocator(),
mediaSource, playlist,
info, info,
emptyTrackSelectorResult); emptyTrackSelectorResult);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
...@@ -1632,7 +1785,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1632,7 +1785,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void maybeUpdateReadingPeriod() throws ExoPlaybackException { private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (readingPeriodHolder == null) { if (readingPeriodHolder == null) {
return; return;
} }
...@@ -2008,6 +2161,44 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2008,6 +2161,44 @@ import java.util.concurrent.atomic.AtomicBoolean;
.sendToTarget(); .sendToTarget();
} }
/**
* Given a period index into an old timeline, finds the first subsequent period that also exists
* in a new timeline. The uid of this period in the new timeline is returned.
*
* @param window A {@link Timeline.Window} to be used internally.
* @param period A {@link Timeline.Period} to be used internally.
* @param repeatMode The repeat mode to use.
* @param shuffleModeEnabled Whether the shuffle mode is enabled.
* @param oldPeriodUid The index of the period in the old timeline.
* @param oldTimeline The old timeline.
* @param newTimeline The new timeline.
* @return The uid in the new timeline of the first subsequent period, or null if no such period
* was found.
*/
/* package */ static @Nullable Object resolveSubsequentPeriod(
Timeline.Window window,
Timeline.Period period,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
Object oldPeriodUid,
Timeline oldTimeline,
Timeline newTimeline) {
int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid);
int newPeriodIndex = C.INDEX_UNSET;
int maxIterations = oldTimeline.getPeriodCount();
for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
oldPeriodIndex =
oldTimeline.getNextPeriodIndex(
oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
if (oldPeriodIndex == C.INDEX_UNSET) {
// We've reached the end of the old timeline.
break;
}
newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex));
}
return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);
}
private static Format[] getFormats(TrackSelection newSelection) { private static Format[] getFormats(TrackSelection newSelection) {
// Build an array of formats contained by the selection. // Build an array of formats contained by the selection.
int length = newSelection != null ? newSelection.length() : 0; int length = newSelection != null ? newSelection.length() : 0;
...@@ -2068,14 +2259,38 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2068,14 +2259,38 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private static final class MediaSourceRefreshInfo { private static final class PlaylistUpdateMessage {
public final MediaSource source; private final List<Playlist.MediaSourceHolder> mediaSourceHolders;
public final Timeline timeline; private final ShuffleOrder shuffleOrder;
private final int windowIndex;
private final long positionUs;
public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { private PlaylistUpdateMessage(
this.source = source; List<Playlist.MediaSourceHolder> mediaSourceHolders,
this.timeline = timeline; ShuffleOrder shuffleOrder,
int windowIndex,
long positionUs) {
this.mediaSourceHolders = mediaSourceHolders;
this.shuffleOrder = shuffleOrder;
this.windowIndex = windowIndex;
this.positionUs = positionUs;
}
}
private static class MoveMediaItemsMessage {
public final int fromIndex;
public final int toIndex;
public final int newFromIndex;
public final ShuffleOrder shuffleOrder;
public MoveMediaItemsMessage(
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
this.fromIndex = fromIndex;
this.toIndex = toIndex;
this.newFromIndex = newFromIndex;
this.shuffleOrder = shuffleOrder;
} }
} }
...@@ -2084,7 +2299,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2084,7 +2299,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;
private @DiscontinuityReason int discontinuityReason; @DiscontinuityReason private int discontinuityReason;
public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
...@@ -2112,5 +2327,4 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2112,5 +2327,4 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.discontinuityReason = discontinuityReason; this.discontinuityReason = discontinuityReason;
} }
} }
} }
...@@ -19,7 +19,6 @@ import androidx.annotation.Nullable; ...@@ -19,7 +19,6 @@ 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;
...@@ -56,7 +55,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -56,7 +55,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 MediaSource mediaSource; private final Playlist playlist;
@Nullable private MediaPeriodHolder next; @Nullable private MediaPeriodHolder next;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
...@@ -70,7 +69,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -70,7 +69,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 mediaSource The media source that produced the media period. * @param playlist The playlist.
* @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.
...@@ -80,13 +79,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -80,13 +79,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
long rendererPositionOffsetUs, long rendererPositionOffsetUs,
TrackSelector trackSelector, TrackSelector trackSelector,
Allocator allocator, Allocator allocator,
MediaSource mediaSource, Playlist playlist,
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.mediaSource = mediaSource; this.playlist = playlist;
this.uid = info.id.periodUid; this.uid = info.id.periodUid;
this.info = info; this.info = info;
this.trackGroups = TrackGroupArray.EMPTY; this.trackGroups = TrackGroupArray.EMPTY;
...@@ -94,8 +93,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -94,8 +93,7 @@ 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( createMediaPeriod(info.id, playlist, allocator, info.startPositionUs, info.endPositionUs);
info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);
} }
/** /**
...@@ -305,7 +303,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -305,7 +303,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, mediaSource, mediaPeriod); releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod);
} }
/** /**
...@@ -402,11 +400,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -402,11 +400,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,
MediaSource mediaSource, Playlist playlist,
Allocator allocator, Allocator allocator,
long startPositionUs, long startPositionUs,
long endPositionUs) { long endPositionUs) {
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); MediaPeriod mediaPeriod = playlist.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(
...@@ -417,12 +415,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -417,12 +415,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, MediaSource mediaSource, MediaPeriod mediaPeriod) { long endPositionUs, Playlist playlist, 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) {
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} else { } else {
mediaSource.releasePeriod(mediaPeriod); playlist.releasePeriod(mediaPeriod);
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
// There's nothing we can do. // There's nothing we can do.
......
...@@ -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.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;
...@@ -134,7 +133,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -134,7 +133,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 mediaSource The media source that produced the media period. * @param playlist The playlist.
* @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.
...@@ -143,7 +142,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -143,7 +142,7 @@ import com.google.android.exoplayer2.util.Assertions;
RendererCapabilities[] rendererCapabilities, RendererCapabilities[] rendererCapabilities,
TrackSelector trackSelector, TrackSelector trackSelector,
Allocator allocator, Allocator allocator,
MediaSource mediaSource, Playlist playlist,
MediaPeriodInfo info, MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) { TrackSelectorResult emptyTrackSelectorResult) {
long rendererPositionOffsetUs = long rendererPositionOffsetUs =
...@@ -158,7 +157,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -158,7 +157,7 @@ import com.google.android.exoplayer2.util.Assertions;
rendererPositionOffsetUs, rendererPositionOffsetUs,
trackSelector, trackSelector,
allocator, allocator,
mediaSource, playlist,
info, info,
emptyTrackSelectorResult); emptyTrackSelectorResult);
if (loading != null) { if (loading != null) {
......
...@@ -162,7 +162,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; ...@@ -162,7 +162,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public MediaPeriodId getDummyFirstMediaPeriodId( public MediaPeriodId getDummyFirstMediaPeriodId(
boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) {
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
return DUMMY_MEDIA_PERIOD_ID; return getDummyPeriodForEmptyTimeline();
} }
int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex;
...@@ -178,6 +178,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; ...@@ -178,6 +178,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber);
} }
/** Returns dummy period id for an empty timeline. */
public MediaPeriodId getDummyPeriodForEmptyTimeline() {
return DUMMY_MEDIA_PERIOD_ID;
}
/** /**
* Copies playback info with new playing position. * Copies playback info with new playing position.
* *
......
...@@ -381,7 +381,8 @@ public interface Player { ...@@ -381,7 +381,8 @@ 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. May be null. * @param manifest The latest manifest in case the timeline has a single window only. Always
* null if the timeline has more than a single window.
* @param 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,
...@@ -619,25 +620,17 @@ public interface Player { ...@@ -619,25 +620,17 @@ public interface Player {
int DISCONTINUITY_REASON_INTERNAL = 4; int DISCONTINUITY_REASON_INTERNAL = 4;
/** /**
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link
* #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. * #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE})
TIMELINE_CHANGE_REASON_PREPARED,
TIMELINE_CHANGE_REASON_RESET,
TIMELINE_CHANGE_REASON_DYNAMIC
})
@interface TimelineChangeReason {} @interface TimelineChangeReason {}
/** Timeline and manifest changed as a result of a player initialization with new media. */ /** Timeline changed as a result of a change of the playlist items or the order of the items. */
int TIMELINE_CHANGE_REASON_PREPARED = 0; int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0;
/** Timeline and manifest changed as a result of a player reset. */ /** Timeline changed as a result of a dynamic update introduced by the played media. */
int TIMELINE_CHANGE_REASON_RESET = 1; int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 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
......
...@@ -42,6 +42,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; ...@@ -42,6 +42,7 @@ 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;
...@@ -163,7 +164,9 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -163,7 +164,9 @@ 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 media sources should be initialized lazily. * @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all
* initial preparation steps (e.g., manifest loads) happen immediately. If true, these
* initial preparations are triggered only when the player starts buffering the media.
* @param 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(
...@@ -300,6 +303,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -300,6 +303,7 @@ public class SimpleExoPlayer extends BasePlayer
loadControl, loadControl,
bandwidthMeter, bandwidthMeter,
analyticsCollector, analyticsCollector,
useLazyPreparation,
clock, clock,
looper); looper);
} }
...@@ -342,7 +346,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -342,7 +346,6 @@ 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;
...@@ -359,6 +362,9 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -359,6 +362,9 @@ 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
...@@ -372,6 +378,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -372,6 +378,7 @@ 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(
...@@ -382,26 +389,14 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -382,26 +389,14 @@ 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, Clock, Looper)} instead, and pass the {@link * BandwidthMeter, AnalyticsCollector, boolean, Clock, Looper)} instead, and pass the {@link
* DrmSessionManager} to the {@link MediaSource} factories. * DrmSessionManager} to the {@link MediaSource} factories.
*/ */
@Deprecated @Deprecated
...@@ -413,6 +408,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -413,6 +408,7 @@ 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;
...@@ -443,7 +439,15 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -443,7 +439,15 @@ public class SimpleExoPlayer extends BasePlayer
// Build the player and associated objects. // Build the player and associated objects.
player = player =
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); new ExoPlayerImpl(
renderers,
trackSelector,
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
analyticsCollector.setPlayer(player); analyticsCollector.setPlayer(player);
addListener(analyticsCollector); addListener(analyticsCollector);
addListener(componentListener); addListener(componentListener);
...@@ -1164,52 +1168,152 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1164,52 +1168,152 @@ public class SimpleExoPlayer extends BasePlayer
return player.getPlaybackError(); return player.getPlaybackError();
} }
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
@Override @Override
@SuppressWarnings("deprecation")
public void retry() { public void retry() {
verifyApplicationThread(); verifyApplicationThread();
if (mediaSource != null prepare();
&& (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) {
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
}
} }
@Override @Override
public void prepare() {
verifyApplicationThread();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
player.prepare();
}
/**
* @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead.
*/
@Deprecated @Deprecated
@Override
@SuppressWarnings("deprecation") @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 /**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()}
* instead.
*/
@Deprecated @Deprecated
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
verifyApplicationThread(); verifyApplicationThread();
setMediaSource(mediaSource); setMediaSources(
prepareInternal(resetPosition, resetState); Collections.singletonList(mediaSource),
/* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET,
/* startPositionMs= */ C.TIME_UNSET);
prepare();
} }
@Override @Override
public void prepare() { public void setMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread(); verifyApplicationThread();
prepareInternal(/* resetPosition= */ false, /* resetState= */ true); analyticsCollector.resetForNewPlaylist();
player.setMediaSources(mediaSources);
} }
@Override @Override
public void setMediaSource(MediaSource mediaSource, long startPositionMs) { public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
verifyApplicationThread(); verifyApplicationThread();
setMediaSourceInternal(mediaSource); analyticsCollector.resetForNewPlaylist();
player.setMediaSource(mediaSource, startPositionMs); player.setMediaSources(mediaSources, resetPosition);
}
@Override
public void setMediaSources(
List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaSources(mediaSources, startWindowIndex, startPositionMs);
} }
@Override @Override
public void setMediaSource(MediaSource mediaSource) { public void setMediaSource(MediaSource mediaSource) {
verifyApplicationThread(); verifyApplicationThread();
setMediaSourceInternal(mediaSource); analyticsCollector.resetForNewPlaylist();
player.setMediaSource(mediaSource); player.setMediaSource(mediaSource);
} }
@Override @Override
public void setMediaSource(MediaSource mediaSource, boolean resetPosition) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaSource(mediaSource, resetPosition);
}
@Override
public void setMediaSource(MediaSource mediaSource, long startPositionMs) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaSource(mediaSource, startPositionMs);
}
@Override
public void addMediaSource(MediaSource mediaSource) {
verifyApplicationThread();
player.addMediaSource(mediaSource);
}
@Override
public void addMediaSource(int index, MediaSource mediaSource) {
verifyApplicationThread();
player.addMediaSource(index, mediaSource);
}
@Override
public void addMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread();
player.addMediaSources(mediaSources);
}
@Override
public void addMediaSources(int index, List<MediaSource> mediaSources) {
verifyApplicationThread();
player.addMediaSources(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
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
verifyApplicationThread(); verifyApplicationThread();
@AudioFocusManager.PlayerCommand @AudioFocusManager.PlayerCommand
...@@ -1286,6 +1390,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1286,6 +1390,7 @@ public class SimpleExoPlayer extends BasePlayer
@Override @Override
public void setForegroundMode(boolean foregroundMode) { public void setForegroundMode(boolean foregroundMode) {
verifyApplicationThread();
player.setForegroundMode(foregroundMode); player.setForegroundMode(foregroundMode);
} }
...@@ -1293,13 +1398,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1293,13 +1398,6 @@ 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();
} }
...@@ -1318,10 +1416,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1318,10 +1416,6 @@ 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;
...@@ -1455,23 +1549,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1455,23 +1549,6 @@ public class SimpleExoPlayer extends BasePlayer
// Internal methods. // Internal methods.
private void prepareInternal(boolean resetPosition, boolean resetState) {
Assertions.checkNotNull(mediaSource);
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
player.prepareInternal(resetPosition, resetState);
}
private void setMediaSourceInternal(MediaSource mediaSource) {
if (this.mediaSource != null) {
this.mediaSource.removeEventListener(analyticsCollector);
analyticsCollector.resetForNewMediaSource();
}
this.mediaSource = mediaSource;
this.mediaSource.addEventListener(eventHandler, analyticsCollector);
}
private void removeSurfaceCallbacks() { private void removeSurfaceCallbacks() {
if (textureView != null) { if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) { if (textureView.getSurfaceTextureListener() != componentListener) {
......
...@@ -135,11 +135,8 @@ public class AnalyticsCollector ...@@ -135,11 +135,8 @@ public class AnalyticsCollector
} }
} }
/** /** Resets the analytics collector for a new playlist. */
* Resets the analytics collector for a new media source. Should be called before the player is public final void resetForNewPlaylist() {
* 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);
...@@ -806,9 +803,13 @@ public class AnalyticsCollector ...@@ -806,9 +803,13 @@ 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) {
boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET; int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
boolean isInTimeline = periodIndex != C.INDEX_UNSET;
MediaPeriodInfo mediaPeriodInfo = MediaPeriodInfo mediaPeriodInfo =
new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); new MediaPeriodInfo(
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);
...@@ -824,7 +825,7 @@ public class AnalyticsCollector ...@@ -824,7 +825,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 resetForNewMediaSource(). // The media period has already been removed from the queue in resetForNewPlaylist().
return false; return false;
} }
mediaPeriodInfoQueue.remove(mediaPeriodInfo); mediaPeriodInfoQueue.remove(mediaPeriodInfo);
......
...@@ -617,12 +617,10 @@ public class EventLogger implements AnalyticsListener { ...@@ -617,12 +617,10 @@ 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_PREPARED: case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE:
return "PREPARED"; return "SOURCE_UPDATE";
case Player.TIMELINE_CHANGE_REASON_RESET: case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED:
return "RESET"; return "PLAYLIST_CHANGED";
case Player.TIMELINE_CHANGE_REASON_DYNAMIC:
return "DYNAMIC";
default: default:
return "?"; return "?";
} }
......
...@@ -21,15 +21,17 @@ import static org.mockito.Mockito.mock; ...@@ -21,15 +21,17 @@ 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;
...@@ -50,19 +52,20 @@ public final class MediaPeriodQueueTest { ...@@ -50,19 +52,20 @@ 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 MediaSource mediaSource; private Playlist playlist;
private FakeMediaSource fakeMediaSource;
private Playlist.MediaSourceHolder mediaSourceHolder;
@Before @Before
public void setUp() { public void setUp() {
mediaPeriodQueue = new MediaPeriodQueue(); mediaPeriodQueue = new MediaPeriodQueue();
mediaSource = mock(MediaSource.class); playlist = mock(Playlist.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);
...@@ -70,7 +73,7 @@ public final class MediaPeriodQueueTest { ...@@ -70,7 +73,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {
setupTimeline(/* initialPositionUs= */ 0); setupTimeline();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* endPositionUs= */ C.TIME_UNSET, /* endPositionUs= */ C.TIME_UNSET,
...@@ -81,7 +84,7 @@ public final class MediaPeriodQueueTest { ...@@ -81,7 +84,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ 0); setupTimeline(/* adGroupTimesUs...= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);
advance(); advance();
...@@ -95,10 +98,7 @@ public final class MediaPeriodQueueTest { ...@@ -95,10 +98,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
/* 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,
...@@ -133,10 +133,7 @@ public final class MediaPeriodQueueTest { ...@@ -133,10 +133,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE);
/* 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,
...@@ -169,7 +166,7 @@ public final class MediaPeriodQueueTest { ...@@ -169,7 +166,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {
setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE); setupTimeline(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* endPositionUs= */ C.TIME_END_OF_SOURCE, /* endPositionUs= */ C.TIME_END_OF_SOURCE,
...@@ -189,10 +186,7 @@ public final class MediaPeriodQueueTest { ...@@ -189,10 +186,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
/* 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.
...@@ -202,10 +196,8 @@ public final class MediaPeriodQueueTest { ...@@ -202,10 +196,8 @@ 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).
setupTimeline( updateAdPlaybackStateAndTimeline(
/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1);
/* 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 =
...@@ -219,10 +211,7 @@ public final class MediaPeriodQueueTest { ...@@ -219,10 +211,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
/* 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.
...@@ -233,10 +222,8 @@ public final class MediaPeriodQueueTest { ...@@ -233,10 +222,8 @@ 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).
setupTimeline( updateAdPlaybackStateAndTimeline(
/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US);
/* 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 =
...@@ -250,10 +237,7 @@ public final class MediaPeriodQueueTest { ...@@ -250,10 +237,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
/* 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.
...@@ -265,10 +249,8 @@ public final class MediaPeriodQueueTest { ...@@ -265,10 +249,8 @@ 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).
setupTimeline( updateAdPlaybackStateAndTimeline(
/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
/* 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;
...@@ -284,10 +266,7 @@ public final class MediaPeriodQueueTest { ...@@ -284,10 +266,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
/* 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.
...@@ -299,10 +278,8 @@ public final class MediaPeriodQueueTest { ...@@ -299,10 +278,8 @@ 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).
setupTimeline( updateAdPlaybackStateAndTimeline(
/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
/* 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;
...@@ -318,10 +295,7 @@ public final class MediaPeriodQueueTest { ...@@ -318,10 +295,7 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline( setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
/* 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.
...@@ -333,10 +307,8 @@ public final class MediaPeriodQueueTest { ...@@ -333,10 +307,8 @@ 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).
setupTimeline( updateAdPlaybackStateAndTimeline(
/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
/* 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 =
...@@ -347,16 +319,25 @@ public final class MediaPeriodQueueTest { ...@@ -347,16 +319,25 @@ public final class MediaPeriodQueueTest {
assertThat(getQueueLength()).isEqualTo(3); assertThat(getQueueLength()).isEqualTo(3);
} }
private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { private void setupTimeline(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, initialPositionUs), mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0),
/* startPositionUs= */ 0, /* startPositionUs= */ 0,
/* contentPositionUs= */ 0, /* contentPositionUs= */ 0,
Player.STATE_READY, Player.STATE_READY,
...@@ -370,6 +351,25 @@ public final class MediaPeriodQueueTest { ...@@ -370,6 +351,25 @@ 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()) {
...@@ -390,7 +390,7 @@ public final class MediaPeriodQueueTest { ...@@ -390,7 +390,7 @@ public final class MediaPeriodQueueTest {
rendererCapabilities, rendererCapabilities,
trackSelector, trackSelector,
allocator, allocator,
mediaSource, playlist,
getNextMediaPeriodInfo(), getNextMediaPeriodInfo(),
new TrackSelectorResult( new TrackSelectorResult(
new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); new RendererConfiguration[0], new TrackSelection[0], /* info= */ null));
...@@ -422,11 +422,6 @@ public final class MediaPeriodQueueTest { ...@@ -422,11 +422,6 @@ 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,
......
...@@ -44,7 +44,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; ...@@ -44,7 +44,6 @@ 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;
...@@ -133,24 +132,29 @@ public final class AnalyticsCollectorTest { ...@@ -133,24 +132,29 @@ 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)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.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(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, 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(SINGLE_PERIOD_TIMELINE); populateEventIds(listener.lastReportedTimeline);
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)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.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);
...@@ -179,9 +183,14 @@ public final class AnalyticsCollectorTest { ...@@ -179,9 +183,14 @@ 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, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); SINGLE_PERIOD_TIMELINE,
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);
...@@ -191,7 +200,8 @@ public final class AnalyticsCollectorTest { ...@@ -191,7 +200,8 @@ public final class AnalyticsCollectorTest {
WINDOW_0 /* BUFFERING */, WINDOW_0 /* BUFFERING */,
period0 /* READY */, period0 /* READY */,
period1 /* ENDED */); period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.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);
...@@ -233,8 +243,8 @@ public final class AnalyticsCollectorTest { ...@@ -233,8 +243,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, Builder.VIDEO_FORMAT), new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource); TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline); populateEventIds(listener.lastReportedTimeline);
...@@ -246,7 +256,8 @@ public final class AnalyticsCollectorTest { ...@@ -246,7 +256,8 @@ public final class AnalyticsCollectorTest {
period1 /* BUFFERING */, period1 /* BUFFERING */,
period1 /* READY */, period1 /* READY */,
period1 /* ENDED */); period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.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);
...@@ -286,8 +297,8 @@ public final class AnalyticsCollectorTest { ...@@ -286,8 +297,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, Builder.VIDEO_FORMAT), new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder("AnalyticsCollectorTest") new ActionSchedule.Builder("AnalyticsCollectorTest")
.pause() .pause()
...@@ -308,7 +319,8 @@ public final class AnalyticsCollectorTest { ...@@ -308,7 +319,8 @@ public final class AnalyticsCollectorTest {
period1 /* READY */, period1 /* READY */,
period1 /* setPlayWhenReady=true */, period1 /* setPlayWhenReady=true */,
period1 /* ENDED */); period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.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);
...@@ -350,9 +362,11 @@ public final class AnalyticsCollectorTest { ...@@ -350,9 +362,11 @@ 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, Builder.VIDEO_FORMAT), new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource( new FakeMediaSource(
SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); SINGLE_PERIOD_TIMELINE,
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 =
...@@ -380,7 +394,8 @@ public final class AnalyticsCollectorTest { ...@@ -380,7 +394,8 @@ public final class AnalyticsCollectorTest {
period1Seq2 /* BUFFERING */, period1Seq2 /* BUFFERING */,
period1Seq2 /* READY */, period1Seq2 /* READY */,
period1Seq2 /* ENDED */); period1Seq2 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.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);
...@@ -428,18 +443,28 @@ public final class AnalyticsCollectorTest { ...@@ -428,18 +443,28 @@ public final class AnalyticsCollectorTest {
@Test @Test
public void testPrepareNewSource() throws Exception { public void testPrepareNewSource() throws Exception {
MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); MediaSource mediaSource1 =
MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.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)
.prepareSource(mediaSource2) .setMediaSources(/* resetPosition= */ false, mediaSource2)
.play() .play()
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);
populateEventIds(SINGLE_PERIOD_TIMELINE); // Populate all event ids with last timeline (after second prepare).
populateEventIds(listener.lastReportedTimeline);
// Populate event id of period 0, sequence 0 with timeline of initial preparation.
period0Seq0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
listener.reportedTimelines.get(1).getUidOfPeriod(/* periodIndex= */ 0),
/* windowSequenceNumber= */ 0));
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly( .containsExactly(
WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* setPlayWhenReady=true */,
...@@ -451,12 +476,16 @@ public final class AnalyticsCollectorTest { ...@@ -451,12 +476,16 @@ 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(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); .containsExactly(
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 /* reset */, period0Seq1 /* prepared */); period0Seq0 /* prepared */, WINDOW_0 /* setMediaSources */, period0Seq1 /* prepared */);
assertThat(listener.getEvents(EVENT_LOAD_STARTED)) assertThat(listener.getEvents(EVENT_LOAD_STARTED))
.containsExactly( .containsExactly(
WINDOW_0 /* manifest */, WINDOW_0 /* manifest */,
...@@ -490,19 +519,20 @@ public final class AnalyticsCollectorTest { ...@@ -490,19 +519,20 @@ public final class AnalyticsCollectorTest {
@Test @Test
public void testReprepareAfterError() throws Exception { public void testReprepareAfterError() throws Exception {
MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); MediaSource mediaSource =
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)
.prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) .prepare()
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
populateEventIds(SINGLE_PERIOD_TIMELINE); populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly( .containsExactly(
WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* setPlayWhenReady=true */,
...@@ -556,7 +586,7 @@ public final class AnalyticsCollectorTest { ...@@ -556,7 +586,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, Builder.VIDEO_FORMAT); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
final ConcatenatingMediaSource concatenatedMediaSource = final ConcatenatingMediaSource concatenatedMediaSource =
new ConcatenatingMediaSource(childMediaSource, childMediaSource); new ConcatenatingMediaSource(childMediaSource, childMediaSource);
long periodDurationMs = long periodDurationMs =
...@@ -588,7 +618,11 @@ public final class AnalyticsCollectorTest { ...@@ -588,7 +618,11 @@ public final class AnalyticsCollectorTest {
period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* setPlayWhenReady=true */,
period1Seq0 /* BUFFERING */, period1Seq0 /* BUFFERING */,
period1Seq0 /* ENDED */); period1Seq0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */,
period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly( .containsExactly(
window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);
...@@ -642,7 +676,7 @@ public final class AnalyticsCollectorTest { ...@@ -642,7 +676,7 @@ public final class AnalyticsCollectorTest {
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
populateEventIds(SINGLE_PERIOD_TIMELINE); populateEventIds(listener.lastReportedTimeline);
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);
} }
...@@ -709,7 +743,7 @@ public final class AnalyticsCollectorTest { ...@@ -709,7 +743,7 @@ public final class AnalyticsCollectorTest {
TestAnalyticsListener listener = new TestAnalyticsListener(); TestAnalyticsListener listener = new TestAnalyticsListener();
try { try {
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource) .setMediaSources(mediaSource)
.setRenderersFactory(renderersFactory) .setRenderersFactory(renderersFactory)
.setAnalyticsListener(listener) .setAnalyticsListener(listener)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
...@@ -731,7 +765,7 @@ public final class AnalyticsCollectorTest { ...@@ -731,7 +765,7 @@ public final class AnalyticsCollectorTest {
private boolean renderedFirstFrame; private boolean renderedFirstFrame;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(Builder.VIDEO_FORMAT); super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
} }
...@@ -789,7 +823,7 @@ public final class AnalyticsCollectorTest { ...@@ -789,7 +823,7 @@ public final class AnalyticsCollectorTest {
private boolean notifiedAudioSessionId; private boolean notifiedAudioSessionId;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
super(Builder.AUDIO_FORMAT); super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
} }
...@@ -873,10 +907,12 @@ public final class AnalyticsCollectorTest { ...@@ -873,10 +907,12 @@ 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;
} }
...@@ -906,6 +942,7 @@ public final class AnalyticsCollectorTest { ...@@ -906,6 +942,7 @@ 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,6 +21,7 @@ import androidx.annotation.Nullable; ...@@ -21,6 +21,7 @@ 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,6 +30,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer; ...@@ -29,6 +30,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
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,6 +40,8 @@ import com.google.android.exoplayer2.util.ConditionVariable; ...@@ -38,6 +40,8 @@ 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 com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
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 {
...@@ -114,6 +118,7 @@ public abstract class Action { ...@@ -114,6 +118,7 @@ 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)}.
...@@ -125,6 +130,7 @@ public abstract class Action { ...@@ -125,6 +130,7 @@ 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;
} }
/** /**
...@@ -133,21 +139,188 @@ public abstract class Action { ...@@ -133,21 +139,188 @@ 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) { public Seek(String tag, int windowIndex, long positionMs, boolean catchIllegalSeekException) {
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#setMediaSources(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.setMediaSources(Arrays.asList(mediaSources), windowIndex, positionMs);
}
}
/** Calls {@link SimpleExoPlayer#addMediaSources(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.addMediaSources(Arrays.asList(mediaSources));
}
}
/** Calls {@link SimpleExoPlayer#setMediaSources(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.setMediaSources(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();
} }
} }
...@@ -209,7 +382,6 @@ public abstract class Action { ...@@ -209,7 +382,6 @@ public abstract class Action {
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.setPlayWhenReady(playWhenReady); player.setPlayWhenReady(playWhenReady);
} }
} }
/** /**
...@@ -295,52 +467,31 @@ public abstract class Action { ...@@ -295,52 +467,31 @@ public abstract class Action {
} }
} }
/** Calls {@link ExoPlayer#prepare(MediaSource)}. */ /** Calls {@link ExoPlayer#prepare()}. */
public static final class PrepareSource extends Action { public static final class Prepare extends Action {
/** @param tag A tag to use for logging. */
private final MediaSource mediaSource; public Prepare(String tag) {
private final boolean resetPosition; super(tag, "Prepare");
private final boolean resetState;
/**
* @param tag A tag to use for logging.
* @param mediaSource The {@link MediaSource} to prepare the player with.
*/
public PrepareSource(String tag, MediaSource mediaSource) {
this(tag, mediaSource, true, true);
}
/**
* @param tag A tag to use for logging.
* @param mediaSource The {@link MediaSource} to prepare the player with.
* @param resetPosition Whether the player's position should be reset.
*/
public PrepareSource(
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(mediaSource, resetPosition, resetState); player.prepare();
} }
} }
/** Calls {@link Player#setRepeatMode(int)}. */ /** Calls {@link Player#setRepeatMode(int)}. */
public static final class SetRepeatMode extends Action { public static final class SetRepeatMode extends Action {
private final @Player.RepeatMode int repeatMode; @Player.RepeatMode private final int repeatMode;
/** /**
* @param tag A tag to use for logging. * @param tag A tag to use for logging.
* @param repeatMode The repeat mode. * @param repeatMode The repeat mode.
*/ */
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;
} }
...@@ -351,6 +502,27 @@ public abstract class Action { ...@@ -351,6 +502,27 @@ 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 {
...@@ -361,7 +533,7 @@ public abstract class Action { ...@@ -361,7 +533,7 @@ public abstract class Action {
* @param shuffleModeEnabled Whether shuffling is enabled. * @param shuffleModeEnabled Whether shuffling is enabled.
*/ */
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;
} }
...@@ -448,7 +620,6 @@ public abstract class Action { ...@@ -448,7 +620,6 @@ 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. */
...@@ -546,17 +717,34 @@ public abstract class Action { ...@@ -546,17 +717,34 @@ public abstract class Action {
public static final class WaitForTimelineChanged extends Action { public static final class WaitForTimelineChanged extends Action {
@Nullable 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. * Creates action waiting for a timeline change for a given reason.
* *
* @param tag A tag to use for logging. * @param tag A tag to use for logging.
* @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline * @param expectedTimeline The expected timeline or null if any timeline change is relevant.
* change. * @param expectedReason The expected timeline change reason.
*/ */
public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) { public WaitForTimelineChanged(
String tag, Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
super(tag, "WaitForTimelineChanged"); super(tag, "WaitForTimelineChanged");
this.expectedTimeline = expectedTimeline; this.expectedTimeline = expectedTimeline != null ? new NoUidTimeline(expectedTimeline) : null;
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
...@@ -574,14 +762,16 @@ public abstract class Action { ...@@ -574,14 +762,16 @@ 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 || timeline.equals(expectedTimeline)) { if ((expectedTimeline == null || new NoUidTimeline(timeline).equals(expectedTimeline))
&& (ignoreExpectedReason || expectedReason == reason)) {
player.removeListener(this); player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler); nextAction.schedule(player, trackSelector, surface, handler);
} }
} }
}; };
player.addListener(listener); player.addListener(listener);
if (expectedTimeline != null && player.getCurrentTimeline().equals(expectedTimeline)) { Timeline currentTimeline = new NoUidTimeline(player.getCurrentTimeline());
if (currentTimeline.equals(expectedTimeline)) {
player.removeListener(listener); player.removeListener(listener);
nextAction.schedule(player, trackSelector, surface, handler); nextAction.schedule(player, trackSelector, surface, handler);
} }
...@@ -732,6 +922,50 @@ public abstract class Action { ...@@ -732,6 +922,50 @@ public abstract class Action {
} }
/** /**
* Waits for a player message to arrive. If the target already received a message, the action
* returns immediately.
*/
public static final class WaitForMessage extends Action {
private final PlayerTarget playerTarget;
/**
* @param tag A tag to use for logging.
* @param playerTarget The target to observe.
*/
public WaitForMessage(String tag, PlayerTarget playerTarget) {
super(tag, "WaitForMessage");
this.playerTarget = playerTarget;
}
@Override
protected void doActionAndScheduleNextImpl(
final SimpleExoPlayer player,
final DefaultTrackSelector trackSelector,
final Surface surface,
final HandlerWrapper handler,
final ActionNode nextAction) {
if (nextAction == null) {
return;
}
PlayerTarget.Callback callback =
new PlayerTarget.Callback() {
@Override
public void onMessageArrived() {
nextAction.schedule(player, trackSelector, surface, handler);
}
};
playerTarget.setCallback(callback);
}
@Override
protected void doActionImpl(
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
// Not triggered.
}
}
/**
* Waits for a specified loading state, returning either immediately or after a call to {@link * Waits for a specified loading state, returning either immediately or after a call to {@link
* Player.EventListener#onLoadingChanged(boolean)}. * Player.EventListener#onLoadingChanged(boolean)}.
*/ */
...@@ -816,7 +1050,7 @@ public abstract class Action { ...@@ -816,7 +1050,7 @@ public abstract class Action {
} }
} }
/** Calls {@link Runnable#run()}. */ /** Calls {@code Runnable.run()}. */
public static final class ExecuteRunnable extends Action { public static final class ExecuteRunnable extends Action {
private final Runnable runnable; private final Runnable runnable;
......
...@@ -28,10 +28,10 @@ import com.google.android.exoplayer2.SimpleExoPlayer; ...@@ -28,10 +28,10 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
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.SetAudioAttributes; import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes;
...@@ -40,10 +40,12 @@ import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; ...@@ -40,10 +40,12 @@ 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;
import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading; import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading;
import com.google.android.exoplayer2.testutil.Action.WaitForMessage;
import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady;
import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState;
import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity;
...@@ -172,7 +174,19 @@ public final class ActionSchedule { ...@@ -172,7 +174,19 @@ 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)); return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false));
}
/**
* Schedules a seek action to be executed.
*
* @param windowIndex The window to seek to.
* @param positionMs The seek position.
* @param catchIllegalSeekException Whether an illegal seek position should be caught or not.
* @return The builder, for convenience.
*/
public Builder seek(int windowIndex, long positionMs, boolean catchIllegalSeekException) {
return apply(new Seek(tag, windowIndex, positionMs, catchIllegalSeekException));
} }
/** /**
...@@ -313,23 +327,99 @@ public final class ActionSchedule { ...@@ -313,23 +327,99 @@ public final class ActionSchedule {
} }
/** /**
* Schedules a new source preparation action. * Schedules a set media items 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 setMediaSources(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 setMediaSources(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 setMediaSources(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 addMediaSources(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 prepareSource(MediaSource mediaSource) { public Builder moveMediaItem(int currentIndex, int newIndex) {
return apply(new PrepareSource(tag, mediaSource)); return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex));
} }
/** /**
* Schedules a new source preparation action. * Schedules a remove media item action to be executed.
* *
* @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean) * @param index The index of the media item to be removed.
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder prepareSource( public Builder removeMediaItem(int index) {
MediaSource mediaSource, boolean resetPosition, boolean resetState) { return apply(new Action.RemoveMediaItem(tag, index));
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));
} }
/** /**
...@@ -342,7 +432,17 @@ public final class ActionSchedule { ...@@ -342,7 +432,17 @@ public final class ActionSchedule {
} }
/** /**
* Schedules a shuffle setting action. * 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.
* *
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
...@@ -394,18 +494,19 @@ public final class ActionSchedule { ...@@ -394,18 +494,19 @@ 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, /* expectedTimeline= */ null)); return apply(new WaitForTimelineChanged(tag));
} }
/** /**
* 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 to wait for. If null, wait for any timeline * @param expectedTimeline The expected timeline.
* change. * @param expectedReason The expected reason of the timeline change.
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder waitForTimelineChanged(Timeline expectedTimeline) { public Builder waitForTimelineChanged(
return apply(new WaitForTimelineChanged(tag, expectedTimeline)); Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason));
} }
/** /**
...@@ -448,6 +549,16 @@ public final class ActionSchedule { ...@@ -448,6 +549,16 @@ public final class ActionSchedule {
} }
/** /**
* Schedules a delay until a message arrives at the {@link PlayerMessage.Target}.
*
* @param playerTarget The target to observe.
* @return The builder, for convenience.
*/
public Builder waitForMessage(PlayerTarget playerTarget) {
return apply(new WaitForMessage(tag, playerTarget));
}
/**
* Schedules a {@link Runnable}. * Schedules a {@link Runnable}.
* *
* @return The builder, for convenience. * @return The builder, for convenience.
...@@ -484,10 +595,28 @@ public final class ActionSchedule { ...@@ -484,10 +595,28 @@ public final class ActionSchedule {
/** /**
* Provides a wrapper for a {@link Target} which has access to the player when handling messages. * Provides a wrapper for a {@link Target} which has access to the player when handling messages.
* Can be used with {@link Builder#sendMessage(Target, long)}. * Can be used with {@link Builder#sendMessage(Target, long)}.
*
* <p>The target can be passed to {@link ActionSchedule.Builder#waitForMessage(PlayerTarget)} to
* wait for a message to arrive at the target.
*/ */
public abstract static class PlayerTarget implements Target { public abstract static class PlayerTarget implements Target {
/** Callback to be called when message arrives. */
public interface Callback {
/** Notifies about the arrival of the message. */
void onMessageArrived();
}
private SimpleExoPlayer player; private SimpleExoPlayer player;
private boolean hasArrived;
private Callback callback;
public void setCallback(Callback callback) {
this.callback = callback;
if (hasArrived) {
callback.onMessageArrived();
}
}
/** Handles the message send to the component and additionally provides access to the player. */ /** Handles the message send to the component and additionally provides access to the player. */
public abstract void handleMessage( public abstract void handleMessage(
...@@ -499,9 +628,12 @@ public final class ActionSchedule { ...@@ -499,9 +628,12 @@ public final class ActionSchedule {
} }
@Override @Override
public final void handleMessage(int messageType, @Nullable Object message) public final void handleMessage(int messageType, @Nullable Object message) {
throws ExoPlaybackException {
handleMessage(player, messageType, message); handleMessage(player, messageType, message);
if (callback != null) {
hasArrived = true;
callback.onMessageArrived();
}
} }
} }
......
...@@ -16,11 +16,14 @@ ...@@ -16,11 +16,14 @@
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.assertFalse;
import static junit.framework.TestCase.assertTrue;
import android.content.Context; import android.content.Context;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -43,6 +46,7 @@ import com.google.android.exoplayer2.util.HandlerWrapper; ...@@ -43,6 +46,7 @@ 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;
...@@ -87,8 +91,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -87,8 +91,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;
...@@ -99,19 +103,31 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -99,19 +103,31 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private Player.EventListener eventListener; private Player.EventListener eventListener;
private AnalyticsListener analyticsListener; private AnalyticsListener analyticsListener;
private Integer expectedPlayerEndedCount; private Integer expectedPlayerEndedCount;
private boolean useLazyPreparation;
private int initialWindowIndex;
private long initialPositionMs;
private boolean skipSettingMediaSources;
public Builder() {
mediaSources = new ArrayList<>();
initialWindowIndex = C.INDEX_UNSET;
initialPositionMs = C.TIME_UNSET;
}
/** /**
* 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 #setMediaSource(MediaSource)}. * not allowed after a call to {@link #setMediaSources(MediaSource...)} or {@link
* #skipSettingMediaSources()}.
* *
* @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(mediaSource).isNull(); assertThat(mediaSources).isEmpty();
assertFalse(skipSettingMediaSources);
this.timeline = timeline; this.timeline = timeline;
return this; return this;
} }
...@@ -119,30 +135,73 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -119,30 +135,73 @@ 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
* #setMediaSource(MediaSource)}. * #setMediaSources(MediaSource...)} or {@link #skipSettingMediaSources()}.
* *
* @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(mediaSource).isNull(); assertThat(mediaSources).isEmpty();
assertFalse(skipSettingMediaSources);
this.manifest = manifest; this.manifest = manifest;
return this; return this;
} }
/** /**
* Sets a {@link MediaSource} to be used by the test runner. The default value is a {@link * Seeks before setting the media sources and preparing the player.
*
* @param windowIndex The window index to seek to.
* @param positionMs The position in milliseconds to seek to.
* @return This builder.
*/
public Builder initialSeek(int windowIndex, long positionMs) {
this.initialWindowIndex = windowIndex;
this.initialPositionMs = positionMs;
return this;
}
/**
* Sets the {@link MediaSource}s 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 the media source is not allowed after calls to * and {@link #setManifest(Object)}. Setting media sources is not allowed after calls to {@link
* {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. * #skipSettingMediaSources()}, {@link #setTimeline(Timeline)} and/or {@link
* #setManifest(Object)}.
* *
* @param mediaSource A {@link MediaSource} to be used by the test runner. * @param mediaSources The {@link MediaSource}s to be used by the test runner.
* @return This builder. * @return This builder.
*/ */
public Builder setMediaSource(MediaSource mediaSource) { public Builder setMediaSources(MediaSource... mediaSources) {
assertThat(timeline).isNull(); assertThat(timeline).isNull();
assertThat(manifest).isNull(); assertThat(manifest).isNull();
this.mediaSource = mediaSource; assertFalse(skipSettingMediaSources);
this.mediaSources = Arrays.asList(mediaSources);
return this;
}
/**
* Skips calling {@link com.google.android.exoplayer2.ExoPlayer#setMediaSources(List)} before
* preparing. Calling this method is not allowed after calls to {@link
* #setMediaSources(MediaSource...)}, {@link #setTimeline(Timeline)} and/or {@link
* #setManifest(Object)}.
*
* @return This builder.
*/
public Builder skipSettingMediaSources() {
assertThat(timeline).isNull();
assertThat(manifest).isNull();
assertTrue(mediaSources.isEmpty());
skipSettingMediaSources = true;
return this;
}
/**
* Sets whether to use lazy preparation.
*
* @param useLazyPreparation Whether to use lazy preparation.
* @return This builder.
*/
public Builder setUseLazyPreparation(boolean useLazyPreparation) {
this.useLazyPreparation = useLazyPreparation;
return this; return this;
} }
...@@ -186,7 +245,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -186,7 +245,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 #setMediaSource(MediaSource)} and renderers with {@link * with {@link #setMediaSources(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.
...@@ -240,7 +299,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -240,7 +299,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(MediaSource)}. * executed immediately before {@link SimpleExoPlayer#prepare()}.
* *
* @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.
...@@ -321,11 +380,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -321,11 +380,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (clock == null) { if (clock == null) {
clock = new AutoAdvancingFakeClock(); clock = new AutoAdvancingFakeClock();
} }
if (mediaSource == null) { if (mediaSources.isEmpty() && !skipSettingMediaSources) {
if (timeline == null) { if (timeline == null) {
timeline = new FakeTimeline(/* windowCount= */ 1, manifest); timeline = new FakeTimeline(/* windowCount= */ 1, manifest);
} }
mediaSource = new FakeMediaSource(timeline, supportedFormats); mediaSources.add(new FakeMediaSource(timeline, supportedFormats));
} }
if (expectedPlayerEndedCount == null) { if (expectedPlayerEndedCount == null) {
expectedPlayerEndedCount = 1; expectedPlayerEndedCount = 1;
...@@ -333,7 +392,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -333,7 +392,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
return new ExoPlayerTestRunner( return new ExoPlayerTestRunner(
context, context,
clock, clock,
mediaSource, initialWindowIndex,
initialPositionMs,
mediaSources,
skipSettingMediaSources,
useLazyPreparation,
renderersFactory, renderersFactory,
trackSelector, trackSelector,
loadControl, loadControl,
...@@ -347,7 +410,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -347,7 +410,9 @@ 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 MediaSource mediaSource; private final int initialWindowIndex;
private final long initialPositionMs;
private final List<MediaSource> mediaSources;
private final RenderersFactory renderersFactory; private final RenderersFactory renderersFactory;
private final DefaultTrackSelector trackSelector; private final DefaultTrackSelector trackSelector;
private final LoadControl loadControl; private final LoadControl loadControl;
...@@ -364,6 +429,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -364,6 +429,9 @@ 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 final boolean skipSettingMediaSources;
private final boolean useLazyPreparation;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private Exception exception; private Exception exception;
...@@ -373,7 +441,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -373,7 +441,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private ExoPlayerTestRunner( private ExoPlayerTestRunner(
Context context, Context context,
Clock clock, Clock clock,
MediaSource mediaSource, int initialWindowIndex,
long initialPositionMs,
List<MediaSource> mediaSources,
boolean skipSettingMediaSources,
boolean useLazyPreparation,
RenderersFactory renderersFactory, RenderersFactory renderersFactory,
DefaultTrackSelector trackSelector, DefaultTrackSelector trackSelector,
LoadControl loadControl, LoadControl loadControl,
...@@ -384,7 +456,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -384,7 +456,11 @@ 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.mediaSource = mediaSource; this.initialWindowIndex = initialWindowIndex;
this.initialPositionMs = initialPositionMs;
this.mediaSources = mediaSources;
this.skipSettingMediaSources = skipSettingMediaSources;
this.useLazyPreparation = useLazyPreparation;
this.renderersFactory = renderersFactory; this.renderersFactory = renderersFactory;
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
this.loadControl = loadControl; this.loadControl = loadControl;
...@@ -396,6 +472,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -396,6 +472,7 @@ 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,6 +511,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -434,6 +511,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
.setBandwidthMeter(bandwidthMeter) .setBandwidthMeter(bandwidthMeter)
.setAnalyticsCollector(new AnalyticsCollector(clock)) .setAnalyticsCollector(new AnalyticsCollector(clock))
.setClock(clock) .setClock(clock)
.setUseLazyPreparation(useLazyPreparation)
.setLooper(Looper.myLooper()) .setLooper(Looper.myLooper())
.build(); .build();
player.addListener(ExoPlayerTestRunner.this); player.addListener(ExoPlayerTestRunner.this);
...@@ -447,8 +525,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -447,8 +525,15 @@ 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.setMediaSource(mediaSource); if (initialWindowIndex != C.INDEX_UNSET) {
player.seekTo(initialWindowIndex, initialPositionMs);
}
if (!skipSettingMediaSources) {
player.setMediaSources(mediaSources, /* resetPosition= */ false);
}
if (doPrepare) {
player.prepare(); player.prepare();
}
} catch (Exception e) { } catch (Exception e) {
handleException(e); handleException(e);
} }
...@@ -500,12 +585,17 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -500,12 +585,17 @@ 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 equal to the provided timelines. * int)} are the same to the provided timelines. This assert differs from testing equality by not
* comparing period ids which may be different due to id mapping of child source period ids.
* *
* @param timelines A list of expected {@link Timeline}s. * @param timelines A list of expected {@link Timeline}s.
*/ */
public void assertTimelinesEqual(Timeline... timelines) { public void assertTimelinesSame(Timeline... timelines) {
assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); assertThat(this.timelines).hasSize(timelines.length);
for (int i = 0; i < timelines.length; i++) {
assertThat(new NoUidTimeline(timelines[i]))
.isEqualTo(new NoUidTimeline(this.timelines.get(i)));
}
} }
/** /**
...@@ -518,6 +608,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -518,6 +608,15 @@ 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.
...@@ -592,10 +691,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -592,10 +691,12 @@ 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) {
timelines.add(timeline);
timelineChangeReasons.add(reason); timelineChangeReasons.add(reason);
if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { timelines.add(timeline);
periodIndices.add(player.getCurrentPeriodIndex()); int currentIndex = player.getCurrentPeriodIndex();
if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) {
// Ignore timeline changes that do not change the period index.
periodIndices.add(currentIndex);
} }
} }
...@@ -606,6 +707,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -606,6 +707,7 @@ 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)) {
......
...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Format; ...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.ForwardingTimeline;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
...@@ -49,6 +50,22 @@ import java.util.List; ...@@ -49,6 +50,22 @@ import java.util.List;
*/ */
public class FakeMediaSource extends BaseMediaSource { public class FakeMediaSource extends BaseMediaSource {
/** A forwarding timeline to provide an initial timeline for fake multi window sources. */
public static class InitialTimeline extends ForwardingTimeline {
public InitialTimeline(Timeline timeline) {
super(timeline);
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
Window childWindow = timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
childWindow.isDynamic = true;
childWindow.isSeekable = false;
return childWindow;
}
}
private static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://manifest.uri")); private static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://manifest.uri"));
private static final int MANIFEST_LOAD_BYTES = 100; private static final int MANIFEST_LOAD_BYTES = 100;
...@@ -92,6 +109,19 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -92,6 +109,19 @@ public class FakeMediaSource extends BaseMediaSource {
return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null; return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null;
} }
@Nullable
@Override
public Timeline getInitialTimeline() {
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1
? null
: new InitialTimeline(timeline);
}
@Override
public boolean isSingleWindow() {
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1;
}
@Override @Override
public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
assertThat(preparedSource).isFalse(); assertThat(preparedSource).isFalse();
...@@ -249,5 +279,4 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -249,5 +279,4 @@ public class FakeMediaSource extends BaseMediaSource {
} }
return new TrackGroupArray(trackGroups); return new TrackGroupArray(trackGroups);
} }
} }
...@@ -25,8 +25,10 @@ import com.google.android.exoplayer2.PlayerMessage; ...@@ -25,8 +25,10 @@ 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}
...@@ -91,21 +93,36 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -91,21 +93,36 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
@Override @Override
public void retry() { public void retry() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/**
* @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead.
*/
@Deprecated
@Override @Override
public void prepare() { public void prepare() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/**
* @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead.
*/
@Deprecated
@Override @Override
public void prepare(MediaSource mediaSource) { public void prepare(MediaSource mediaSource) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()}
* instead.
*/
@Deprecated
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
...@@ -122,6 +139,72 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -122,6 +139,72 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
} }
@Override @Override
public void setMediaSource(MediaSource mediaSource, boolean resetPosition) {
throw new UnsupportedOperationException();
}
@Override
public void setMediaSources(List<MediaSource> mediaSources) {
throw new UnsupportedOperationException();
}
@Override
public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
throw new UnsupportedOperationException();
}
@Override
public void setMediaSources(
List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs) {
throw new UnsupportedOperationException();
}
@Override
public void addMediaSource(MediaSource mediaSource) {
throw new UnsupportedOperationException();
}
@Override
public void addMediaSource(int index, MediaSource mediaSource) {
throw new UnsupportedOperationException();
}
@Override
public void addMediaSources(List<MediaSource> mediaSources) {
throw new UnsupportedOperationException();
}
@Override
public void addMediaSources(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();
} }
...@@ -142,6 +225,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -142,6 +225,11 @@ 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();
} }
......
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