Ensure tracks in PlaybackInfo are always in sync with position.

Currently both are updated by separate setters. If an exception is thrown between
the two setters, the state may not be consistent.

Avoid this problem by always setting them together.

PiperOrigin-RevId: 290293600
This commit is contained in:
tonihei 2020-01-17 18:15:59 +00:00 committed by Ian Baker
parent ff89170b00
commit 4cf614c639
4 changed files with 117 additions and 45 deletions

View File

@ -802,7 +802,9 @@ import java.util.concurrent.TimeoutException;
playbackInfo.periodId,
/* positionUs= */ 0,
playbackInfo.contentPositionUs,
playbackInfo.totalBufferedDurationUs);
playbackInfo.totalBufferedDurationUs,
playbackInfo.trackGroups,
playbackInfo.trackSelectorResult);
}
if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
// Update the masking variables, which are used when the timeline becomes empty.

View File

@ -22,6 +22,7 @@ import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
@ -999,10 +1000,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
resetRendererPosition(periodPositionUs);
maybeContinueLoading();
} else {
queue.clear(/* keepFrontPeriodUid= */ true);
// New period has not been prepared.
playbackInfo =
playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult);
queue.clear(/* keepFrontPeriodUid= */ true);
resetRendererPosition(periodPositionUs);
}
@ -1141,12 +1140,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
MediaPeriodId mediaPeriodId = playbackInfo.periodId;
long contentPositionUs = playbackInfo.contentPositionUs;
boolean resetTrackInfo = clearPlaylist;
if (resetPosition) {
mediaPeriodId =
timeline.isEmpty()
? playbackInfo.getDummyPeriodForEmptyTimeline()
: getDummyFirstMediaPeriodForAds();
contentPositionUs = C.TIME_UNSET;
if (!mediaPeriodId.equals(playbackInfo.periodId)) {
resetTrackInfo = true;
}
}
// Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
@ -1159,8 +1162,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo.playbackState,
resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false,
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
resetTrackInfo ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
resetTrackInfo ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
mediaPeriodId,
startPositionUs,
/* totalBufferedDurationUs= */ 0,
@ -1390,11 +1393,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
long periodPositionUs =
playingPeriodHolder.applyTrackSelection(
newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
playbackInfo =
copyWithNewPosition(
playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs);
if (playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs) {
playbackInfo =
copyWithNewPosition(
playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs);
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
resetRendererPosition(periodPositionUs);
}
@ -1418,9 +1421,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
}
playbackInfo =
playbackInfo.copyWithTrackInfo(
playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else {
// Release and re-prepare/buffer periods after the one whose selection changed.
@ -1883,7 +1883,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
disablePlayingPeriodRenderersForTransition(rendererWasEnabledFlags);
MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
enablePlayingPeriodRenderers(rendererWasEnabledFlags);
playbackInfo =
copyWithNewPosition(
newPlayingPeriodHolder.info.id,
@ -1894,6 +1893,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
: Player.DISCONTINUITY_REASON_AD_INSERTION;
playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
enablePlayingPeriodRenderers(rendererWasEnabledFlags);
updatePlaybackPositions();
advancedPlayingPeriod = true;
}
@ -1957,6 +1957,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
// This is the first prepared period, so update the position and the renderers.
resetRendererPosition(loadingPeriodHolder.info.startPositionUs);
enablePlayingPeriodRenderers();
playbackInfo =
copyWithNewPosition(
playbackInfo.periodId, playbackInfo.positionUs, playbackInfo.contentPositionUs);
}
maybeContinueLoading();
}
@ -2024,11 +2027,35 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
@CheckResult
private PlaybackInfo copyWithNewPosition(
MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) {
deliverPendingMessageAtStartPositionRequired = true;
deliverPendingMessageAtStartPositionRequired =
positionUs != playbackInfo.positionUs || !mediaPeriodId.equals(playbackInfo.periodId);
TrackGroupArray trackGroupArray = playbackInfo.trackGroups;
TrackSelectorResult trackSelectorResult = playbackInfo.trackSelectorResult;
if (playlist.isPrepared()) {
@Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
trackGroupArray =
playingPeriodHolder == null
? TrackGroupArray.EMPTY
: playingPeriodHolder.getTrackGroups();
trackSelectorResult =
playingPeriodHolder == null
? emptyTrackSelectorResult
: playingPeriodHolder.getTrackSelectorResult();
} else if (!mediaPeriodId.equals(playbackInfo.periodId)) {
// Reset previously kept track info if unprepared and the period changes.
trackGroupArray = TrackGroupArray.EMPTY;
trackSelectorResult = emptyTrackSelectorResult;
}
return playbackInfo.copyWithNewPosition(
mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs());
mediaPeriodId,
positionUs,
contentPositionUs,
getTotalBufferedDurationUs(),
trackGroupArray,
trackSelectorResult);
}
private void disablePlayingPeriodRenderersForTransition(boolean[] outRendererWasEnabledFlags)
@ -2058,9 +2085,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void enablePlayingPeriodRenderers(boolean[] rendererWasEnabledFlags)
throws ExoPlaybackException {
MediaPeriodHolder playingPeriodHolder = Assertions.checkNotNull(queue.getPlayingPeriod());
playbackInfo =
playbackInfo.copyWithTrackInfo(
playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
int enabledRendererCount = 0;
for (int i = 0; i < renderers.length; i++) {
if (playingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {

View File

@ -191,6 +191,9 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
* @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored
* if {@code periodId.isAd()} is true.
* @param totalBufferedDurationUs New buffered duration. See {@link #totalBufferedDurationUs}.
* @param trackGroups The track groups for the new position. See {@link #trackGroups}.
* @param trackSelectorResult The track selector result for the new position. See {@link
* #trackSelectorResult}.
* @return Copied playback info with new playing position.
*/
@CheckResult
@ -198,7 +201,9 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
MediaPeriodId periodId,
long positionUs,
long contentPositionUs,
long totalBufferedDurationUs) {
long totalBufferedDurationUs,
TrackGroupArray trackGroups,
TrackSelectorResult trackSelectorResult) {
return new PlaybackInfo(
timeline,
periodId,
@ -311,32 +316,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
positionUs);
}
/**
* Copies playback info with new track information.
*
* @param trackGroups New track groups. See {@link #trackGroups}.
* @param trackSelectorResult New track selector result. See {@link #trackSelectorResult}.
* @return Copied playback info with new track information.
*/
@CheckResult
public PlaybackInfo copyWithTrackInfo(
TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
return new PlaybackInfo(
timeline,
periodId,
startPositionUs,
contentPositionUs,
playbackState,
playbackError,
isLoading,
trackGroups,
trackSelectorResult,
loadingMediaPeriodId,
bufferedPositionUs,
totalBufferedDurationUs,
positionUs);
}
/**
* Copies playback info with new loading media period.
*

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
@ -5591,6 +5592,72 @@ public final class ExoPlayerTest {
assertArrayEquals(new int[] {1, 0}, currentWindowIndices);
}
@Test
public void errorThrownDuringPeriodTransition_keepsConsistentPlayerState() throws Exception {
FakeMediaSource source1 =
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), Builder.VIDEO_FORMAT);
FakeMediaSource source2 =
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), Builder.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeRenderer audioRenderer =
new FakeRenderer(Builder.AUDIO_FORMAT) {
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
// Fail when enabling the renderer. This will happen during the period transition.
throw createRendererException(new IllegalStateException(), Builder.AUDIO_FORMAT);
}
};
AtomicReference<TrackGroupArray> trackGroupsAfterError = new AtomicReference<>();
AtomicReference<TrackSelectionArray> trackSelectionsAfterError = new AtomicReference<>();
AtomicInteger windowIndexAfterError = new AtomicInteger();
ActionSchedule actionSchedule =
new ActionSchedule.Builder("errorThrownDuringPeriodTransition_keepsConsistentPlayerState")
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.addAnalyticsListener(
new AnalyticsListener() {
@Override
public void onPlayerError(
EventTime eventTime, ExoPlaybackException error) {
trackGroupsAfterError.set(player.getCurrentTrackGroups());
trackSelectionsAfterError.set(player.getCurrentTrackSelections());
windowIndexAfterError.set(player.getCurrentWindowIndex());
}
});
}
})
.pause()
// Wait until fully buffered so that the new renderer can be enabled immediately.
.waitForIsLoading(true)
.waitForIsLoading(false)
.waitForIsLoading(true)
.waitForIsLoading(false)
.play()
.build();
ExoPlayerTestRunner testRunner =
new Builder()
.setMediaSources(source1, source2)
.setActionSchedule(actionSchedule)
.setRenderers(videoRenderer, audioRenderer)
.build(context);
assertThrows(
ExoPlaybackException.class,
() ->
testRunner
.start(/* doPrepare= */ true)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS));
assertThat(windowIndexAfterError.get()).isEqualTo(1);
assertThat(trackGroupsAfterError.get().length).isEqualTo(1);
assertThat(trackGroupsAfterError.get().get(0).getFormat(0)).isEqualTo(Builder.AUDIO_FORMAT);
assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer.
assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer.
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {