Add masking for playWhenReady.

Masking is needed as soon as updates to a value can happen both in EPI
and EPII. PlayWhenReady is currently not masked because all updates
happen in EPI only. As soon as we allow pausing at certain times
(e.g. end of a stream), playWhenReady changes may be triggered by EPII
as well and that's why we need masking.

To know when the value actually changed, we also need to update the
internal state to include whether playback is supppressed.

PiperOrigin-RevId: 300284613
This commit is contained in:
tonihei 2020-03-11 09:57:50 +00:00 committed by Oliver Woodman
parent f82bc244ea
commit 949bbcfb2e
6 changed files with 245 additions and 116 deletions

View File

@ -70,12 +70,13 @@ import java.util.concurrent.TimeoutException;
private final List<Playlist.MediaSourceHolder> 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<ListenerHolder> 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(

View File

@ -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<PendingMessageInfo> 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;
}
}
}

View File

@ -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);

View File

@ -548,6 +548,7 @@ public final class ExoPlayerTest {
// only on seek processed callback).
.seek(5)
.seek(60)
.waitForSeekProcessed()
.play()
.build();
final List<Integer> 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<TrackGroupArray> trackGroupsList = new ArrayList<>();

View File

@ -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);

View File

@ -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))