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 @@
([6773](https://github.com/google/ExoPlayer/issues/6773)).
* Suppress ProGuard warnings for compile-time `javax.annotation` package
([#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) ###
......
......@@ -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.DownloadRequest;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.MergingMediaSource;
......@@ -82,6 +81,8 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.List;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends AppCompatActivity
......@@ -147,7 +148,7 @@ public class PlayerActivity extends AppCompatActivity
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private MediaSource mediaSource;
private List<MediaSource> mediaSources;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper;
......@@ -349,12 +350,10 @@ public class PlayerActivity extends AppCompatActivity
private void initializePlayer() {
if (player == null) {
Intent intent = getIntent();
mediaSource = createTopLevelMediaSource(intent);
if (mediaSource == null) {
mediaSources = createTopLevelMediaSources(intent);
if (mediaSources.isEmpty()) {
return;
}
TrackSelection.Factory trackSelectionFactory;
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
......@@ -395,13 +394,12 @@ public class PlayerActivity extends AppCompatActivity
if (haveStartPosition) {
player.seekTo(startWindow, startPosition);
}
player.setMediaSource(mediaSource);
player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition);
player.prepare();
updateButtonVisibility();
}
@Nullable
private MediaSource createTopLevelMediaSource(Intent intent) {
private List<MediaSource> createTopLevelMediaSources(Intent intent) {
String action = intent.getAction();
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
......@@ -429,10 +427,10 @@ public class PlayerActivity extends AppCompatActivity
}
}
MediaSource[] mediaSources = new MediaSource[samples.length];
for (int i = 0; i < samples.length; i++) {
mediaSources[i] = createLeafMediaSource(samples[i]);
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo;
List<MediaSource> mediaSources = new ArrayList<>();
for (UriSample sample : samples) {
MediaSource mediaSource = createLeafMediaSource(sample);
Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo;
if (subtitleInfo != null) {
Format subtitleFormat =
Format.createTextSampleFormat(
......@@ -443,33 +441,30 @@ public class PlayerActivity extends AppCompatActivity
MediaSource subtitleMediaSource =
new SingleSampleMediaSource.Factory(dataSourceFactory)
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource);
mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource);
}
mediaSources.add(mediaSource);
}
MediaSource mediaSource =
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
if (seenAdsTagUri) {
if (seenAdsTagUri && mediaSources.size() == 1) {
Uri adTagUri = samples[0].adTagUri;
if (actionIsListView) {
showToast(R.string.unsupported_ads_in_concatenation);
} else {
if (!adTagUri.equals(loadedAdTagUri)) {
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
if (adsMediaSource != null) {
mediaSource = adsMediaSource;
mediaSources.set(0, adsMediaSource);
} else {
showToast(R.string.ima_not_loaded);
}
}
} else if (seenAdsTagUri && mediaSources.size() > 1) {
showToast(R.string.unsupported_ads_in_concatenation);
releaseAdsLoader();
} else {
releaseAdsLoader();
}
return mediaSource;
return mediaSources;
}
private MediaSource createLeafMediaSource(UriSample parameters) {
......@@ -557,7 +552,7 @@ public class PlayerActivity extends AppCompatActivity
debugViewHelper = null;
player.release();
player = null;
mediaSource = null;
mediaSources = null;
trackSelector = null;
}
if (adsLoader != null) {
......
......@@ -110,7 +110,6 @@ public final class CastPlayer extends BasePlayer {
private int pendingSeekCount;
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs;
private boolean waitingForInitialTimeline;
/**
* @param castContext The context from which the cast session is obtained.
......@@ -173,7 +172,6 @@ public final class CastPlayer extends BasePlayer {
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
positionMs, null);
}
......@@ -641,15 +639,13 @@ public final class CastPlayer extends BasePlayer {
private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) {
@Player.TimelineChangeReason
int reason =
waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onTimelineChanged(currentTimeline, reason)));
listener ->
listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
}
}
......
......@@ -288,7 +288,7 @@ public class ImaAdsLoaderTest {
this.adPlaybackState = adPlaybackState;
fakeExoPlayer.updateTimeline(
new SinglePeriodAdTimeline(contentTimeline, adPlaybackState),
Player.TIMELINE_CHANGE_REASON_DYNAMIC);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Override
......
......@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
......@@ -39,6 +40,7 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import java.util.List;
/**
* An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link
......@@ -139,7 +141,7 @@ public interface ExoPlayer extends Player {
private LoadControl loadControl;
private BandwidthMeter bandwidthMeter;
private Looper looper;
private AnalyticsCollector analyticsCollector;
@Nullable private AnalyticsCollector analyticsCollector;
private boolean useLazyPreparation;
private boolean buildCalled;
......@@ -172,7 +174,7 @@ public interface ExoPlayer extends Player {
new DefaultLoadControl(),
DefaultBandwidthMeter.getSingletonInstance(context),
Util.getLooper(),
new AnalyticsCollector(Clock.DEFAULT),
/* analyticsCollector= */ null,
/* useLazyPreparation= */ true,
Clock.DEFAULT);
}
......@@ -199,7 +201,7 @@ public interface ExoPlayer extends Player {
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
Looper looper,
AnalyticsCollector analyticsCollector,
@Nullable AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock) {
Assertions.checkArgument(renderers.length > 0);
......@@ -335,7 +337,15 @@ public interface ExoPlayer extends Player {
Assertions.checkState(!buildCalled);
buildCalled = true;
ExoPlayerImpl player =
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
new ExoPlayerImpl(
renderers,
trackSelector,
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
if (releaseTimeoutMs > 0) {
player.experimental_setReleaseTimeoutMs(releaseTimeoutMs);
......@@ -348,52 +358,64 @@ public interface ExoPlayer extends Player {
/** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
/**
* Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback
* has not failed or been stopped.
*/
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
void retry();
/** Prepares the player. */
void prepare();
/**
* @deprecated Use {@code setMediaSource(mediaSource, C.TIME_UNSET)} and {@link #prepare()}
* instead.
*/
/** @deprecated Use {@link #setMediaSource(MediaSource)} and {@link #prepare()} instead. */
@Deprecated
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
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
* #prepare()} has to be called immediately after calling this method.
* @param mediaSources The new {@link MediaSource MediaSources}.
*/
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}.
*/
void setMediaSource(MediaSource mediaSource);
/**
* Sets 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>
* Clears the playlist and adds the specified {@link MediaSource}.
*
* @param mediaSource The new {@link MediaSource}.
* @param startPositionMs The position in milliseconds to start playback from.
......@@ -401,6 +423,93 @@ public interface ExoPlayer extends Player {
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
* 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
......
......@@ -297,6 +297,7 @@ public final class ExoPlayerFactory {
drmSessionManager,
bandwidthMeter,
analyticsCollector,
/* useLazyPreparation= */ true,
Clock.DEFAULT,
looper);
}
......@@ -345,6 +346,13 @@ public final class ExoPlayerFactory {
BandwidthMeter bandwidthMeter,
Looper looper) {
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;
import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
......@@ -35,6 +37,9 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeoutException;
......@@ -62,19 +67,20 @@ import java.util.concurrent.TimeoutException;
private final CopyOnWriteArrayList<ListenerHolder> listeners;
private final Timeline.Period period;
private final ArrayDeque<Runnable> pendingListenerNotifications;
private final List<Playlist.MediaSourceHolder> mediaSourceHolders;
private final boolean useLazyPreparation;
@Nullable private MediaSource mediaSource;
private boolean playWhenReady;
@PlaybackSuppressionReason private int playbackSuppressionReason;
@RepeatMode private int repeatMode;
private boolean shuffleModeEnabled;
private int pendingOperationAcks;
private boolean hasPendingPrepare;
private boolean hasPendingSeek;
private boolean foregroundMode;
private int pendingSetPlaybackParametersAcks;
private PlaybackParameters playbackParameters;
private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
......@@ -91,6 +97,10 @@ import java.util.concurrent.TimeoutException;
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param analyticsCollector The {@link AnalyticsCollector} that will be used by the instance.
* @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest
* loads and other initial preparation steps happen immediately. If true, these initial
* preparations are triggered only when the player starts buffering the media.
* @param clock The {@link Clock} that will be used by the instance.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
......@@ -101,6 +111,8 @@ import java.util.concurrent.TimeoutException;
TrackSelector trackSelector,
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
@Nullable AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock,
Looper looper) {
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
......@@ -108,10 +120,13 @@ import java.util.concurrent.TimeoutException;
Assertions.checkState(renderers.length > 0);
this.renderers = Assertions.checkNotNull(renderers);
this.trackSelector = Assertions.checkNotNull(trackSelector);
this.playWhenReady = false;
this.repeatMode = Player.REPEAT_MODE_OFF;
this.shuffleModeEnabled = false;
this.listeners = new CopyOnWriteArrayList<>();
this.useLazyPreparation = useLazyPreparation;
playWhenReady = false;
repeatMode = Player.REPEAT_MODE_OFF;
shuffleModeEnabled = false;
listeners = new CopyOnWriteArrayList<>();
mediaSourceHolders = new ArrayList<>();
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
emptyTrackSelectorResult =
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
......@@ -121,6 +136,7 @@ import java.util.concurrent.TimeoutException;
playbackParameters = PlaybackParameters.DEFAULT;
seekParameters = SeekParameters.DEFAULT;
playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
maskingWindowIndex = C.INDEX_UNSET;
eventHandler =
new Handler(looper) {
@Override
......@@ -130,6 +146,9 @@ import java.util.concurrent.TimeoutException;
};
playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
pendingListenerNotifications = new ArrayDeque<>();
if (analyticsCollector != null) {
analyticsCollector.setPlayer(this);
}
internalPlayer =
new ExoPlayerImplInternal(
renderers,
......@@ -140,6 +159,7 @@ import java.util.concurrent.TimeoutException;
playWhenReady,
repeatMode,
shuffleModeEnabled,
analyticsCollector,
eventHandler,
clock);
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
......@@ -226,45 +246,182 @@ import java.util.concurrent.TimeoutException;
return playbackInfo.playbackError;
}
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
@Override
public void retry() {
if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) {
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
}
prepare();
}
@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
@Override
public void prepare(MediaSource mediaSource) {
setMediaSource(mediaSource);
prepareInternal(/* resetPosition= */ true, /* resetState= */ true);
prepare();
}
@Override
/**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()}
* instead.
*/
@Deprecated
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
setMediaSource(mediaSource);
prepareInternal(resetPosition, resetState);
setMediaSource(mediaSource, resetPosition);
prepare();
}
@Override
public void prepare() {
Assertions.checkNotNull(mediaSource);
prepareInternal(/* resetPosition= */ false, /* resetState= */ true);
public void setMediaSource(MediaSource mediaSource) {
setMediaSources(Collections.singletonList(mediaSource));
}
@Override
public void setMediaSource(MediaSource mediaSource, long startPositionMs) {
if (!getCurrentTimeline().isEmpty()) {
stop(/* reset= */ true);
setMediaSources(
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
public void setMediaSource(MediaSource mediaSource) {
this.mediaSource = mediaSource;
public void setMediaSources(List<MediaSource> mediaSources) {
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
......@@ -365,18 +522,7 @@ import java.util.concurrent.TimeoutException;
.sendToTarget();
return;
}
maskingWindowIndex = windowIndex;
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);
}
maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs);
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
}
......@@ -427,13 +573,9 @@ import java.util.concurrent.TimeoutException;
@Override
public void stop(boolean reset) {
if (reset) {
mediaSource = null;
}
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
/* resetPosition= */ reset,
/* resetState= */ reset,
/* clearPlaylist= */ reset,
/* resetError= */ reset,
/* playbackState= */ Player.STATE_IDLE);
// Trigger internal stop first before updating the playback info and notifying external
......@@ -446,7 +588,7 @@ import java.util.concurrent.TimeoutException;
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* seekProcessed= */ false);
}
......@@ -455,7 +597,6 @@ import java.util.concurrent.TimeoutException;
Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
+ ExoPlayerLibraryInfo.registeredModules() + "]");
mediaSource = null;
if (!internalPlayer.release()) {
notifyListeners(
listener ->
......@@ -466,8 +607,7 @@ import java.util.concurrent.TimeoutException;
eventHandler.removeCallbacksAndMessages(null);
playbackInfo =
getResetPlaybackInfo(
/* resetPosition= */ false,
/* resetState= */ false,
/* clearPlaylist= */ false,
/* resetError= */ false,
/* playbackState= */ Player.STATE_IDLE);
}
......@@ -493,12 +633,8 @@ import java.util.concurrent.TimeoutException;
@Override
public int getCurrentWindowIndex() {
if (shouldMaskPosition()) {
return maskingWindowIndex;
} else {
return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
.windowIndex;
}
int currentWindowIndex = getCurrentWindowIndexInternal();
return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex;
}
@Override
......@@ -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.
/* package */ void handleEvent(Message msg) {
switch (msg.what) {
case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED:
handlePlaybackInfo(
(PlaybackInfo) msg.obj,
/* playbackInfo= */ (PlaybackInfo) msg.obj,
/* operationAcks= */ msg.arg1,
/* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
/* positionDiscontinuityReason= */ msg.arg2);
......@@ -631,27 +768,13 @@ import java.util.concurrent.TimeoutException;
}
}
/* package */ void prepareInternal(boolean resetPosition, boolean resetState) {
Assertions.checkNotNull(mediaSource);
PlaybackInfo playbackInfo =
getResetPlaybackInfo(
resetPosition,
resetState,
/* resetError= */ true,
/* playbackState= */ Player.STATE_BUFFERING);
// Trigger internal prepare first before updating the playback info and notifying external
// listeners to ensure that new operations issued in the listener notifications reach the
// player after this prepare. The internal player can't change the playback info immediately
// because it uses a callback.
hasPendingPrepare = true;
pendingOperationAcks++;
internalPlayer.prepare(mediaSource, resetPosition, resetState);
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false);
private int getCurrentWindowIndexInternal() {
if (shouldMaskPosition()) {
return maskingWindowIndex;
} else {
return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
.windowIndex;
}
}
private void handlePlaybackParameters(
......@@ -685,59 +808,51 @@ import java.util.concurrent.TimeoutException;
}
if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
// Update the masking variables, which are used when the timeline becomes empty.
maskingPeriodIndex = 0;
maskingWindowIndex = 0;
maskingWindowPositionMs = 0;
resetMaskingPosition();
}
@Player.TimelineChangeReason
int timelineChangeReason =
hasPendingPrepare
? Player.TIMELINE_CHANGE_REASON_PREPARED
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
boolean seekProcessed = hasPendingSeek;
hasPendingPrepare = false;
hasPendingSeek = false;
updatePlaybackInfo(
playbackInfo,
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
seekProcessed);
}
}
private PlaybackInfo getResetPlaybackInfo(
boolean resetPosition,
boolean resetState,
boolean resetError,
@Player.State int playbackState) {
if (resetPosition) {
maskingWindowIndex = 0;
maskingPeriodIndex = 0;
maskingWindowPositionMs = 0;
boolean clearPlaylist, boolean resetError, @Player.State int playbackState) {
if (clearPlaylist) {
// Reset list of media source holders which are used for creating the masking timeline.
removeMediaSourceHolders(
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
resetMaskingPosition();
} else {
maskingWindowIndex = getCurrentWindowIndex();
maskingPeriodIndex = getCurrentPeriodIndex();
maskingWindowPositionMs = getCurrentPosition();
}
// Also reset period-based PlaybackInfo positions if resetting the state.
resetPosition = resetPosition || resetState;
MediaPeriodId mediaPeriodId =
resetPosition
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
: playbackInfo.periodId;
long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
Timeline timeline = playbackInfo.timeline;
MediaPeriodId mediaPeriodId = playbackInfo.periodId;
long contentPositionUs = playbackInfo.contentPositionUs;
long startPositionUs = playbackInfo.positionUs;
if (clearPlaylist) {
timeline = Timeline.EMPTY;
mediaPeriodId = playbackInfo.getDummyPeriodForEmptyTimeline();
contentPositionUs = C.TIME_UNSET;
startPositionUs = C.TIME_UNSET;
}
return new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
timeline,
mediaPeriodId,
startPositionUs,
contentPositionUs,
playbackState,
resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId,
startPositionUs,
/* totalBufferedDurationUs= */ 0,
......@@ -747,8 +862,8 @@ import java.util.concurrent.TimeoutException;
private void updatePlaybackInfo(
PlaybackInfo playbackInfo,
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
@DiscontinuityReason int positionDiscontinuityReason,
@TimelineChangeReason int timelineChangeReason,
boolean seekProcessed) {
boolean previousIsPlaying = isPlaying();
// Assign playback info immediately such that all getters return the right values.
......@@ -769,6 +884,218 @@ import java.util.concurrent.TimeoutException;
/* 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) {
CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
......@@ -804,7 +1131,7 @@ import java.util.concurrent.TimeoutException;
private final TrackSelector trackSelector;
private final boolean positionDiscontinuity;
private final @Player.DiscontinuityReason int positionDiscontinuityReason;
private final @Player.TimelineChangeReason int timelineChangeReason;
private final int timelineChangeReason;
private final boolean seekProcessed;
private final boolean playbackStateChanged;
private final boolean playbackErrorChanged;
......@@ -838,15 +1165,15 @@ import java.util.concurrent.TimeoutException;
playbackErrorChanged =
previousPlaybackInfo.playbackError != playbackInfo.playbackError
&& playbackInfo.playbackError != null;
timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
timelineChanged = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline);
trackSelectorResultChanged =
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
}
@Override
public void run() {
if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
if (timelineChanged) {
invokeAll(
listenerSnapshot,
listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
......
......@@ -25,11 +25,11 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
......@@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/** Implements the internal behavior of {@link ExoPlayerImpl}. */
......@@ -51,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
implements Handler.Callback,
MediaPeriod.Callback,
TrackSelector.InvalidationListener,
MediaSourceCaller,
Playlist.PlaylistInfoRefreshListener,
PlaybackParameterListener,
PlayerMessage.Sender {
......@@ -70,16 +71,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int MSG_SET_SEEK_PARAMETERS = 5;
private static final int MSG_STOP = 6;
private static final int MSG_RELEASE = 7;
private static final int MSG_REFRESH_SOURCE_INFO = 8;
private static final int MSG_PERIOD_PREPARED = 9;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
private static final int MSG_SET_REPEAT_MODE = 12;
private static final int MSG_SET_SHUFFLE_ENABLED = 13;
private static final int MSG_SET_FOREGROUND_MODE = 14;
private static final int MSG_SEND_MESSAGE = 15;
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;
private static final int MSG_PERIOD_PREPARED = 8;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
private static final int MSG_SET_REPEAT_MODE = 11;
private static final int MSG_SET_SHUFFLE_ENABLED = 12;
private static final int MSG_SET_FOREGROUND_MODE = 13;
private static final int MSG_SEND_MESSAGE = 14;
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
private static final int MSG_SET_MEDIA_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 IDLE_INTERVAL_MS = 1000;
......@@ -102,12 +108,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final ArrayList<PendingMessageInfo> pendingMessages;
private final Clock clock;
private final MediaPeriodQueue queue;
private final Playlist playlist;
@SuppressWarnings("unused")
private SeekParameters seekParameters;
private PlaybackInfo playbackInfo;
private MediaSource mediaSource;
private Renderer[] enabledRenderers;
private boolean released;
private boolean playWhenReady;
......@@ -117,8 +123,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private boolean shuffleModeEnabled;
private boolean foregroundMode;
private int pendingPrepareCount;
private SeekPosition pendingInitialSeekPosition;
@Nullable private SeekPosition pendingInitialSeekPosition;
private long rendererPositionUs;
private int nextPendingMessageIndex;
private boolean deliverPendingMessageAtStartPositionRequired;
......@@ -134,6 +139,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean playWhenReady,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
@Nullable AnalyticsCollector analyticsCollector,
Handler eventHandler,
Clock clock) {
this.renderers = renderers;
......@@ -174,16 +180,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
internalPlaybackThread.start();
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
deliverPendingMessageAtStartPositionRequired = true;
playlist = new Playlist(this);
if (analyticsCollector != null) {
playlist.setAnalyticsCollector(eventHandler, analyticsCollector);
}
}
public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) {
this.releaseTimeoutMs = releaseTimeoutMs;
}
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
handler
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
.sendToTarget();
public void prepare() {
handler.obtainMessage(MSG_PREPARE).sendToTarget();
}
public void setPlayWhenReady(boolean playWhenReady) {
......@@ -216,6 +224,50 @@ import java.util.concurrent.atomic.AtomicBoolean;
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
public synchronized void sendMessage(PlayerMessage message) {
if (released || !internalPlaybackThread.isAlive()) {
......@@ -275,13 +327,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return internalPlaybackThread.getLooper();
}
// MediaSource.MediaSourceCaller implementation.
// Playlist.PlaylistInfoRefreshListener implementation.
@Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
handler
.obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
.sendToTarget();
public void onPlaylistUpdateRequested() {
handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED);
}
// MediaPeriod.Callback implementation.
......@@ -313,14 +363,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Handler.Callback implementation.
@Override
@SuppressWarnings("unchecked")
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_PREPARE:
prepareInternal(
(MediaSource) msg.obj,
/* resetPosition= */ msg.arg1 != 0,
/* resetState= */ msg.arg2 != 0);
prepareInternal();
break;
case MSG_SET_PLAY_WHEN_READY:
setPlayWhenReadyInternal(msg.arg1 != 0);
......@@ -356,9 +404,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
case MSG_PERIOD_PREPARED:
handlePeriodPrepared((MediaPeriod) msg.obj);
break;
case MSG_REFRESH_SOURCE_INFO:
handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);
break;
case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
handleContinueLoadingRequested((MediaPeriod) msg.obj);
break;
......@@ -375,6 +420,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
sendMessageToTargetThread((PlayerMessage) msg.obj);
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:
releaseInternal();
// Return immediately to not send playback info updates after release.
......@@ -509,21 +572,77 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
pendingPrepareCount++;
private void prepareInternal() {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
resetInternal(
/* resetRenderers= */ false,
/* releaseMediaSource= */ true,
resetPosition,
resetState,
/* resetPosition= */ false,
/* releasePlaylist= */ false,
/* clearPlaylist= */ false,
/* resetError= */ true);
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
playlist.prepare(bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
private void setMediaItemsInternal(PlaylistUpdateMessage playlistUpdateMessage)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
if (playlistUpdateMessage.windowIndex != C.INDEX_UNSET) {
pendingInitialSeekPosition =
new SeekPosition(
new Playlist.PlaylistTimeline(
playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder),
playlistUpdateMessage.windowIndex,
playlistUpdateMessage.positionUs);
}
Timeline timeline =
playlist.setMediaSources(
playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder);
handlePlaylistInfoRefreshed(timeline);
}
private void addMediaItemsInternal(PlaylistUpdateMessage addMessage, int insertionIndex)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
Timeline timeline =
playlist.addMediaSources(
insertionIndex == C.INDEX_UNSET ? playlist.getSize() : insertionIndex,
addMessage.mediaSourceHolders,
addMessage.shuffleOrder);
handlePlaylistInfoRefreshed(timeline);
}
private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
Timeline timeline =
playlist.moveMediaSourceRange(
moveMediaItemsMessage.fromIndex,
moveMediaItemsMessage.toIndex,
moveMediaItemsMessage.newFromIndex,
moveMediaItemsMessage.shuffleOrder);
handlePlaylistInfoRefreshed(timeline);
}
private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
Timeline timeline = playlist.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder);
handlePlaylistInfoRefreshed(timeline);
}
private void playlistUpdateRequestedInternal() throws ExoPlaybackException {
handlePlaylistInfoRefreshed(playlist.createTimeline());
}
private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
Timeline timeline = playlist.setShuffleOrder(shuffleOrder);
handlePlaylistInfoRefreshed(timeline);
}
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
rebuffering = false;
this.playWhenReady = playWhenReady;
......@@ -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.
MediaPeriodId periodId = queue.getPlayingPeriod().info.id;
long newPositionUs =
seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true);
seekToPeriodPosition(
periodId,
playbackInfo.positionUs,
/* forceDisableRenderers= */ true,
/* forceBufferingState= */ false);
if (newPositionUs != playbackInfo.positionUs) {
playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);
if (sendDiscontinuity) {
......@@ -741,14 +864,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
long periodPositionUs;
long contentPositionUs;
boolean seekPositionAdjusted;
@Nullable
Pair<Object, Long> resolvedSeekPosition =
resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
if (resolvedSeekPosition == null) {
// 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.
periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);
periodPositionUs = C.TIME_UNSET;
periodId = getDummyFirstMediaPeriodForAds();
contentPositionUs = C.TIME_UNSET;
periodPositionUs = C.TIME_UNSET;
seekPositionAdjusted = true;
} else {
// Update the resolved seek position to take ads into account.
......@@ -765,7 +889,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
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.
pendingInitialSeekPosition = seekPosition;
} else if (periodPositionUs == C.TIME_UNSET) {
......@@ -773,9 +897,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
setState(Player.STATE_ENDED);
resetInternal(
/* resetRenderers= */ false,
/* releaseMediaSource= */ false,
/* resetPosition= */ true,
/* resetState= */ false,
/* releasePlaylist= */ false,
/* clearPlaylist= */ false,
/* resetError= */ true);
} else {
// Execute the seek in the current media periods.
......@@ -795,7 +919,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return;
}
}
newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs);
newPeriodPositionUs =
seekToPeriodPosition(
periodId,
newPeriodPositionUs,
/* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED);
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
periodPositionUs = newPeriodPositionUs;
}
......@@ -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 {
// Force disable renderers if they are reading from a period other than the one being played.
return seekToPeriodPosition(
periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod());
periodId,
periodPositionUs,
queue.getPlayingPeriod() != queue.getReadingPeriod(),
forceBufferingState);
}
private long seekToPeriodPosition(
MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers)
MediaPeriodId periodId,
long periodPositionUs,
boolean forceDisableRenderers,
boolean forceBufferingState)
throws ExoPlaybackException {
stopRenderers();
rebuffering = false;
if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) {
if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) {
setState(Player.STATE_BUFFERING);
}
......@@ -920,13 +1055,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
resetInternal(
/* resetRenderers= */ forceResetRenderers || !foregroundMode,
/* releaseMediaSource= */ true,
/* resetPosition= */ resetPositionAndState,
/* resetState= */ resetPositionAndState,
/* releasePlaylist= */ true,
/* clearPlaylist= */ resetPositionAndState,
/* resetError= */ resetPositionAndState);
playbackInfoUpdate.incrementPendingOperationAcks(
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
pendingPrepareCount = 0;
playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0);
loadControl.onStopped();
setState(Player.STATE_IDLE);
}
......@@ -934,9 +1067,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void releaseInternal() {
resetInternal(
/* resetRenderers= */ true,
/* releaseMediaSource= */ true,
/* resetPosition= */ true,
/* resetState= */ true,
/* releasePlaylist= */ true,
/* clearPlaylist= */ true,
/* resetError= */ false);
loadControl.onReleased();
setState(Player.STATE_IDLE);
......@@ -949,9 +1082,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void resetInternal(
boolean resetRenderers,
boolean releaseMediaSource,
boolean resetPosition,
boolean resetState,
boolean releasePlaylist,
boolean clearPlaylist,
boolean resetError) {
handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false;
......@@ -979,8 +1112,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (resetPosition) {
pendingInitialSeekPosition = null;
} else if (resetState) {
// When resetting the state, also reset the period-based PlaybackInfo position and convert
} else if (clearPlaylist) {
// When clearing the playlist, also reset the period-based PlaybackInfo position and convert
// existing position to initial seek instead.
resetPosition = true;
if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
......@@ -991,51 +1124,65 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
queue.clear(/* keepFrontPeriodUid= */ !resetState);
queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist);
shouldContinueLoading = false;
if (resetState) {
queue.setTimeline(Timeline.EMPTY);
Timeline timeline = playbackInfo.timeline;
if (clearPlaylist) {
timeline = playlist.clear(/* shuffleOrder= */ null);
queue.setTimeline(timeline);
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
}
pendingMessages.clear();
nextPendingMessageIndex = 0;
}
MediaPeriodId mediaPeriodId =
resetPosition
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
: playbackInfo.periodId;
MediaPeriodId mediaPeriodId = playbackInfo.periodId;
long contentPositionUs = playbackInfo.contentPositionUs;
if (resetPosition) {
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.
long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
playbackInfo =
new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
timeline,
mediaPeriodId,
startPositionUs,
contentPositionUs,
playbackInfo.playbackState,
resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false,
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId,
startPositionUs,
/* totalBufferedDurationUs= */ 0,
startPositionUs);
if (releaseMediaSource) {
if (mediaSource != null) {
mediaSource.releaseSource(/* caller= */ this);
mediaSource = null;
if (releasePlaylist) {
playlist.release();
}
}
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 {
if (message.getPositionMs() == C.TIME_UNSET) {
// If no delivery time is specified, trigger immediate message delivery.
sendMessageToTarget(message);
} else if (mediaSource == null || pendingPrepareCount > 0) {
} else if (playbackInfo.timeline.isEmpty()) {
// Still waiting for initial timeline to resolve position.
pendingMessages.add(new PendingMessageInfo(message));
} else {
......@@ -1355,86 +1502,109 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
}
mediaSource.maybeThrowSourceInfoRefreshError();
}
private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
throws ExoPlaybackException {
if (sourceRefreshInfo.source != mediaSource) {
// Stale event.
return;
playlist.maybeThrowSourceInfoRefreshError();
}
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
pendingPrepareCount = 0;
private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException {
Timeline oldTimeline = playbackInfo.timeline;
Timeline timeline = sourceRefreshInfo.timeline;
queue.setTimeline(timeline);
playbackInfo = playbackInfo.copyWithTimeline(timeline);
resolvePendingMessagePositions();
MediaPeriodId newPeriodId = playbackInfo.periodId;
if (timeline.isEmpty()) {
@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 =
playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
long newContentPositionUs = oldContentPositionUs;
boolean forceBufferingState = false;
if (pendingInitialSeekPosition != null) {
// Resolve initial seek position.
@Nullable
Pair<Object, Long> periodPosition =
resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
pendingInitialSeekPosition = null;
if (periodPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed and a suitable seek position could not be resolved in the new one.
handleSourceInfoRefreshEndedPlayback();
return;
// The initial seek in the empty old timeline is invalid in the new timeline.
handleEndOfPlaylist();
// Use the period resulting from the reset.
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;
newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs);
} else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) {
// Resolve unset start position to default position.
pendingInitialSeekPosition = null;
} else if (oldTimeline.isEmpty()) {
// Resolve to default position if the old timeline is empty and no seek is requested above.
Pair<Object, Long> defaultPosition =
getPeriodPosition(
timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second);
if (!newPeriodId.isAd()) {
// Keep unset start position if we need to play an ad first.
newContentPositionUs = defaultPosition.second;
}
} else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) {
timeline,
timeline.getFirstWindowIndex(shuffleModeEnabled),
/* windowPositionUs= */ C.TIME_UNSET);
newPeriodUid = defaultPosition.first;
newContentPositionUs = C.TIME_UNSET;
} else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) {
// The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
// window we can restart from.
Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline);
if (newPeriodUid == null) {
// We failed to resolve a suitable restart position.
handleSourceInfoRefreshEndedPlayback();
return;
}
// We resolved a subsequent period. Start at the default position in the corresponding window.
@Nullable
Object subsequentPeriodUid =
resolveSubsequentPeriod(
window, period, repeatMode, shuffleModeEnabled, newPeriodUid, oldTimeline, timeline);
if (subsequentPeriodUid == null) {
// We failed to resolve a suitable restart position but the timeline is not empty.
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 =
getPeriodPosition(
timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET);
newContentPositionUs = defaultPosition.second;
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs);
} else {
// Recheck if the current ad still needs to be played or if we need to start playing an ad.
newPeriodId =
queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs);
if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) {
timeline,
timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex,
C.TIME_UNSET);
newPeriodUid = defaultPosition.first;
newContentPositionUs = C.TIME_UNSET;
}
}
// 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
// only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential
// 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.
if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
seekToCurrentPosition(/* sendDiscontinuity= */ false);
}
} else {
// Something changed. Seek to new start position.
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
@Nullable MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
if (periodHolder != null) {
// Update the new playing media period info if it already exists.
while (periodHolder.getNext() != null) {
......@@ -1444,9 +1614,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
}
// Actually do the seek.
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);
}
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
......@@ -1477,48 +1654,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
return maxReadPositionUs;
}
private void handleSourceInfoRefreshEndedPlayback() {
private void handleEndOfPlaylist() {
if (playbackInfo.playbackState != Player.STATE_IDLE) {
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(
/* resetRenderers= */ false,
/* releaseMediaSource= */ false,
/* resetPosition= */ true,
/* resetState= */ false,
/* releasePlaylist= */ false,
/* clearPlaylist= */ false,
/* 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
* internal timeline.
*
......@@ -1566,7 +1715,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (trySubsequentPeriods) {
// Try and find a subsequent period from the seek timeline in the internal timeline.
@Nullable
Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
Object periodUid =
resolveSubsequentPeriod(
window,
period,
repeatMode,
shuffleModeEnabled,
periodPosition.first,
seekTimeline,
timeline);
if (periodUid != null) {
// We found one. Use the default position of the corresponding window.
return getPeriodPosition(
......@@ -1587,13 +1744,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void updatePeriods() throws ExoPlaybackException, IOException {
if (mediaSource == null) {
// The player has no media source yet.
return;
}
if (pendingPrepareCount > 0) {
if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) {
// We're waiting to get information about periods.
mediaSource.maybeThrowSourceInfoRefreshError();
playlist.maybeThrowSourceInfoRefreshError();
return;
}
maybeUpdateLoadingPeriod();
......@@ -1613,7 +1766,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
rendererCapabilities,
trackSelector,
loadControl.getAllocator(),
mediaSource,
playlist,
info,
emptyTrackSelectorResult);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
......@@ -1632,7 +1785,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
@Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (readingPeriodHolder == null) {
return;
}
......@@ -2008,6 +2161,44 @@ import java.util.concurrent.atomic.AtomicBoolean;
.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) {
// Build an array of formats contained by the selection.
int length = newSelection != null ? newSelection.length() : 0;
......@@ -2068,14 +2259,38 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private static final class MediaSourceRefreshInfo {
private static final class PlaylistUpdateMessage {
public final MediaSource source;
public final Timeline timeline;
private final List<Playlist.MediaSourceHolder> mediaSourceHolders;
private final ShuffleOrder shuffleOrder;
private final int windowIndex;
private final long positionUs;
public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) {
this.source = source;
this.timeline = timeline;
private PlaylistUpdateMessage(
List<Playlist.MediaSourceHolder> mediaSourceHolders,
ShuffleOrder shuffleOrder,
int windowIndex,
long positionUs) {
this.mediaSourceHolders = mediaSourceHolders;
this.shuffleOrder = shuffleOrder;
this.windowIndex = windowIndex;
this.positionUs = positionUs;
}
}
private static class MoveMediaItemsMessage {
public final int fromIndex;
public final int toIndex;
public final int newFromIndex;
public final ShuffleOrder shuffleOrder;
public MoveMediaItemsMessage(
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;
private PlaybackInfo lastPlaybackInfo;
private int operationAcks;
private boolean positionDiscontinuity;
private @DiscontinuityReason int discontinuityReason;
@DiscontinuityReason private int discontinuityReason;
public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
......@@ -2112,5 +2327,4 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.discontinuityReason = discontinuityReason;
}
}
}
......@@ -19,7 +19,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ClippingMediaPeriod;
import com.google.android.exoplayer2.source.EmptySampleStream;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
......@@ -56,7 +55,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final boolean[] mayRetainStreamFlags;
private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector;
private final MediaSource mediaSource;
private final Playlist playlist;
@Nullable private MediaPeriodHolder next;
private TrackGroupArray trackGroups;
......@@ -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 trackSelector The track selector.
* @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 emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
* renderer.
......@@ -80,13 +79,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
long rendererPositionOffsetUs,
TrackSelector trackSelector,
Allocator allocator,
MediaSource mediaSource,
Playlist playlist,
MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) {
this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
this.trackSelector = trackSelector;
this.mediaSource = mediaSource;
this.playlist = playlist;
this.uid = info.id.periodUid;
this.info = info;
this.trackGroups = TrackGroupArray.EMPTY;
......@@ -94,8 +93,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
mediaPeriod =
createMediaPeriod(
info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);
createMediaPeriod(info.id, playlist, allocator, info.startPositionUs, info.endPositionUs);
}
/**
......@@ -305,7 +303,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Releases the media period. No other method should be called after the release. */
public void release() {
disableTrackSelectionsInResult();
releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);
releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod);
}
/**
......@@ -402,11 +400,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Returns a media period corresponding to the given {@code id}. */
private static MediaPeriod createMediaPeriod(
MediaPeriodId id,
MediaSource mediaSource,
Playlist playlist,
Allocator allocator,
long startPositionUs,
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) {
mediaPeriod =
new ClippingMediaPeriod(
......@@ -417,12 +415,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
private static void releaseMediaPeriod(
long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
long endPositionUs, Playlist playlist, MediaPeriod mediaPeriod) {
try {
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} else {
mediaSource.releasePeriod(mediaPeriod);
playlist.releasePeriod(mediaPeriod);
}
} catch (RuntimeException e) {
// There's nothing we can do.
......
......@@ -19,7 +19,6 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
......@@ -134,7 +133,7 @@ import com.google.android.exoplayer2.util.Assertions;
* @param rendererCapabilities The renderer capabilities.
* @param trackSelector The track selector.
* @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 emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
* renderer.
......@@ -143,7 +142,7 @@ import com.google.android.exoplayer2.util.Assertions;
RendererCapabilities[] rendererCapabilities,
TrackSelector trackSelector,
Allocator allocator,
MediaSource mediaSource,
Playlist playlist,
MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) {
long rendererPositionOffsetUs =
......@@ -158,7 +157,7 @@ import com.google.android.exoplayer2.util.Assertions;
rendererPositionOffsetUs,
trackSelector,
allocator,
mediaSource,
playlist,
info,
emptyTrackSelectorResult);
if (loading != null) {
......
......@@ -162,7 +162,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public MediaPeriodId getDummyFirstMediaPeriodId(
boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) {
if (timeline.isEmpty()) {
return DUMMY_MEDIA_PERIOD_ID;
return getDummyPeriodForEmptyTimeline();
}
int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex;
......@@ -178,6 +178,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
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.
*
......
......@@ -381,7 +381,8 @@ public interface Player {
* {@link #onPositionDiscontinuity(int)}.
*
* @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.
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
......@@ -619,25 +620,17 @@ public interface Player {
int DISCONTINUITY_REASON_INTERNAL = 4;
/**
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link
* #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}.
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link
* #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TIMELINE_CHANGE_REASON_PREPARED,
TIMELINE_CHANGE_REASON_RESET,
TIMELINE_CHANGE_REASON_DYNAMIC
})
@IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE})
@interface TimelineChangeReason {}
/** Timeline and manifest changed as a result of a player initialization with new media. */
int TIMELINE_CHANGE_REASON_PREPARED = 0;
/** Timeline and manifest changed as a result of a player reset. */
int TIMELINE_CHANGE_REASON_RESET = 1;
/**
* Timeline or manifest changed as a result of an dynamic update introduced by the played media.
*/
int TIMELINE_CHANGE_REASON_DYNAMIC = 2;
/** Timeline changed as a result of a change of the playlist items or the order of the items. */
int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0;
/** Timeline changed as a result of a dynamic update introduced by the played media. */
int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1;
/** Returns the component of this player for audio output, or null if audio is not supported. */
@Nullable
......
......@@ -42,6 +42,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
......@@ -163,7 +164,9 @@ public class SimpleExoPlayer extends BasePlayer
* @param bandwidthMeter A {@link BandwidthMeter}.
* @param looper A {@link Looper} that must be used for all calls to the player.
* @param analyticsCollector An {@link AnalyticsCollector}.
* @param useLazyPreparation Whether 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}.
*/
public Builder(
......@@ -300,6 +303,7 @@ public class SimpleExoPlayer extends BasePlayer
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
}
......@@ -342,7 +346,6 @@ public class SimpleExoPlayer extends BasePlayer
private int audioSessionId;
private AudioAttributes audioAttributes;
private float audioVolume;
@Nullable private MediaSource mediaSource;
private List<Cue> currentCues;
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
@Nullable private CameraMotionListener cameraMotionListener;
......@@ -359,6 +362,9 @@ public class SimpleExoPlayer extends BasePlayer
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will
* collect and forward all player events.
* @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest
* loads and other initial preparation steps happen immediately. If true, these initial
* preparations are triggered only when the player starts buffering the media.
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
* Clock#DEFAULT}, unless the player is being used from a test.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
......@@ -372,6 +378,7 @@ public class SimpleExoPlayer extends BasePlayer
LoadControl loadControl,
BandwidthMeter bandwidthMeter,
AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock,
Looper looper) {
this(
......@@ -382,26 +389,14 @@ public class SimpleExoPlayer extends BasePlayer
DrmSessionManager.getDummyDrmSessionManager(),
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
}
/**
* @param context A {@link Context}.
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
* @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
* player events.
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
* Clock#DEFAULT}, unless the player is being used from a test.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
* @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl,
* BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link
* BandwidthMeter, AnalyticsCollector, boolean, Clock, Looper)} instead, and pass the {@link
* DrmSessionManager} to the {@link MediaSource} factories.
*/
@Deprecated
......@@ -413,6 +408,7 @@ public class SimpleExoPlayer extends BasePlayer
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
BandwidthMeter bandwidthMeter,
AnalyticsCollector analyticsCollector,
boolean useLazyPreparation,
Clock clock,
Looper looper) {
this.bandwidthMeter = bandwidthMeter;
......@@ -443,7 +439,15 @@ public class SimpleExoPlayer extends BasePlayer
// Build the player and associated objects.
player =
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
new ExoPlayerImpl(
renderers,
trackSelector,
loadControl,
bandwidthMeter,
analyticsCollector,
useLazyPreparation,
clock,
looper);
analyticsCollector.setPlayer(player);
addListener(analyticsCollector);
addListener(componentListener);
......@@ -1164,52 +1168,152 @@ public class SimpleExoPlayer extends BasePlayer
return player.getPlaybackError();
}
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
@Override
@SuppressWarnings("deprecation")
public void retry() {
verifyApplicationThread();
if (mediaSource != null
&& (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) {
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
}
prepare();
}
@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
@Override
@SuppressWarnings("deprecation")
public void prepare(MediaSource mediaSource) {
prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
}
@Override
/**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()}
* instead.
*/
@Deprecated
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
verifyApplicationThread();
setMediaSource(mediaSource);
prepareInternal(resetPosition, resetState);
setMediaSources(
Collections.singletonList(mediaSource),
/* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET,
/* startPositionMs= */ C.TIME_UNSET);
prepare();
}
@Override
public void prepare() {
public void setMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread();
prepareInternal(/* resetPosition= */ false, /* resetState= */ true);
analyticsCollector.resetForNewPlaylist();
player.setMediaSources(mediaSources);
}
@Override
public void setMediaSource(MediaSource mediaSource, long startPositionMs) {
public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
verifyApplicationThread();
setMediaSourceInternal(mediaSource);
player.setMediaSource(mediaSource, startPositionMs);
analyticsCollector.resetForNewPlaylist();
player.setMediaSources(mediaSources, resetPosition);
}
@Override
public void setMediaSources(
List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaSources(mediaSources, startWindowIndex, startPositionMs);
}
@Override
public void setMediaSource(MediaSource mediaSource) {
verifyApplicationThread();
setMediaSourceInternal(mediaSource);
analyticsCollector.resetForNewPlaylist();
player.setMediaSource(mediaSource);
}
@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) {
verifyApplicationThread();
@AudioFocusManager.PlayerCommand
......@@ -1286,6 +1390,7 @@ public class SimpleExoPlayer extends BasePlayer
@Override
public void setForegroundMode(boolean foregroundMode) {
verifyApplicationThread();
player.setForegroundMode(foregroundMode);
}
......@@ -1293,13 +1398,6 @@ public class SimpleExoPlayer extends BasePlayer
public void stop(boolean reset) {
verifyApplicationThread();
player.stop(reset);
if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector);
analyticsCollector.resetForNewMediaSource();
if (reset) {
mediaSource = null;
}
}
audioFocusManager.handleStop();
currentCues = Collections.emptyList();
}
......@@ -1318,10 +1416,6 @@ public class SimpleExoPlayer extends BasePlayer
}
surface = null;
}
if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector);
mediaSource = null;
}
if (isPriorityTaskManagerRegistered) {
Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);
isPriorityTaskManagerRegistered = false;
......@@ -1455,23 +1549,6 @@ public class SimpleExoPlayer extends BasePlayer
// Internal methods.
private void prepareInternal(boolean resetPosition, boolean resetState) {
Assertions.checkNotNull(mediaSource);
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
player.prepareInternal(resetPosition, resetState);
}
private void setMediaSourceInternal(MediaSource mediaSource) {
if (this.mediaSource != null) {
this.mediaSource.removeEventListener(analyticsCollector);
analyticsCollector.resetForNewMediaSource();
}
this.mediaSource = mediaSource;
this.mediaSource.addEventListener(eventHandler, analyticsCollector);
}
private void removeSurfaceCallbacks() {
if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) {
......
......@@ -135,11 +135,8 @@ public class AnalyticsCollector
}
}
/**
* Resets the analytics collector for a new media source. Should be called before the player is
* prepared with a new media source.
*/
public final void resetForNewMediaSource() {
/** Resets the analytics collector for a new playlist. */
public final void resetForNewPlaylist() {
// Copying the list is needed because onMediaPeriodReleased will modify the list.
List<MediaPeriodInfo> mediaPeriodInfos =
new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
......@@ -806,9 +803,13 @@ public class AnalyticsCollector
/** Updates the queue with a newly created media period. */
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 =
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);
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
......@@ -824,7 +825,7 @@ public class AnalyticsCollector
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
if (mediaPeriodInfo == null) {
// The media period has already been removed from the queue in resetForNewMediaSource().
// The media period has already been removed from the queue in resetForNewPlaylist().
return false;
}
mediaPeriodInfoQueue.remove(mediaPeriodInfo);
......
......@@ -617,12 +617,10 @@ public class EventLogger implements AnalyticsListener {
private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) {
switch (reason) {
case Player.TIMELINE_CHANGE_REASON_PREPARED:
return "PREPARED";
case Player.TIMELINE_CHANGE_REASON_RESET:
return "RESET";
case Player.TIMELINE_CHANGE_REASON_DYNAMIC:
return "DYNAMIC";
case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE:
return "SOURCE_UPDATE";
case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED:
return "PLAYLIST_CHANGED";
default:
return "?";
}
......
......@@ -21,15 +21,17 @@ import static org.mockito.Mockito.mock;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -50,19 +52,20 @@ public final class MediaPeriodQueueTest {
private MediaPeriodQueue mediaPeriodQueue;
private AdPlaybackState adPlaybackState;
private Timeline timeline;
private Object periodUid;
private PlaybackInfo playbackInfo;
private RendererCapabilities[] rendererCapabilities;
private TrackSelector trackSelector;
private Allocator allocator;
private MediaSource mediaSource;
private Playlist playlist;
private FakeMediaSource fakeMediaSource;
private Playlist.MediaSourceHolder mediaSourceHolder;
@Before
public void setUp() {
mediaPeriodQueue = new MediaPeriodQueue();
mediaSource = mock(MediaSource.class);
playlist = mock(Playlist.class);
rendererCapabilities = new RendererCapabilities[0];
trackSelector = mock(TrackSelector.class);
allocator = mock(Allocator.class);
......@@ -70,7 +73,7 @@ public final class MediaPeriodQueueTest {
@Test
public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {
setupTimeline(/* initialPositionUs= */ 0);
setupTimeline();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0,
/* endPositionUs= */ C.TIME_UNSET,
......@@ -81,7 +84,7 @@ public final class MediaPeriodQueueTest {
@Test
public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ 0);
setupTimeline(/* adGroupTimesUs...= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 0);
assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);
advance();
......@@ -95,10 +98,7 @@ public final class MediaPeriodQueueTest {
@Test
public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0,
/* endPositionUs= */ FIRST_AD_START_TIME_US,
......@@ -133,10 +133,7 @@ public final class MediaPeriodQueueTest {
@Test
public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
C.TIME_END_OF_SOURCE);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0,
/* endPositionUs= */ FIRST_AD_START_TIME_US,
......@@ -169,7 +166,7 @@ public final class MediaPeriodQueueTest {
@Test
public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {
setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE);
setupTimeline(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE);
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* startPositionUs= */ 0,
/* endPositionUs= */ C.TIME_END_OF_SOURCE,
......@@ -189,10 +186,7 @@ public final class MediaPeriodQueueTest {
@Test
public void
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad.
......@@ -202,10 +196,8 @@ public final class MediaPeriodQueueTest {
enqueueNext(); // Second ad.
// Change position of second ad (= change duration of content between ads).
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US + 1);
updateAdPlaybackStateAndTimeline(
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled =
......@@ -219,10 +211,7 @@ public final class MediaPeriodQueueTest {
@Test
public void
updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad.
......@@ -233,10 +222,8 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading first ad.
// Change position of first ad (= change duration of content before first ad).
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US + 1,
SECOND_AD_START_TIME_US);
updateAdPlaybackStateAndTimeline(
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled =
......@@ -250,10 +237,7 @@ public final class MediaPeriodQueueTest {
@Test
public void
updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad.
......@@ -265,10 +249,8 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading content between ads.
// Change position of second ad (= change duration of content between ads).
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US - 1000);
updateAdPlaybackStateAndTimeline(
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US;
......@@ -284,10 +266,7 @@ public final class MediaPeriodQueueTest {
@Test
public void
updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad.
......@@ -299,10 +278,8 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading content between ads.
// Change position of second ad (= change duration of content between ads).
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US - 1000);
updateAdPlaybackStateAndTimeline(
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US;
......@@ -318,10 +295,7 @@ public final class MediaPeriodQueueTest {
@Test
public void
updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US);
setupTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
enqueueNext(); // Content before first ad.
......@@ -333,10 +307,8 @@ public final class MediaPeriodQueueTest {
advanceReading(); // Reading content between ads.
// Change position of second ad (= change duration of content between ads).
setupTimeline(
/* initialPositionUs= */ 0,
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US,
SECOND_AD_START_TIME_US - 1000);
updateAdPlaybackStateAndTimeline(
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled =
......@@ -347,16 +319,25 @@ public final class MediaPeriodQueueTest {
assertThat(getQueueLength()).isEqualTo(3);
}
private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) {
private void setupTimeline(long... adGroupTimesUs) {
adPlaybackState =
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);
mediaPeriodQueue.setTimeline(timeline);
playbackInfo =
new PlaybackInfo(
timeline,
mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs),
mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0),
/* startPositionUs= */ 0,
/* contentPositionUs= */ 0,
Player.STATE_READY,
......@@ -370,6 +351,25 @@ public final class MediaPeriodQueueTest {
/* positionUs= */ 0);
}
private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) {
adPlaybackState =
new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);
updateTimeline();
}
private void updateTimeline() {
SinglePeriodAdTimeline adTimeline =
new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null);
mediaPeriodQueue.setTimeline(createPlaylistTimeline());
}
private Playlist.PlaylistTimeline createPlaylistTimeline() {
return new Playlist.PlaylistTimeline(
Collections.singleton(mediaSourceHolder),
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1));
}
private void advance() {
enqueueNext();
if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) {
......@@ -390,7 +390,7 @@ public final class MediaPeriodQueueTest {
rendererCapabilities,
trackSelector,
allocator,
mediaSource,
playlist,
getNextMediaPeriodInfo(),
new TrackSelectorResult(
new RendererConfiguration[0], new TrackSelection[0], /* info= */ null));
......@@ -422,11 +422,6 @@ public final class MediaPeriodQueueTest {
updateTimeline();
}
private void updateTimeline() {
timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
mediaPeriodQueue.setTimeline(timeline);
}
private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
long startPositionUs,
long endPositionUs,
......
......@@ -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.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.testutil.FakeTimeline;
......@@ -133,24 +132,29 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
listener.assertNoMoreEvents();
}
@Test
public void testSinglePeriod() throws Exception {
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);
populateEventIds(SINGLE_PERIOD_TIMELINE);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady */,
WINDOW_0 /* BUFFERING */,
period0 /* READY */,
period0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0 /* started */, period0 /* stopped */);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0);
......@@ -179,9 +183,14 @@ public final class AnalyticsCollectorTest {
public void testAutomaticPeriodTransition() throws Exception {
MediaSource mediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT),
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE, 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);
populateEventIds(listener.lastReportedTimeline);
......@@ -191,7 +200,8 @@ public final class AnalyticsCollectorTest {
WINDOW_0 /* BUFFERING */,
period0 /* READY */,
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_LOADING_CHANGED))
.containsExactly(period0, period0, period0, period0);
......@@ -233,8 +243,8 @@ public final class AnalyticsCollectorTest {
public void testPeriodTransitionWithRendererChange() throws Exception {
MediaSource mediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT));
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
populateEventIds(listener.lastReportedTimeline);
......@@ -246,7 +256,8 @@ public final class AnalyticsCollectorTest {
period1 /* BUFFERING */,
period1 /* READY */,
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_LOADING_CHANGED))
.containsExactly(period0, period0, period0, period0);
......@@ -286,8 +297,8 @@ public final class AnalyticsCollectorTest {
public void testSeekToOtherPeriod() throws Exception {
MediaSource mediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT));
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
ActionSchedule actionSchedule =
new ActionSchedule.Builder("AnalyticsCollectorTest")
.pause()
......@@ -308,7 +319,8 @@ public final class AnalyticsCollectorTest {
period1 /* READY */,
period1 /* setPlayWhenReady=true */,
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_SEEK_STARTED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1);
......@@ -350,9 +362,11 @@ public final class AnalyticsCollectorTest {
public void testSeekBackAfterReadingAhead() throws Exception {
MediaSource mediaSource =
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.VIDEO_FORMAT, Builder.AUDIO_FORMAT));
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
long periodDurationMs =
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
ActionSchedule actionSchedule =
......@@ -380,7 +394,8 @@ public final class AnalyticsCollectorTest {
period1Seq2 /* BUFFERING */,
period1Seq2 /* READY */,
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))
.containsExactly(period0, period1Seq2);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
......@@ -428,18 +443,28 @@ public final class AnalyticsCollectorTest {
@Test
public void testPrepareNewSource() throws Exception {
MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
MediaSource mediaSource1 =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
MediaSource mediaSource2 =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("AnalyticsCollectorTest")
.pause()
.waitForPlaybackState(Player.STATE_READY)
.prepareSource(mediaSource2)
.setMediaSources(/* resetPosition= */ false, mediaSource2)
.play()
.build();
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))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
......@@ -451,12 +476,16 @@ public final class AnalyticsCollectorTest {
period0Seq1 /* READY */,
period0Seq1 /* ENDED */);
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))
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))
.containsExactly(
period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */);
period0Seq0 /* prepared */, WINDOW_0 /* setMediaSources */, period0Seq1 /* prepared */);
assertThat(listener.getEvents(EVENT_LOAD_STARTED))
.containsExactly(
WINDOW_0 /* manifest */,
......@@ -490,19 +519,20 @@ public final class AnalyticsCollectorTest {
@Test
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 =
new ActionSchedule.Builder("AnalyticsCollectorTest")
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 0)
.prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false)
.prepare()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
populateEventIds(SINGLE_PERIOD_TIMELINE);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
......@@ -556,7 +586,7 @@ public final class AnalyticsCollectorTest {
@Test
public void testDynamicTimelineChange() throws Exception {
MediaSource childMediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
final ConcatenatingMediaSource concatenatedMediaSource =
new ConcatenatingMediaSource(childMediaSource, childMediaSource);
long periodDurationMs =
......@@ -588,7 +618,11 @@ public final class AnalyticsCollectorTest {
period1Seq0 /* setPlayWhenReady=true */,
period1Seq0 /* BUFFERING */,
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))
.containsExactly(
window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);
......@@ -642,7 +676,7 @@ public final class AnalyticsCollectorTest {
.build();
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_PROCESSED)).containsExactly(period0);
}
......@@ -709,7 +743,7 @@ public final class AnalyticsCollectorTest {
TestAnalyticsListener listener = new TestAnalyticsListener();
try {
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setMediaSources(mediaSource)
.setRenderersFactory(renderersFactory)
.setAnalyticsListener(listener)
.setActionSchedule(actionSchedule)
......@@ -731,7 +765,7 @@ public final class AnalyticsCollectorTest {
private boolean renderedFirstFrame;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
super(Builder.VIDEO_FORMAT);
super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters();
}
......@@ -789,7 +823,7 @@ public final class AnalyticsCollectorTest {
private boolean notifiedAudioSessionId;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
super(Builder.AUDIO_FORMAT);
super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
decoderCounters = new DecoderCounters();
}
......@@ -873,10 +907,12 @@ public final class AnalyticsCollectorTest {
public Timeline lastReportedTimeline;
private final List<Timeline> reportedTimelines;
private final ArrayList<ReportedEvent> reportedEvents;
public TestAnalyticsListener() {
reportedEvents = new ArrayList<>();
reportedTimelines = new ArrayList<>();
lastReportedTimeline = Timeline.EMPTY;
}
......@@ -906,6 +942,7 @@ public final class AnalyticsCollectorTest {
@Override
public void onTimelineChanged(EventTime eventTime, int reason) {
lastReportedTimeline = eventTime.timeline;
reportedTimelines.add(eventTime.timeline);
reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime));
}
......
......@@ -21,6 +21,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.IllegalSeekPositionException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.PlayerMessage;
......@@ -29,6 +30,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
......@@ -38,6 +40,8 @@ import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.List;
/** Base class for actions to perform during playback tests. */
public abstract class Action {
......@@ -114,6 +118,7 @@ public abstract class Action {
private final Integer windowIndex;
private final long positionMs;
private final boolean catchIllegalSeekException;
/**
* Action calls {@link Player#seekTo(long)}.
......@@ -125,6 +130,7 @@ public abstract class Action {
super(tag, "Seek:" + positionMs);
this.windowIndex = null;
this.positionMs = positionMs;
catchIllegalSeekException = false;
}
/**
......@@ -133,21 +139,188 @@ public abstract class Action {
* @param tag A tag to use for logging.
* @param windowIndex The window to seek to.
* @param positionMs The seek position.
* @param catchIllegalSeekException Whether {@link IllegalSeekPositionException} should be
* silently caught or not.
*/
public Seek(String tag, int windowIndex, long positionMs) {
public Seek(String tag, int windowIndex, long positionMs, boolean catchIllegalSeekException) {
super(tag, "Seek:" + positionMs);
this.windowIndex = windowIndex;
this.positionMs = positionMs;
this.catchIllegalSeekException = catchIllegalSeekException;
}
@Override
protected void doActionImpl(
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
try {
if (windowIndex == null) {
player.seekTo(positionMs);
} else {
player.seekTo(windowIndex, positionMs);
}
} catch (IllegalSeekPositionException e) {
if (!catchIllegalSeekException) {
throw e;
}
}
}
}
/** 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 {
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.setPlayWhenReady(playWhenReady);
}
}
/**
......@@ -295,52 +467,31 @@ public abstract class Action {
}
}
/** Calls {@link ExoPlayer#prepare(MediaSource)}. */
public static final class PrepareSource extends Action {
private final MediaSource mediaSource;
private final boolean resetPosition;
private final boolean resetState;
/**
* @param tag A tag to use for logging.
* @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;
/** Calls {@link ExoPlayer#prepare()}. */
public static final class Prepare extends Action {
/** @param tag A tag to use for logging. */
public Prepare(String tag) {
super(tag, "Prepare");
}
@Override
protected void doActionImpl(
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.prepare(mediaSource, resetPosition, resetState);
player.prepare();
}
}
/** Calls {@link Player#setRepeatMode(int)}. */
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 repeatMode The repeat mode.
*/
public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) {
super(tag, "SetRepeatMode:" + repeatMode);
super(tag, "SetRepeatMode: " + repeatMode);
this.repeatMode = repeatMode;
}
......@@ -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)}. */
public static final class SetShuffleModeEnabled extends Action {
......@@ -361,7 +533,7 @@ public abstract class Action {
* @param shuffleModeEnabled Whether shuffling is enabled.
*/
public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) {
super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled);
super(tag, "SetShuffleModeEnabled: " + shuffleModeEnabled);
this.shuffleModeEnabled = shuffleModeEnabled;
}
......@@ -448,7 +620,6 @@ public abstract class Action {
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
player.setPlaybackParameters(playbackParameters);
}
}
/** Throws a playback exception on the playback thread. */
......@@ -546,17 +717,34 @@ public abstract class Action {
public static final class WaitForTimelineChanged extends Action {
@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 expectedTimeline The expected timeline to wait for. If null, wait for any timeline
* change.
* @param expectedTimeline The expected timeline or null if any timeline change is relevant.
* @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");
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
......@@ -574,14 +762,16 @@ public abstract class Action {
@Override
public void onTimelineChanged(
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);
nextAction.schedule(player, trackSelector, surface, handler);
}
}
};
player.addListener(listener);
if (expectedTimeline != null && player.getCurrentTimeline().equals(expectedTimeline)) {
Timeline currentTimeline = new NoUidTimeline(player.getCurrentTimeline());
if (currentTimeline.equals(expectedTimeline)) {
player.removeListener(listener);
nextAction.schedule(player, trackSelector, surface, handler);
}
......@@ -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
* Player.EventListener#onLoadingChanged(boolean)}.
*/
......@@ -816,7 +1050,7 @@ public abstract class Action {
}
}
/** Calls {@link Runnable#run()}. */
/** Calls {@code Runnable.run()}. */
public static final class ExecuteRunnable extends Action {
private final Runnable runnable;
......
......@@ -28,10 +28,10 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface;
import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable;
import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition;
import com.google.android.exoplayer2.testutil.Action.PrepareSource;
import com.google.android.exoplayer2.testutil.Action.Seek;
import com.google.android.exoplayer2.testutil.Action.SendMessages;
import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes;
......@@ -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.SetRepeatMode;
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
import com.google.android.exoplayer2.testutil.Action.SetShuffleOrder;
import com.google.android.exoplayer2.testutil.Action.SetVideoSurface;
import com.google.android.exoplayer2.testutil.Action.Stop;
import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
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.WaitForPlaybackState;
import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity;
......@@ -172,7 +174,19 @@ public final class ActionSchedule {
* @return The builder, for convenience.
*/
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 {
}
/**
* 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.
*/
public Builder prepareSource(MediaSource mediaSource) {
return apply(new PrepareSource(tag, mediaSource));
public Builder moveMediaItem(int currentIndex, int newIndex) {
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.
*/
public Builder prepareSource(
MediaSource mediaSource, boolean resetPosition, boolean resetState) {
return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState));
public Builder removeMediaItem(int index) {
return apply(new Action.RemoveMediaItem(tag, index));
}
/**
* Schedules a remove media items action to be executed.
*
* @param fromIndex The start of the range of media items to be removed.
* @param toIndex The end of the range of media items to be removed (exclusive).
* @return The builder, for convenience.
*/
public Builder removeMediaItems(int fromIndex, int toIndex) {
return apply(new Action.RemoveMediaItems(tag, fromIndex, toIndex));
}
/**
* Schedules a prepare action to be executed.
*
* @return The builder, for convenience.
*/
public Builder prepare() {
return apply(new Action.Prepare(tag));
}
/**
* Schedules a clear media items action to be created.
*
* @return The builder. for convenience,
*/
public Builder clearMediaItems() {
return apply(new Action.ClearMediaItems(tag));
}
/**
......@@ -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.
*/
......@@ -394,18 +494,19 @@ public final class ActionSchedule {
* @return The builder, for convenience.
*/
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.
*
* @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline
* change.
* @param expectedTimeline The expected timeline.
* @param expectedReason The expected reason of the timeline change.
* @return The builder, for convenience.
*/
public Builder waitForTimelineChanged(Timeline expectedTimeline) {
return apply(new WaitForTimelineChanged(tag, expectedTimeline));
public Builder waitForTimelineChanged(
Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason));
}
/**
......@@ -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}.
*
* @return The builder, for convenience.
......@@ -484,10 +595,28 @@ public final class ActionSchedule {
/**
* 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)}.
*
* <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 {
/** Callback to be called when message arrives. */
public interface Callback {
/** Notifies about the arrival of the message. */
void onMessageArrived();
}
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. */
public abstract void handleMessage(
......@@ -499,9 +628,12 @@ public final class ActionSchedule {
}
@Override
public final void handleMessage(int messageType, @Nullable Object message)
throws ExoPlaybackException {
public final void handleMessage(int messageType, @Nullable Object message) {
handleMessage(player, messageType, message);
if (callback != null) {
hasArrived = true;
callback.onMessageArrived();
}
}
}
......
......@@ -16,11 +16,14 @@
package com.google.android.exoplayer2.testutil;
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.os.HandlerThread;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
......@@ -43,6 +46,7 @@ import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
......@@ -87,8 +91,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private Clock clock;
private Timeline timeline;
private List<MediaSource> mediaSources;
private Object manifest;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
private LoadControl loadControl;
private BandwidthMeter bandwidthMeter;
......@@ -99,19 +103,31 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private Player.EventListener eventListener;
private AnalyticsListener analyticsListener;
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
* default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link
* FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is
* not allowed after a call to {@link #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
* runner.
* @return This builder.
*/
public Builder setTimeline(Timeline timeline) {
assertThat(mediaSource).isNull();
assertThat(mediaSources).isEmpty();
assertFalse(skipSettingMediaSources);
this.timeline = timeline;
return this;
}
......@@ -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
* 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.
* @return This builder.
*/
public Builder setManifest(Object manifest) {
assertThat(mediaSource).isNull();
assertThat(mediaSources).isEmpty();
assertFalse(skipSettingMediaSources);
this.manifest = manifest;
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)}
* and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to
* {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}.
* and {@link #setManifest(Object)}. Setting media sources is not allowed after calls to {@link
* #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.
*/
public Builder setMediaSource(MediaSource mediaSource) {
public Builder setMediaSources(MediaSource... mediaSources) {
assertThat(timeline).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;
}
......@@ -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
* periods and for setting up a {@link FakeRenderer}. The default value is a single {@link
* #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source
* with {@link #setMediaSource(MediaSource)} and renderers with {@link
* with {@link #setMediaSources(MediaSource...)} and renderers with {@link
* #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set.
*
* @param supportedFormats A list of supported {@link Format}s.
......@@ -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
* 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.
* @return This builder.
......@@ -321,11 +380,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (clock == null) {
clock = new AutoAdvancingFakeClock();
}
if (mediaSource == null) {
if (mediaSources.isEmpty() && !skipSettingMediaSources) {
if (timeline == null) {
timeline = new FakeTimeline(/* windowCount= */ 1, manifest);
}
mediaSource = new FakeMediaSource(timeline, supportedFormats);
mediaSources.add(new FakeMediaSource(timeline, supportedFormats));
}
if (expectedPlayerEndedCount == null) {
expectedPlayerEndedCount = 1;
......@@ -333,7 +392,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
return new ExoPlayerTestRunner(
context,
clock,
mediaSource,
initialWindowIndex,
initialPositionMs,
mediaSources,
skipSettingMediaSources,
useLazyPreparation,
renderersFactory,
trackSelector,
loadControl,
......@@ -347,7 +410,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final Context context;
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 DefaultTrackSelector trackSelector;
private final LoadControl loadControl;
......@@ -364,6 +429,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final ArrayList<Integer> timelineChangeReasons;
private final ArrayList<Integer> periodIndices;
private final ArrayList<Integer> discontinuityReasons;
private final ArrayList<Integer> playbackStates;
private final boolean skipSettingMediaSources;
private final boolean useLazyPreparation;
private SimpleExoPlayer player;
private Exception exception;
......@@ -373,7 +441,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private ExoPlayerTestRunner(
Context context,
Clock clock,
MediaSource mediaSource,
int initialWindowIndex,
long initialPositionMs,
List<MediaSource> mediaSources,
boolean skipSettingMediaSources,
boolean useLazyPreparation,
RenderersFactory renderersFactory,
DefaultTrackSelector trackSelector,
LoadControl loadControl,
......@@ -384,7 +456,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
int expectedPlayerEndedCount) {
this.context = context;
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.trackSelector = trackSelector;
this.loadControl = loadControl;
......@@ -396,6 +472,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
this.timelineChangeReasons = new ArrayList<>();
this.periodIndices = new ArrayList<>();
this.discontinuityReasons = new ArrayList<>();
this.playbackStates = new ArrayList<>();
this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);
this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
this.playerThread = new HandlerThread("ExoPlayerTest thread");
......@@ -434,6 +511,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
.setBandwidthMeter(bandwidthMeter)
.setAnalyticsCollector(new AnalyticsCollector(clock))
.setClock(clock)
.setUseLazyPreparation(useLazyPreparation)
.setLooper(Looper.myLooper())
.build();
player.addListener(ExoPlayerTestRunner.this);
......@@ -447,8 +525,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (actionSchedule != null) {
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();
}
} catch (Exception e) {
handleException(e);
}
......@@ -500,12 +585,17 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
/**
* 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.
*/
public void assertTimelinesEqual(Timeline... timelines) {
assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder();
public void assertTimelinesSame(Timeline... timelines) {
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
}
/**
* Asserts that the playback states reported by {@link
* Player.EventListener#onPlayerStateChanged(boolean, int)} are equal to the provided playback
* states.
*/
public void assertPlaybackStatesEqual(Integer... states) {
assertThat(playbackStates).containsExactlyElementsIn(Arrays.asList(states)).inOrder();
}
/**
* Asserts that the last track group array reported by {@link
* Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the
* provided track group array.
......@@ -592,10 +691,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
@Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
timelines.add(timeline);
timelineChangeReasons.add(reason);
if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) {
periodIndices.add(player.getCurrentPeriodIndex());
timelines.add(timeline);
int currentIndex = player.getCurrentPeriodIndex();
if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) {
// Ignore timeline changes that do not change the period index.
periodIndices.add(currentIndex);
}
}
......@@ -606,6 +707,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
@Override
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
playbackStates.add(playbackState);
playerWasPrepared |= playbackState != Player.STATE_IDLE;
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playerWasPrepared)) {
......
......@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
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.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
......@@ -49,6 +50,22 @@ import java.util.List;
*/
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 int MANIFEST_LOAD_BYTES = 100;
......@@ -92,6 +109,19 @@ public class FakeMediaSource extends BaseMediaSource {
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
public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
assertThat(preparedSource).isFalse();
......@@ -249,5 +279,4 @@ public class FakeMediaSource extends BaseMediaSource {
}
return new TrackGroupArray(trackGroups);
}
}
......@@ -25,8 +25,10 @@ import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import java.util.List;
/**
* An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException}
......@@ -91,21 +93,36 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
throw new UnsupportedOperationException();
}
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
@Override
public void retry() {
throw new UnsupportedOperationException();
}
/**
* @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead.
*/
@Deprecated
@Override
public void prepare() {
throw new UnsupportedOperationException();
}
/**
* @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead.
*/
@Deprecated
@Override
public void prepare(MediaSource mediaSource) {
throw new UnsupportedOperationException();
}
/**
* @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()}
* instead.
*/
@Deprecated
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
throw new UnsupportedOperationException();
......@@ -122,6 +139,72 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
}
@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) {
throw new UnsupportedOperationException();
}
......@@ -142,6 +225,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
}
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
throw new UnsupportedOperationException();
}
@Override
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
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