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 e807f3f169..57da40fc68 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 @@ -70,12 +70,13 @@ import java.util.concurrent.TimeoutException; private final List mediaSourceHolders; private final boolean useLazyPreparation; - private boolean playWhenReady; - @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; private boolean hasPendingSeek; + private boolean hasPendingDiscontinuity; + @DiscontinuityReason private int pendingDiscontinuityReason; + @PlayWhenReadyChangeReason private int pendingPlayWhenReadyChangeReason; private boolean foregroundMode; private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; @@ -121,9 +122,7 @@ import java.util.concurrent.TimeoutException; this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); this.useLazyPreparation = useLazyPreparation; - playWhenReady = false; repeatMode = Player.REPEAT_MODE_OFF; - shuffleModeEnabled = false; listeners = new CopyOnWriteArrayList<>(); mediaSourceHolders = new ArrayList<>(); shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); @@ -135,7 +134,6 @@ import java.util.concurrent.TimeoutException; period = new Timeline.Period(); playbackParameters = PlaybackParameters.DEFAULT; seekParameters = SeekParameters.DEFAULT; - playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; maskingWindowIndex = C.INDEX_UNSET; eventHandler = new Handler(looper) { @@ -156,7 +154,6 @@ import java.util.concurrent.TimeoutException; emptyTrackSelectorResult, loadControl, bandwidthMeter, - playWhenReady, repeatMode, shuffleModeEnabled, analyticsCollector, @@ -237,7 +234,7 @@ import java.util.concurrent.TimeoutException; @Override @PlaybackSuppressionReason public int getPlaybackSuppressionReason() { - return playbackSuppressionReason; + return playbackInfo.playbackSuppressionReason; } @Deprecated @@ -283,6 +280,7 @@ import java.util.concurrent.TimeoutException; /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, /* seekProcessed= */ false); } @@ -443,40 +441,36 @@ import java.util.concurrent.TimeoutException; @PlaybackSuppressionReason int playbackSuppressionReason, @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { boolean oldIsPlaying = isPlaying(); - boolean oldInternalPlayWhenReady = - this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; - boolean internalPlayWhenReady = - playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; - if (oldInternalPlayWhenReady != internalPlayWhenReady) { - internalPlayer.setPlayWhenReady(internalPlayWhenReady); + boolean playWhenReadyChanged = playbackInfo.playWhenReady != playWhenReady; + boolean suppressionReasonChanged = + playbackInfo.playbackSuppressionReason != playbackSuppressionReason; + if (!playWhenReadyChanged && !suppressionReasonChanged) { + return; } - boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; - boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason; - this.playWhenReady = playWhenReady; - this.playbackSuppressionReason = playbackSuppressionReason; + pendingOperationAcks++; + playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); + internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason); boolean isPlaying = isPlaying(); boolean isPlayingChanged = oldIsPlaying != isPlaying; - if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) { - int playbackState = playbackInfo.playbackState; - notifyListeners( - listener -> { - if (playWhenReadyChanged) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); - } - if (suppressionReasonChanged) { - listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); - } - if (isPlayingChanged) { - listener.onIsPlayingChanged(isPlaying); - } - }); - } + int playbackState = playbackInfo.playbackState; + notifyListeners( + listener -> { + if (playWhenReadyChanged) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); + } + if (suppressionReasonChanged) { + listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); + } + if (isPlayingChanged) { + listener.onIsPlayingChanged(isPlaying); + } + }); } @Override public boolean getPlayWhenReady() { - return playWhenReady; + return playbackInfo.playWhenReady; } @Override @@ -601,6 +595,7 @@ import java.util.concurrent.TimeoutException; /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, /* seekProcessed= */ false); } @@ -763,14 +758,9 @@ 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= */ (PlaybackInfo) msg.obj, - /* operationAcks= */ msg.arg1, - /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, - /* positionDiscontinuityReason= */ msg.arg2); + handlePlaybackInfo((ExoPlayerImplInternal.PlaybackInfoUpdate) msg.obj); break; case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0); @@ -802,24 +792,31 @@ import java.util.concurrent.TimeoutException; } } - private void handlePlaybackInfo( - PlaybackInfo playbackInfo, - int operationAcks, - boolean positionDiscontinuity, - @DiscontinuityReason int positionDiscontinuityReason) { - pendingOperationAcks -= operationAcks; + private void handlePlaybackInfo(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate) { + pendingOperationAcks -= playbackInfoUpdate.operationAcks; + if (playbackInfoUpdate.positionDiscontinuity) { + hasPendingDiscontinuity = true; + pendingDiscontinuityReason = playbackInfoUpdate.discontinuityReason; + } + if (playbackInfoUpdate.hasPlayWhenReadyChangeReason) { + pendingPlayWhenReadyChangeReason = playbackInfoUpdate.playWhenReadyChangeReason; + } if (pendingOperationAcks == 0) { - if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { + if (!this.playbackInfo.timeline.isEmpty() + && playbackInfoUpdate.playbackInfo.timeline.isEmpty()) { // Update the masking variables, which are used when the timeline becomes empty. resetMaskingPosition(); } boolean seekProcessed = hasPendingSeek; + boolean positionDiscontinuity = hasPendingDiscontinuity; hasPendingSeek = false; + hasPendingDiscontinuity = false; updatePlaybackInfo( - playbackInfo, + playbackInfoUpdate.playbackInfo, positionDiscontinuity, - positionDiscontinuityReason, + pendingDiscontinuityReason, TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + pendingPlayWhenReadyChangeReason, seekProcessed); } } @@ -856,6 +853,8 @@ import java.util.concurrent.TimeoutException; clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, + playbackInfo.playWhenReady, + playbackInfo.playbackSuppressionReason, positionUs, /* totalBufferedDurationUs= */ 0, positionUs); @@ -866,12 +865,11 @@ import java.util.concurrent.TimeoutException; boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason, @TimelineChangeReason int timelineChangeReason, + @PlayWhenReadyChangeReason int playWhenReadyChangeReason, boolean seekProcessed) { - boolean previousIsPlaying = isPlaying(); // Assign playback info immediately such that all getters return the right values. PlaybackInfo previousPlaybackInfo = this.playbackInfo; this.playbackInfo = playbackInfo; - boolean isPlaying = isPlaying(); notifyListeners( new PlaybackInfoUpdate( playbackInfo, @@ -881,9 +879,8 @@ import java.util.concurrent.TimeoutException; positionDiscontinuity, positionDiscontinuityReason, timelineChangeReason, - seekProcessed, - playWhenReady, - /* isPlayingChanged= */ previousIsPlaying != isPlaying)); + playWhenReadyChangeReason, + seekProcessed)); } @SuppressWarnings("deprecation") @@ -1136,16 +1133,18 @@ import java.util.concurrent.TimeoutException; private final CopyOnWriteArrayList listenerSnapshot; private final TrackSelector trackSelector; private final boolean positionDiscontinuity; - private final @Player.DiscontinuityReason int positionDiscontinuityReason; - private final int timelineChangeReason; + @DiscontinuityReason private final int positionDiscontinuityReason; + @TimelineChangeReason private final int timelineChangeReason; + @PlayWhenReadyChangeReason private final int playWhenReadyChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; private final boolean playbackErrorChanged; private final boolean timelineChanged; private final boolean isLoadingChanged; private final boolean trackSelectorResultChanged; - private final boolean playWhenReady; private final boolean isPlayingChanged; + private final boolean playWhenReadyChanged; + private final boolean playbackSuppressionReasonChanged; public PlaybackInfoUpdate( PlaybackInfo playbackInfo, @@ -1155,18 +1154,16 @@ import java.util.concurrent.TimeoutException; boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason, @TimelineChangeReason int timelineChangeReason, - boolean seekProcessed, - boolean playWhenReady, - boolean isPlayingChanged) { + @PlayWhenReadyChangeReason int playWhenReadyChangeReason, + boolean seekProcessed) { this.playbackInfo = playbackInfo; this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners); this.trackSelector = trackSelector; this.positionDiscontinuity = positionDiscontinuity; this.positionDiscontinuityReason = positionDiscontinuityReason; this.timelineChangeReason = timelineChangeReason; + this.playWhenReadyChangeReason = playWhenReadyChangeReason; this.seekProcessed = seekProcessed; - this.playWhenReady = playWhenReady; - this.isPlayingChanged = isPlayingChanged; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; playbackErrorChanged = previousPlaybackInfo.playbackError != playbackInfo.playbackError @@ -1175,6 +1172,10 @@ import java.util.concurrent.TimeoutException; timelineChanged = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline); trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; + playWhenReadyChanged = previousPlaybackInfo.playWhenReady != playbackInfo.playWhenReady; + playbackSuppressionReasonChanged = + previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason; + isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo); } @SuppressWarnings("deprecation") @@ -1202,30 +1203,49 @@ import java.util.concurrent.TimeoutException; playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections)); } if (isLoadingChanged) { + invokeAll( + listenerSnapshot, listener -> listener.onIsLoadingChanged(playbackInfo.isLoading)); + } + if (playbackStateChanged || playWhenReadyChanged) { invokeAll( listenerSnapshot, - listener -> { - listener.onIsLoadingChanged(playbackInfo.isLoading); - }); + listener -> + listener.onPlayerStateChanged( + playbackInfo.playWhenReady, playbackInfo.playbackState)); } if (playbackStateChanged) { invokeAll( listenerSnapshot, - listener -> { - listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); - listener.onPlaybackStateChanged(playbackInfo.playbackState); - }); + listener -> listener.onPlaybackStateChanged(playbackInfo.playbackState)); } - if (isPlayingChanged) { + if (playWhenReadyChanged) { invokeAll( listenerSnapshot, listener -> - listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY)); + listener.onPlayWhenReadyChanged( + playbackInfo.playWhenReady, playWhenReadyChangeReason)); + } + if (playbackSuppressionReasonChanged) { + invokeAll( + listenerSnapshot, + listener -> + listener.onPlaybackSuppressionReasonChanged( + playbackInfo.playbackSuppressionReason)); + } + if (isPlayingChanged) { + invokeAll( + listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo))); } if (seekProcessed) { invokeAll(listenerSnapshot, EventListener::onSeekProcessed); } } + + private static boolean isPlaying(PlaybackInfo playbackInfo) { + return playbackInfo.playbackState == Player.STATE_READY + && playbackInfo.playWhenReady + && playbackInfo.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + } } private static void invokeAll( 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 e93d336ab9..a81deb1ba5 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,8 @@ 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.PlayWhenReadyChangeReason; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaPeriod; @@ -107,7 +109,6 @@ import java.util.concurrent.atomic.AtomicBoolean; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; - private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList pendingMessages; private final Clock clock; private final MediaPeriodQueue queue; @@ -117,9 +118,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private SeekParameters seekParameters; private PlaybackInfo playbackInfo; + private PlaybackInfoUpdate playbackInfoUpdate; private Renderer[] enabledRenderers; private boolean released; - private boolean playWhenReady; private boolean pauseAtEndOfWindow; private boolean pendingPauseAtEndOfPeriod; private boolean rebuffering; @@ -141,7 +142,6 @@ import java.util.concurrent.atomic.AtomicBoolean; TrackSelectorResult emptyTrackSelectorResult, LoadControl loadControl, BandwidthMeter bandwidthMeter, - boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, @Nullable AnalyticsCollector analyticsCollector, @@ -152,7 +152,6 @@ import java.util.concurrent.atomic.AtomicBoolean; this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; - this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; @@ -164,7 +163,7 @@ import java.util.concurrent.atomic.AtomicBoolean; seekParameters = SeekParameters.DEFAULT; playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); - playbackInfoUpdate = new PlaybackInfoUpdate(); + playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); @@ -198,8 +197,11 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.obtainMessage(MSG_PREPARE).sendToTarget(); } - public void setPlayWhenReady(boolean playWhenReady) { - handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); + public void setPlayWhenReady( + boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { + handler + .obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason) + .sendToTarget(); } public void setPauseAtEndOfWindow(boolean pauseAtEndOfWindow) { @@ -381,7 +383,11 @@ import java.util.concurrent.atomic.AtomicBoolean; prepareInternal(); break; case MSG_SET_PLAY_WHEN_READY: - setPlayWhenReadyInternal(msg.arg1 != 0); + setPlayWhenReadyInternal( + /* playWhenReady= */ msg.arg1 != 0, + /* playbackSuppressionReason= */ msg.arg2, + /* operationAck= */ true, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); break; case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); @@ -571,17 +577,10 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void maybeNotifyPlaybackInfoChanged() { - if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { - eventHandler - .obtainMessage( - MSG_PLAYBACK_INFO_CHANGED, - playbackInfoUpdate.operationAcks, - playbackInfoUpdate.positionDiscontinuity - ? playbackInfoUpdate.discontinuityReason - : C.INDEX_UNSET, - playbackInfo) - .sendToTarget(); - playbackInfoUpdate.reset(playbackInfo); + playbackInfoUpdate.setPlaybackInfo(playbackInfo); + if (playbackInfoUpdate.hasPendingChange) { + eventHandler.obtainMessage(MSG_PLAYBACK_INFO_CHANGED, playbackInfoUpdate).sendToTarget(); + playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); } } @@ -656,10 +655,17 @@ import java.util.concurrent.atomic.AtomicBoolean; handlePlaylistInfoRefreshed(timeline); } - private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { + private void setPlayWhenReadyInternal( + boolean playWhenReady, + @PlaybackSuppressionReason int playbackSuppressionReason, + boolean operationAck, + @Player.PlayWhenReadyChangeReason int reason) + throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0); + playbackInfoUpdate.setPlayWhenReadyChangeReason(reason); + playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); rebuffering = false; - this.playWhenReady = playWhenReady; - if (!playWhenReady) { + if (!shouldPlayWhenReady()) { stopRenderers(); updatePlaybackPositions(); } else { @@ -840,7 +846,12 @@ import java.util.concurrent.atomic.AtomicBoolean; || playingPeriodDurationUs <= playbackInfo.positionUs); if (finishedRendering && pendingPauseAtEndOfPeriod) { pendingPauseAtEndOfPeriod = false; - setPlayWhenReadyInternal(false); + // TODO: Add new change reason for timed pause requests. + setPlayWhenReadyInternal( + /* playWhenReady= */ false, + playbackInfo.playbackSuppressionReason, + /* operationAck= */ false, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); } if (finishedRendering && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); @@ -848,12 +859,12 @@ import java.util.concurrent.atomic.AtomicBoolean; } else if (playbackInfo.playbackState == Player.STATE_BUFFERING && shouldTransitionToReadyState(renderersAllowPlayback)) { setState(Player.STATE_READY); - if (playWhenReady) { + if (shouldPlayWhenReady()) { startRenderers(); } } else if (playbackInfo.playbackState == Player.STATE_READY && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) { - rebuffering = playWhenReady; + rebuffering = shouldPlayWhenReady(); setState(Player.STATE_BUFFERING); stopRenderers(); } @@ -864,7 +875,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY) || playbackInfo.playbackState == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { @@ -906,7 +917,7 @@ import java.util.concurrent.atomic.AtomicBoolean; periodId = firstPeriodAndPosition.first; periodPositionUs = firstPeriodAndPosition.second; requestedContentPosition = C.TIME_UNSET; - seekPositionAdjusted = true; + seekPositionAdjusted = !playbackInfo.timeline.isEmpty(); } else { // Update the resolved seek position to take ads into account. Object periodUid = resolvedSeekPosition.first; @@ -1209,6 +1220,8 @@ import java.util.concurrent.atomic.AtomicBoolean; resetTrackInfo ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetTrackInfo ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, + playbackInfo.playWhenReady, + playbackInfo.playbackSuppressionReason, startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs); @@ -1806,7 +1819,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private boolean shouldAdvancePlayingPeriod() { - if (!playWhenReady) { + if (!shouldPlayWhenReady()) { return false; } if (pendingPauseAtEndOfPeriod) { @@ -2048,7 +2061,7 @@ import java.util.concurrent.atomic.AtomicBoolean; TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex); Format[] formats = getFormats(newSelection); // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY; + boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !wasRendererEnabled && playing; // Enable the renderer. @@ -2121,6 +2134,11 @@ import java.util.concurrent.atomic.AtomicBoolean; .sendToTarget(); } + private boolean shouldPlayWhenReady() { + return playbackInfo.playWhenReady + && playbackInfo.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE; + } + private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( Timeline timeline, PlaybackInfo playbackInfo, @@ -2606,27 +2624,31 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private static final class PlaybackInfoUpdate { + /* package */ static final class PlaybackInfoUpdate { - private PlaybackInfo lastPlaybackInfo; - private int operationAcks; - private boolean positionDiscontinuity; - @DiscontinuityReason private int discontinuityReason; + private boolean hasPendingChange; - public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { - return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; - } + public PlaybackInfo playbackInfo; + public int operationAcks; + public boolean positionDiscontinuity; + @DiscontinuityReason public int discontinuityReason; + public boolean hasPlayWhenReadyChangeReason; + @PlayWhenReadyChangeReason public int playWhenReadyChangeReason; - public void reset(PlaybackInfo playbackInfo) { - lastPlaybackInfo = playbackInfo; - operationAcks = 0; - positionDiscontinuity = false; + public PlaybackInfoUpdate(PlaybackInfo playbackInfo) { + this.playbackInfo = playbackInfo; } public void incrementPendingOperationAcks(int operationAcks) { + hasPendingChange |= operationAcks > 0; this.operationAcks += operationAcks; } + public void setPlaybackInfo(PlaybackInfo playbackInfo) { + hasPendingChange |= this.playbackInfo != playbackInfo; + this.playbackInfo = playbackInfo; + } + public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { if (positionDiscontinuity && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { @@ -2635,8 +2657,16 @@ import java.util.concurrent.atomic.AtomicBoolean; Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); return; } + hasPendingChange = true; positionDiscontinuity = true; this.discontinuityReason = discontinuityReason; } + + public void setPlayWhenReadyChangeReason( + @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { + hasPendingChange = true; + this.hasPlayWhenReadyChangeReason = true; + this.playWhenReadyChangeReason = playWhenReadyChangeReason; + } } } 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 d545ce4905..f183af0d8c 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -58,6 +59,10 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final TrackSelectorResult trackSelectorResult; /** The {@link MediaPeriodId} of the currently loading media period in the {@link #timeline}. */ public final MediaPeriodId loadingMediaPeriodId; + /** Whether playback should proceed when {@link #playbackState} == {@link Player#STATE_READY}. */ + public final boolean playWhenReady; + /** Reason why playback is suppressed even though {@link #playWhenReady} is {@code true}. */ + @PlaybackSuppressionReason public final int playbackSuppressionReason; /** * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start @@ -94,6 +99,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; TrackGroupArray.EMPTY, emptyTrackSelectorResult, DUMMY_MEDIA_PERIOD_ID, + /* playWhenReady= */ false, + Player.PLAYBACK_SUPPRESSION_REASON_NONE, /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0); @@ -124,6 +131,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult, MediaPeriodId loadingMediaPeriodId, + boolean playWhenReady, + @PlaybackSuppressionReason int playbackSuppressionReason, long bufferedPositionUs, long totalBufferedDurationUs, long positionUs) { @@ -136,6 +145,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.trackGroups = trackGroups; this.trackSelectorResult = trackSelectorResult; this.loadingMediaPeriodId = loadingMediaPeriodId; + this.playWhenReady = playWhenReady; + this.playbackSuppressionReason = playbackSuppressionReason; this.bufferedPositionUs = bufferedPositionUs; this.totalBufferedDurationUs = totalBufferedDurationUs; this.positionUs = positionUs; @@ -177,6 +188,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -200,6 +213,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -223,6 +238,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -246,6 +263,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -269,6 +288,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); @@ -292,6 +313,37 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; trackGroups, trackSelectorResult, loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new information about whether playback should proceed when ready. + * + * @param playWhenReady Whether playback should proceed when {@link #playbackState} == {@link + * Player#STATE_READY}. + * @param playbackSuppressionReason Reason why playback is suppressed even though {@link + * #playWhenReady} is {@code true}. + * @return Copied playback info with new information. + */ + @CheckResult + public PlaybackInfo copyWithPlayWhenReady( + boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { + return new PlaybackInfo( + timeline, + periodId, + requestedContentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, bufferedPositionUs, totalBufferedDurationUs, positionUs); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index e951c67725..ea0e18ca18 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -548,6 +548,7 @@ public final class ExoPlayerTest { // only on seek processed callback). .seek(5) .seek(60) + .waitForSeekProcessed() .play() .build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); @@ -2790,6 +2791,7 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_READY) .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForSeekProcessed() .play() .build(); List trackGroupsList = new ArrayList<>(); 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 5b3b321e09..fb424cc92c 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 @@ -421,6 +421,8 @@ public final class MediaPeriodQueueTest { /* trackGroups= */ null, /* trackSelectorResult= */ null, /* loadingMediaPeriodId= */ null, + /* playWhenReady= */ false, + Player.PLAYBACK_SUPPRESSION_REASON_NONE, /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index f779b70637..ac0c444769 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -315,6 +315,7 @@ public final class AnalyticsCollectorTest { .pause() .waitForPlaybackState(Player.STATE_READY) .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForSeekProcessed() .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); @@ -327,8 +328,8 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* setPlayWhenReady=false */, period0 /* READY */, period1 /* BUFFERING */, - period1 /* READY */, period1 /* setPlayWhenReady=true */, + period1 /* READY */, period1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); @@ -466,6 +467,9 @@ public final class AnalyticsCollectorTest { .pause() .waitForPlaybackState(Player.STATE_READY) .setMediaSources(/* resetPosition= */ false, mediaSource2) + .waitForTimelineChanged() + // Wait until loading started to prevent flakiness caused by loading finishing too fast. + .waitForIsLoading(true) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); @@ -486,7 +490,7 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* setPlayWhenReady=false */, period0Seq0 /* READY */, WINDOW_0 /* BUFFERING */, - WINDOW_0 /* setPlayWhenReady=true */, + period0Seq1 /* setPlayWhenReady=true */, period0Seq1 /* READY */, period0Seq1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) @@ -545,6 +549,8 @@ public final class AnalyticsCollectorTest { .waitForPlaybackState(Player.STATE_IDLE) .seek(/* positionMs= */ 0) .prepare() + // Wait until loading started to assert loading events without flakiness. + .waitForIsLoading(true) .play() .waitForPlaybackState(Player.STATE_ENDED) .build(); @@ -698,6 +704,8 @@ public final class AnalyticsCollectorTest { .waitForIsLoading(true) .waitForIsLoading(false) .removeMediaItem(/* index= */ 0) + .waitForPlaybackState(Player.STATE_BUFFERING) + .waitForPlaybackState(Player.STATE_READY) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); @@ -719,8 +727,8 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* BUFFERING */, period0Seq0 /* READY */, period0Seq1 /* BUFFERING */, - period0Seq1 /* setPlayWhenReady=true */, period0Seq1 /* READY */, + period0Seq1 /* setPlayWhenReady=true */, period0Seq1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( @@ -815,6 +823,19 @@ public final class AnalyticsCollectorTest { } }) .pause() + // Ensure everything is preloaded. + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) .waitForPlaybackState(Player.STATE_READY) // Wait in each content part to ensure previously triggered events get a chance to be // delivered. This prevents flakiness caused by playback progressing too fast. @@ -1018,6 +1039,8 @@ public final class AnalyticsCollectorTest { .waitForIsLoading(false) // Seek behind the midroll. .seek(6 * C.MICROS_PER_SECOND) + // Wait until loading started again to assert loading events without flakiness. + .waitForIsLoading(true) .play() .waitForPlaybackState(Player.STATE_ENDED) .build(); @@ -1047,8 +1070,8 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, contentBeforeMidroll /* READY */, - contentAfterMidroll /* setPlayWhenReady=true */, midrollAd /* BUFFERING */, + midrollAd /* setPlayWhenReady=true */, midrollAd /* READY */, contentAfterMidroll /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))