diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 44e9da1587..53fae5031c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -839,7 +839,7 @@ import java.util.concurrent.TimeoutException; long startPositionUs = playbackInfo.positionUs; if (clearPlaylist) { timeline = Timeline.EMPTY; - mediaPeriodId = playbackInfo.getDummyPeriodForEmptyTimeline(); + mediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline(); contentPositionUs = C.TIME_UNSET; startPositionUs = C.TIME_UNSET; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 56b68b3daa..2e2d592d0a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -26,6 +26,7 @@ import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -663,7 +664,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; - if (!queue.updateRepeatMode(repeatMode)) { + if (!queue.updateRepeatMode(playbackInfo.timeline, repeatMode)) { seekToCurrentPosition(/* sendDiscontinuity= */ true); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); @@ -672,7 +673,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) throws ExoPlaybackException { this.shuffleModeEnabled = shuffleModeEnabled; - if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) { + if (!queue.updateShuffleModeEnabled(playbackInfo.timeline, shuffleModeEnabled)) { seekToCurrentPosition(/* sendDiscontinuity= */ true); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); @@ -867,7 +868,14 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean seekPositionAdjusted; @Nullable Pair resolvedSeekPosition = - resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); + resolveSeekPosition( + playbackInfo.timeline, + seekPosition, + /* trySubsequentPeriods= */ true, + repeatMode, + shuffleModeEnabled, + window, + period); 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. @@ -879,7 +887,8 @@ import java.util.concurrent.atomic.AtomicBoolean; // Update the resolved seek position to take ads into account. Object periodUid = resolvedSeekPosition.first; contentPositionUs = resolvedSeekPosition.second; - periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs); + periodId = + queue.resolveMediaPeriodIdForAds(playbackInfo.timeline, periodUid, contentPositionUs); if (periodId.isAd()) { periodPositionUs = 0; seekPositionAdjusted = true; @@ -1131,7 +1140,6 @@ import java.util.concurrent.atomic.AtomicBoolean; Timeline timeline = playbackInfo.timeline; if (clearPlaylist) { timeline = playlist.clear(/* shuffleOrder= */ null); - queue.setTimeline(timeline); for (PendingMessageInfo pendingMessageInfo : pendingMessages) { pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } @@ -1144,7 +1152,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (resetPosition) { mediaPeriodId = timeline.isEmpty() - ? playbackInfo.getDummyPeriodForEmptyTimeline() + ? PlaybackInfo.getDummyPeriodForEmptyTimeline() : getDummyFirstMediaPeriodForAds(); contentPositionUs = C.TIME_UNSET; if (!mediaPeriodId.equals(playbackInfo.periodId)) { @@ -1175,11 +1183,13 @@ import java.util.concurrent.atomic.AtomicBoolean; private MediaPeriodId getDummyFirstMediaPeriodForAds() { MediaPeriodId dummyFirstMediaPeriodId = - playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); + getDummyFirstMediaPeriodId( + playbackInfo.timeline, playbackInfo.periodId, 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); + queue.resolveMediaPeriodIdForAds( + playbackInfo.timeline, dummyFirstMediaPeriodId.periodUid, /* positionUs= */ 0); } return dummyFirstMediaPeriodId; } @@ -1260,13 +1270,19 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) { if (pendingMessageInfo.resolvedPeriodUid == null) { // Position is still unresolved. Try to find window in current timeline. + @Nullable Pair periodPosition = resolveSeekPosition( + playbackInfo.timeline, new SeekPosition( pendingMessageInfo.message.getTimeline(), pendingMessageInfo.message.getWindowIndex(), C.msToUs(pendingMessageInfo.message.getPositionMs())), - /* trySubsequentPeriods= */ false); + /* trySubsequentPeriods= */ false, + repeatMode, + shuffleModeEnabled, + window, + period); if (periodPosition == null) { return false; } @@ -1509,100 +1525,53 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException { - Timeline oldTimeline = playbackInfo.timeline; - queue.setTimeline(timeline); + PositionUpdateForPlaylistChange positionUpdate = + resolvePositionForPlaylistChange( + timeline, + playbackInfo, + pendingInitialSeekPosition, + queue, + repeatMode, + shuffleModeEnabled, + window, + period); + playbackInfo = playbackInfo.copyWithTimeline(timeline); resolvePendingMessagePositions(); - 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 = - oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; - long newContentPositionUs = oldContentPositionUs; - boolean forceBufferingState = false; - if (pendingInitialSeekPosition != null) { - // Resolve initial seek position. - @Nullable - Pair periodPosition = - resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); - if (periodPosition == null) { - // 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; - } + if (!timeline.isEmpty()) { + // Retain pending seek position only while the timeline is still empty. pendingInitialSeekPosition = null; - } else if (oldTimeline.isEmpty()) { - // Resolve to default position if the old timeline is empty and no seek is requested above. - Pair defaultPosition = - getPeriodPosition( - 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. - @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 defaultPosition = - getPeriodPosition( - 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 oldPeriodId = playbackInfo.periodId; + long oldContentPositionUs = + oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; + MediaPeriodId newPeriodId = positionUpdate.periodId; + long newContentPositionUs = positionUpdate.contentPositionUs; + boolean forceBufferingState = positionUpdate.forceBufferingState; + long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; + + if (positionUpdate.endPlayback) { + if (playbackInfo.playbackState != Player.STATE_IDLE) { + setState(Player.STATE_ENDED); + } + playbackInfo = copyWithNewPosition(newPeriodId, newPositionUs, newContentPositionUs); + // Reset, but retain the playlist and new position. + resetInternal( + /* resetRenderers= */ false, + /* resetPosition= */ false, + /* releasePlaylist= */ false, + /* clearPlaylist= */ false, + /* resetError= */ true); + if (timeline.isEmpty()) { + return; + } } - 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. - MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds; 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())) { + if (!queue.updateQueuedPeriods( + timeline, rendererPositionUs, getMaxRendererReadPositionUs())) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } } else { @@ -1613,11 +1582,10 @@ import java.util.concurrent.atomic.AtomicBoolean; while (periodHolder.getNext() != null) { periodHolder = periodHolder.getNext(); if (periodHolder.info.id.equals(newPeriodId)) { - periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info); + periodHolder.info = queue.getUpdatedMediaPeriodInfo(timeline, periodHolder.info); } } } - long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; 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; @@ -1657,95 +1625,6 @@ import java.util.concurrent.atomic.AtomicBoolean; return maxReadPositionUs; } - private void handleEndOfPlaylist() { - if (playbackInfo.playbackState != Player.STATE_IDLE) { - setState(Player.STATE_ENDED); - } - // Reset, but retain the playlist so that it can still resume after a seek or be modified. - resetInternal( - /* resetRenderers= */ false, - /* resetPosition= */ true, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, - /* resetError= */ true); - } - - /** - * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the - * internal timeline. - * - * @param seekPosition The position to resolve. - * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching - * period if the original period is no longer available. - * @return The resolved position, or null if resolution was not successful. - * @throws IllegalSeekPositionException If the window index of the seek position is outside the - * bounds of the timeline. - */ - @Nullable - private Pair resolveSeekPosition( - SeekPosition seekPosition, boolean trySubsequentPeriods) { - Timeline timeline = playbackInfo.timeline; - Timeline seekTimeline = seekPosition.timeline; - if (timeline.isEmpty()) { - // We don't have a valid timeline yet, so we can't resolve the position. - return null; - } - if (seekTimeline.isEmpty()) { - // The application performed a blind seek with an empty timeline (most likely based on - // knowledge of what the future timeline will be). Use the internal timeline. - seekTimeline = timeline; - } - // Map the SeekPosition to a position in the corresponding timeline. - Pair periodPosition; - try { - periodPosition = - seekTimeline.getPeriodPosition( - window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); - } catch (IndexOutOfBoundsException e) { - // The window index of the seek position was outside the bounds of the timeline. - return null; - } - if (timeline.equals(seekTimeline)) { - // Our internal timeline is the seek timeline, so the mapped position is correct. - return periodPosition; - } - // Attempt to find the mapped period in the internal timeline. - int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); - if (periodIndex != C.INDEX_UNSET) { - // We successfully located the period in the internal timeline. - return periodPosition; - } - if (trySubsequentPeriods) { - // Try and find a subsequent period from the seek timeline in the internal timeline. - @Nullable - 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( - timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET); - } - } - // We didn't find one. Give up. - return null; - } - - /** - * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the - * current timeline. - */ - private Pair getPeriodPosition( - Timeline timeline, int windowIndex, long windowPositionUs) { - return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); - } - private void updatePeriods() throws ExoPlaybackException, IOException { if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { // We're waiting to get information about periods. @@ -2199,6 +2078,234 @@ import java.util.concurrent.atomic.AtomicBoolean; .sendToTarget(); } + private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( + Timeline timeline, + PlaybackInfo playbackInfo, + @Nullable SeekPosition pendingInitialSeekPosition, + MediaPeriodQueue queue, + @RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Timeline.Window window, + Timeline.Period period) { + if (timeline.isEmpty()) { + return new PositionUpdateForPlaylistChange( + PlaybackInfo.getDummyPeriodForEmptyTimeline(), + /* contentPositionUs= */ C.TIME_UNSET, + /* forceBufferingState= */ false, + /* endPlayback= */ true); + } + MediaPeriodId oldPeriodId = playbackInfo.periodId; + Object newPeriodUid = oldPeriodId.periodUid; + long oldContentPositionUs = + oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; + long newContentPositionUs = oldContentPositionUs; + boolean forceBufferingState = false; + boolean endPlayback = false; + if (pendingInitialSeekPosition != null) { + // Resolve initial seek position. + @Nullable + Pair periodPosition = + resolveSeekPosition( + timeline, + pendingInitialSeekPosition, + /* trySubsequentPeriods= */ true, + repeatMode, + shuffleModeEnabled, + window, + period); + if (periodPosition == null) { + // The initial seek in the empty old timeline is invalid in the new timeline. + endPlayback = true; + newPeriodUid = + getDummyFirstMediaPeriodId( + timeline, playbackInfo.periodId, shuffleModeEnabled, window, period) + .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; + } + } else if (playbackInfo.timeline.isEmpty()) { + // Resolve to default position if the old timeline is empty and no seek is requested above. + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + 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. + @Nullable + Object subsequentPeriodUid = + resolveSubsequentPeriod( + window, + period, + repeatMode, + shuffleModeEnabled, + newPeriodUid, + playbackInfo.timeline, + timeline); + if (subsequentPeriodUid == null) { + // We failed to resolve a suitable restart position but the timeline is not empty. + endPlayback = true; + newPeriodUid = + getDummyFirstMediaPeriodId( + timeline, playbackInfo.periodId, shuffleModeEnabled, window, period) + .periodUid; + newContentPositionUs = C.TIME_UNSET; + } else { + // We resolved a subsequent period. Start at the default position in the corresponding + // window. + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex, + /* windowPositionUs= */ 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(timeline, 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. + MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds; + + return new PositionUpdateForPlaylistChange( + newPeriodId, newContentPositionUs, forceBufferingState, endPlayback); + } + + /** + * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the + * internal timeline. + * + * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. + * @return The resolved position, or null if resolution was not successful. + * @throws IllegalSeekPositionException If the window index of the seek position is outside the + * bounds of the timeline. + */ + @Nullable + private static Pair resolveSeekPosition( + Timeline timeline, + SeekPosition seekPosition, + boolean trySubsequentPeriods, + @RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Timeline.Window window, + Timeline.Period period) { + Timeline seekTimeline = seekPosition.timeline; + if (timeline.isEmpty()) { + // We don't have a valid timeline yet, so we can't resolve the position. + return null; + } + if (seekTimeline.isEmpty()) { + // The application performed a blind seek with an empty timeline (most likely based on + // knowledge of what the future timeline will be). Use the internal timeline. + seekTimeline = timeline; + } + // Map the SeekPosition to a position in the corresponding timeline. + Pair periodPosition; + try { + periodPosition = + seekTimeline.getPeriodPosition( + window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); + } catch (IndexOutOfBoundsException e) { + // The window index of the seek position was outside the bounds of the timeline. + return null; + } + if (timeline.equals(seekTimeline)) { + // Our internal timeline is the seek timeline, so the mapped position is correct. + return periodPosition; + } + // Attempt to find the mapped period in the internal timeline. + int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); + if (periodIndex != C.INDEX_UNSET) { + // We successfully located the period in the internal timeline. + return periodPosition; + } + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + @Nullable + 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 timeline.getPeriodPosition( + window, + period, + timeline.getPeriodByUid(periodUid, period).windowIndex, + /* windowPositionUs= */ C.TIME_UNSET); + } + } + // We didn't find one. Give up. + return null; + } + + /** + * Returns dummy media period id for the first-to-be-played period of the current timeline. + * + * @param timeline The timeline whose first-to-be-played period needs to be found. + * @param currentMediaPeriodId The current media period id, not guaranteed to be part of {@code + * timeline}. + * @param shuffleModeEnabled Whether shuffle mode is enabled. + * @param window A writable {@link Timeline.Window}. + * @param period A writable {@link Timeline.Period}. + * @return A dummy media period id for the first-to-be-played period of the current timeline. + */ + private static MediaPeriodId getDummyFirstMediaPeriodId( + Timeline timeline, + MediaPeriodId currentMediaPeriodId, + boolean shuffleModeEnabled, + Timeline.Window window, + Timeline.Period period) { + if (timeline.isEmpty()) { + return PlaybackInfo.getDummyPeriodForEmptyTimeline(); + } + int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; + int currentPeriodIndex = timeline.getIndexOfPeriod(currentMediaPeriodId.periodUid); + long windowSequenceNumber = C.INDEX_UNSET; + if (currentPeriodIndex != C.INDEX_UNSET) { + int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; + if (firstWindowIndex == currentWindowIndex) { + // Keep window sequence number if the new position is still in the same window. + windowSequenceNumber = currentMediaPeriodId.windowSequenceNumber; + } + } + return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); + } + /** * 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. @@ -2260,6 +2367,24 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + private static final class PositionUpdateForPlaylistChange { + public final MediaPeriodId periodId; + public final long contentPositionUs; + public final boolean forceBufferingState; + public final boolean endPlayback; + + public PositionUpdateForPlaylistChange( + MediaPeriodId periodId, + long contentPositionUs, + boolean forceBufferingState, + boolean endPlayback) { + this.periodId = periodId; + this.contentPositionUs = contentPositionUs; + this.forceBufferingState = forceBufferingState; + this.endPlayback = endPlayback; + } + } + private static final class PendingMessageInfo implements Comparable { public final PlayerMessage message; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 0b2802528e..52d0ce774b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -43,7 +43,6 @@ import com.google.android.exoplayer2.util.Assertions; private final Timeline.Window window; private long nextWindowSequenceNumber; - private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; @Nullable private MediaPeriodHolder playing; @@ -57,33 +56,32 @@ import com.google.android.exoplayer2.util.Assertions; public MediaPeriodQueue() { period = new Timeline.Period(); window = new Timeline.Window(); - timeline = Timeline.EMPTY; - } - - /** - * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued - * media periods to take into account the new timeline. - */ - public void setTimeline(Timeline timeline) { - this.timeline = timeline; } /** * Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled. * If not, it is necessary to seek to the current playback position. + * + * @param timeline The current timeline. + * @param repeatMode The new repeat mode. + * @return Whether the repeat mode change has been fully handled. */ - public boolean updateRepeatMode(@RepeatMode int repeatMode) { + public boolean updateRepeatMode(Timeline timeline, @RepeatMode int repeatMode) { this.repeatMode = repeatMode; - return updateForPlaybackModeChange(); + return updateForPlaybackModeChange(timeline); } /** * Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully * handled. If not, it is necessary to seek to the current playback position. + * + * @param timeline The current timeline. + * @param shuffleModeEnabled Whether shuffling mode is enabled. + * @return Whether the shuffle mode change has been fully handled. */ - public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) { + public boolean updateShuffleModeEnabled(Timeline timeline, boolean shuffleModeEnabled) { this.shuffleModeEnabled = shuffleModeEnabled; - return updateForPlaybackModeChange(); + return updateForPlaybackModeChange(timeline); } /** Returns whether {@code mediaPeriod} is the current loading media period. */ @@ -124,7 +122,7 @@ import com.google.android.exoplayer2.util.Assertions; long rendererPositionUs, PlaybackInfo playbackInfo) { return loading == null ? getFirstMediaPeriodInfo(playbackInfo) - : getFollowingMediaPeriodInfo(loading, rendererPositionUs); + : getFollowingMediaPeriodInfo(playbackInfo.timeline, loading, rendererPositionUs); } /** @@ -286,13 +284,15 @@ import com.google.android.exoplayer2.util.Assertions; * current playback position. The method assumes that the first media period in the queue is still * consistent with the new timeline. * + * @param timeline The new timeline. * @param rendererPositionUs The current renderer position in microseconds. * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read * the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they * have read to the end. * @return Whether the timeline change has been handled completely. */ - public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) { + public boolean updateQueuedPeriods( + Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. @@ -307,9 +307,10 @@ import com.google.android.exoplayer2.util.Assertions; // The id and start position of the first period have already been verified by // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline // and isLastInPeriod flags. - newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo); + newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo); } else { - newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); + newPeriodInfo = + getFollowingMediaPeriodInfo(timeline, previousPeriodHolder, rendererPositionUs); if (newPeriodInfo == null) { // We've loaded a next media period that is not in the new timeline. return !removeAfter(previousPeriodHolder); @@ -349,13 +350,14 @@ import com.google.android.exoplayer2.util.Assertions; * account the current timeline. This method must only be called if the period is still part of * the current timeline. * + * @param timeline The current timeline used to update the media period. * @param info Media period info for a media period based on an old timeline. * @return The updated media period info for the current timeline. */ - public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) { + public MediaPeriodInfo getUpdatedMediaPeriodInfo(Timeline timeline, MediaPeriodInfo info) { MediaPeriodId id = info.id; boolean isLastInPeriod = isLastInPeriod(id); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod); timeline.getPeriodByUid(info.id.periodUid, period); long durationUs = id.isAd() @@ -378,13 +380,16 @@ import com.google.android.exoplayer2.util.Assertions; * played, returning an identifier for an ad group if one needs to be played before the specified * position, or an identifier for a content media period if not. * + * @param timeline The timeline the period is part of. * @param periodUid The uid of the timeline period to play. * @param positionUs The next content position in the period to play. * @return The identifier for the first media period to play, taking into account unplayed ads. */ - public MediaPeriodId resolveMediaPeriodIdForAds(Object periodUid, long positionUs) { - long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodUid); - return resolveMediaPeriodIdForAds(periodUid, positionUs, windowSequenceNumber); + public MediaPeriodId resolveMediaPeriodIdForAds( + Timeline timeline, Object periodUid, long positionUs) { + long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid); + return resolveMediaPeriodIdForAds( + timeline, periodUid, positionUs, windowSequenceNumber, period); } // Internal methods. @@ -394,14 +399,20 @@ import com.google.android.exoplayer2.util.Assertions; * played, returning an identifier for an ad group if one needs to be played before the specified * position, or an identifier for a content media period if not. * + * @param timeline The timeline the period is part of. * @param periodUid The uid of the timeline period to play. * @param positionUs The next content position in the period to play. * @param windowSequenceNumber The sequence number of the window in the buffered sequence of * windows this period is part of. + * @param period A scratch {@link Timeline.Period}. * @return The identifier for the first media period to play, taking into account unplayed ads. */ - private MediaPeriodId resolveMediaPeriodIdForAds( - Object periodUid, long positionUs, long windowSequenceNumber) { + private static MediaPeriodId resolveMediaPeriodIdForAds( + Timeline timeline, + Object periodUid, + long positionUs, + long windowSequenceNumber, + Timeline.Period period) { timeline.getPeriodByUid(periodUid, period); int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); if (adGroupIndex == C.INDEX_UNSET) { @@ -418,10 +429,11 @@ import com.google.android.exoplayer2.util.Assertions; * the window sequence number of an existing matching media period or by creating a new window * sequence number. * + * @param timeline The timeline the period is part of. * @param periodUid The uid of the timeline period. * @return A window sequence number for a media period created for this timeline period. */ - private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) { + private long resolvePeriodIndexToWindowSequenceNumber(Timeline timeline, Object periodUid) { int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex; if (oldFrontPeriodUid != null) { int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid); @@ -481,8 +493,10 @@ import com.google.android.exoplayer2.util.Assertions; /** * Updates the queue for any playback mode change, and returns whether the change was fully * handled. If not, it is necessary to seek to the current playback position. + * + * @param timeline The current timeline. */ - private boolean updateForPlaybackModeChange() { + private boolean updateForPlaybackModeChange(Timeline timeline) { // Find the last existing period holder that matches the new period order. MediaPeriodHolder lastValidPeriodHolder = playing; if (lastValidPeriodHolder == null) { @@ -514,7 +528,7 @@ import com.google.android.exoplayer2.util.Assertions; boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder); // Update the period info for the last holder, as it may now be the last period in the timeline. - lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); + lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(timeline, lastValidPeriodHolder.info); // If renderers may have read from a period that's been removed, it is necessary to restart. return !readingPeriodRemoved; @@ -525,13 +539,17 @@ import com.google.android.exoplayer2.util.Assertions; */ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { return getMediaPeriodInfo( - playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs); + playbackInfo.timeline, + playbackInfo.periodId, + playbackInfo.contentPositionUs, + playbackInfo.startPositionUs); } /** * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s * media period. * + * @param timeline The current timeline. * @param mediaPeriodHolder The media period holder. * @param rendererPositionUs The current renderer position in microseconds. * @return The following media period's info, or {@code null} if it is not yet possible to get the @@ -539,7 +557,7 @@ import com.google.android.exoplayer2.util.Assertions; */ @Nullable private MediaPeriodInfo getFollowingMediaPeriodInfo( - MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) { + Timeline timeline, MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) { // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod // but if the timeline is not ready to provide the next period it can't return a non-null value // until the timeline is updated. Store whether the next timeline period is ready when the @@ -571,6 +589,7 @@ import com.google.android.exoplayer2.util.Assertions; // want it to be from its default start position, so project the default start position // forward by the duration of the buffer, and start buffering from this point. contentPositionUs = C.TIME_UNSET; + @Nullable Pair defaultPosition = timeline.getPeriodPosition( window, @@ -595,8 +614,9 @@ import com.google.android.exoplayer2.util.Assertions; contentPositionUs = 0; } MediaPeriodId periodId = - resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); + resolveMediaPeriodIdForAds( + timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, period); + return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -614,6 +634,7 @@ import com.google.android.exoplayer2.util.Assertions; return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup) ? null : getMediaPeriodInfoForAd( + timeline, currentPeriodId.periodUid, adGroupIndex, nextAdIndexInAdGroup, @@ -625,6 +646,7 @@ import com.google.android.exoplayer2.util.Assertions; if (startPositionUs == C.TIME_UNSET) { // If we're transitioning from an ad group to content starting from its default position, // project the start position forward as if this were a transition to a new window. + @Nullable Pair defaultPosition = timeline.getPeriodPosition( window, @@ -638,7 +660,10 @@ import com.google.android.exoplayer2.util.Assertions; startPositionUs = defaultPosition.second; } return getMediaPeriodInfoForContent( - currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber); + timeline, + currentPeriodId.periodUid, + startPositionUs, + currentPeriodId.windowSequenceNumber); } } else { // Play the next ad group if it's available. @@ -646,6 +671,7 @@ import com.google.android.exoplayer2.util.Assertions; if (nextAdGroupIndex == C.INDEX_UNSET) { // The next ad group can't be played. Play content from the previous end position instead. return getMediaPeriodInfoForContent( + timeline, currentPeriodId.periodUid, /* startPositionUs= */ mediaPeriodInfo.durationUs, currentPeriodId.windowSequenceNumber); @@ -654,6 +680,7 @@ import com.google.android.exoplayer2.util.Assertions; return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup) ? null : getMediaPeriodInfoForAd( + timeline, currentPeriodId.periodUid, nextAdGroupIndex, adIndexInAdGroup, @@ -663,24 +690,27 @@ import com.google.android.exoplayer2.util.Assertions; } private MediaPeriodInfo getMediaPeriodInfo( - MediaPeriodId id, long contentPositionUs, long startPositionUs) { + Timeline timeline, MediaPeriodId id, long contentPositionUs, long startPositionUs) { timeline.getPeriodByUid(id.periodUid, period); if (id.isAd()) { if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { return null; } return getMediaPeriodInfoForAd( + timeline, id.periodUid, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs, id.windowSequenceNumber); } else { - return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber); + return getMediaPeriodInfoForContent( + timeline, id.periodUid, startPositionUs, id.windowSequenceNumber); } } private MediaPeriodInfo getMediaPeriodInfoForAd( + Timeline timeline, Object periodUid, int adGroupIndex, int adIndexInAdGroup, @@ -707,11 +737,11 @@ import com.google.android.exoplayer2.util.Assertions; } private MediaPeriodInfo getMediaPeriodInfoForContent( - Object periodUid, long startPositionUs, long windowSequenceNumber) { + Timeline timeline, Object periodUid, long startPositionUs, long windowSequenceNumber) { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); boolean isLastInPeriod = isLastInPeriod(id); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod); long endPositionUs = nextAdGroupIndex != C.INDEX_UNSET ? period.getAdGroupTimeUs(nextAdGroupIndex) @@ -734,7 +764,8 @@ import com.google.android.exoplayer2.util.Assertions; return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET; } - private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { + private boolean isLastInTimeline( + Timeline timeline, MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { int periodIndex = timeline.getIndexOfPeriod(id.periodUid); int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 31cae2931c..5b65e1956f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -151,35 +151,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.positionUs = positionUs; } - /** - * Returns dummy media period id for the first-to-be-played period of the current timeline. - * - * @param shuffleModeEnabled Whether shuffle mode is enabled. - * @param window A writable {@link Timeline.Window}. - * @param period A writable {@link Timeline.Period}. - * @return A dummy media period id for the first-to-be-played period of the current timeline. - */ - public MediaPeriodId getDummyFirstMediaPeriodId( - boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { - if (timeline.isEmpty()) { - return getDummyPeriodForEmptyTimeline(); - } - int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); - int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; - int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid); - long windowSequenceNumber = C.INDEX_UNSET; - if (currentPeriodIndex != C.INDEX_UNSET) { - int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; - if (firstWindowIndex == currentWindowIndex) { - // Keep window sequence number if the new position is still in the same window. - windowSequenceNumber = periodId.windowSequenceNumber; - } - } - return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); - } - /** Returns dummy period id for an empty timeline. */ - public MediaPeriodId getDummyPeriodForEmptyTimeline() { + public static MediaPeriodId getDummyPeriodForEmptyTimeline() { return DUMMY_MEDIA_PERIOD_ID; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 904702a9d5..dd21042917 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -202,7 +202,7 @@ public final class MediaPeriodQueueTest { setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( - /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); + playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); assertThat(changeHandled).isTrue(); assertThat(getQueueLength()).isEqualTo(3); @@ -228,7 +228,9 @@ public final class MediaPeriodQueueTest { setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( - /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); + playbackInfo.timeline, + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); assertThat(changeHandled).isFalse(); assertThat(getQueueLength()).isEqualTo(1); @@ -256,6 +258,7 @@ public final class MediaPeriodQueueTest { long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( + playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds); @@ -285,6 +288,7 @@ public final class MediaPeriodQueueTest { long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( + playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds); @@ -313,7 +317,9 @@ public final class MediaPeriodQueueTest { setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = mediaPeriodQueue.updateQueuedPeriods( - /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); + playbackInfo.timeline, + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); assertThat(changeHandled).isFalse(); assertThat(getQueueLength()).isEqualTo(3); @@ -332,12 +338,11 @@ public final class MediaPeriodQueueTest { Timeline timeline = createPlaylistTimeline(); periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); - mediaPeriodQueue.setTimeline(timeline); playbackInfo = new PlaybackInfo( timeline, - mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0), + mediaPeriodQueue.resolveMediaPeriodIdForAds(timeline, periodUid, /* positionUs= */ 0), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, Player.STATE_READY, @@ -361,7 +366,7 @@ public final class MediaPeriodQueueTest { SinglePeriodAdTimeline adTimeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null); - mediaPeriodQueue.setTimeline(createPlaylistTimeline()); + playbackInfo = playbackInfo.copyWithTimeline(createPlaylistTimeline()); } private Playlist.PlaylistTimeline createPlaylistTimeline() {