Reorder renderer changes at period transitions

Currently the following sequence of events happens at automatic period transitions:
1. Update queue (=release old playing period)
2. Disable unused renderers
3. Enable newly needed renderers

This order requires difficult to follow workarounds in AnalyticsCollector for all
events related to step 2 (disable renderers). The current media period has already
been advanced so can't be used. The current workaround saves the last known playing
media period that was published as part of a PlaybackInfo update in ExoPlayerImpl.
This works in most cases, but is inherently wrong because the published state in
ExoPlayerImpl may be completely unrelated to the updates happening on the playback
thread (e.g. if many other operations are pending).

Simplify and fix this problem by changing the order of the events above:
1. Disable unused renderers.
2. Update queue
3. Enable newly needed renderers.
This way the current playing media period can be used for both renderer disable
and renderer enable events, thus it's correct in all cases and the workaround
in AnalyticsCollector can be removed.

PiperOrigin-RevId: 290037225
This commit is contained in:
tonihei 2020-01-16 11:48:42 +00:00 committed by Oliver Woodman
parent 8d2fd383f9
commit 2e7978a0e2
3 changed files with 84 additions and 83 deletions

View File

@ -950,15 +950,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
setState(Player.STATE_BUFFERING);
}
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;
// Find the requested period if it's already prepared.
@Nullable MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
@Nullable MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;
while (newPlayingPeriodHolder != null) {
if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) {
queue.removeAfter(newPlayingPeriodHolder);
break;
}
newPlayingPeriodHolder = queue.advancePlayingPeriod();
newPlayingPeriodHolder = newPlayingPeriodHolder.getNext();
}
// Disable all renderers if the period being played is changing, if the seek results in negative
@ -971,15 +970,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
disableRenderer(renderer);
}
enabledRenderers = new Renderer[0];
oldPlayingPeriodHolder = null;
if (newPlayingPeriodHolder != null) {
// Update the queue and reenable renderers if the requested media period already exists.
while (queue.getPlayingPeriod() != newPlayingPeriodHolder) {
queue.advancePlayingPeriod();
}
newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
enablePlayingPeriodRenderers();
}
}
// Update the holders.
// Do the actual seeking.
if (newPlayingPeriodHolder != null) {
updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
queue.removeAfter(newPlayingPeriodHolder);
if (newPlayingPeriodHolder.hasEnabledTracks) {
periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
newPlayingPeriodHolder.mediaPeriod.discardBuffer(
@ -1694,7 +1697,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// The window index of the seek position was outside the bounds of the timeline.
return null;
}
if (timeline == seekTimeline) {
if (timeline.equals(seekTimeline)) {
// Our internal timeline is the seek timeline, so the mapped position is correct.
return periodPosition;
}
@ -1869,8 +1872,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
// anymore and need to re-enable the renderers. Set all current streams final to do that.
setAllRendererStreamsFinal();
}
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
disablePlayingPeriodRenderersForTransition(rendererWasEnabledFlags);
MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
enablePlayingPeriodRenderers(rendererWasEnabledFlags);
playbackInfo =
copyWithNewPosition(
newPlayingPeriodHolder.info.id,
@ -1943,7 +1948,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (loadingPeriodHolder == queue.getPlayingPeriod()) {
// This is the first prepared period, so update the position and the renderers.
resetRendererPosition(loadingPeriodHolder.info.startPositionUs);
updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null);
enablePlayingPeriodRenderers();
}
maybeContinueLoading();
}
@ -2025,22 +2030,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs());
}
@SuppressWarnings("ParameterNotNullable")
private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
private void disablePlayingPeriodRenderersForTransition(boolean[] outRendererWasEnabledFlags)
throws ExoPlaybackException {
MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();
if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) {
return;
}
int enabledRendererCount = 0;
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
MediaPeriodHolder oldPlayingPeriodHolder = Assertions.checkNotNull(queue.getPlayingPeriod());
MediaPeriodHolder newPlayingPeriodHolder =
Assertions.checkNotNull(oldPlayingPeriodHolder.getNext());
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {
enabledRendererCount++;
}
if (rendererWasEnabledFlags[i]
outRendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
if (outRendererWasEnabledFlags[i]
&& (!newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)
|| (renderer.isCurrentStreamFinal()
&& renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) {
@ -2050,10 +2048,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
disableRenderer(renderer);
}
}
}
private void enablePlayingPeriodRenderers() throws ExoPlaybackException {
enablePlayingPeriodRenderers(/* rendererWasEnabledFlags= */ new boolean[renderers.length]);
}
private void enablePlayingPeriodRenderers(boolean[] rendererWasEnabledFlags)
throws ExoPlaybackException {
MediaPeriodHolder playingPeriodHolder = Assertions.checkNotNull(queue.getPlayingPeriod());
playbackInfo =
playbackInfo.copyWithTrackInfo(
newPlayingPeriodHolder.getTrackGroups(),
newPlayingPeriodHolder.getTrackSelectorResult());
playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
int enabledRendererCount = 0;
for (int i = 0; i < renderers.length; i++) {
if (playingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {
enabledRendererCount++;
}
}
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
}

View File

@ -127,7 +127,7 @@ public class AnalyticsCollector
*/
public final void notifySeekStarted() {
if (!mediaPeriodQueueTracker.isSeeking()) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
mediaPeriodQueueTracker.onSeekStarted();
for (AnalyticsListener listener : listeners) {
listener.onSeekStarted(eventTime);
@ -149,7 +149,7 @@ public class AnalyticsCollector
@Override
public final void onMetadata(Metadata metadata) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onMetadata(eventTime, metadata);
}
@ -195,9 +195,7 @@ public class AnalyticsCollector
@Override
public final void onAudioDisabled(DecoderCounters counters) {
// The renderers are disabled after we changed the playing media period on the playback thread
// but before this change is reported to the app thread.
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
}
@ -260,7 +258,7 @@ public class AnalyticsCollector
@Override
public final void onDroppedFrames(int count, long elapsedMs) {
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDroppedVideoFrames(eventTime, count, elapsedMs);
}
@ -268,9 +266,7 @@ public class AnalyticsCollector
@Override
public final void onVideoDisabled(DecoderCounters counters) {
// The renderers are disabled after we changed the playing media period on the playback thread
// but before this change is reported to the app thread.
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
}
@ -416,7 +412,7 @@ public class AnalyticsCollector
@Override
public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onTimelineChanged(eventTime, reason);
}
@ -425,7 +421,7 @@ public class AnalyticsCollector
@Override
public final void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onTracksChanged(eventTime, trackGroups, trackSelections);
}
@ -433,7 +429,7 @@ public class AnalyticsCollector
@Override
public final void onLoadingChanged(boolean isLoading) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onLoadingChanged(eventTime, isLoading);
}
@ -441,7 +437,7 @@ public class AnalyticsCollector
@Override
public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);
}
@ -450,7 +446,7 @@ public class AnalyticsCollector
@Override
public void onPlaybackSuppressionReasonChanged(
@PlaybackSuppressionReason int playbackSuppressionReason) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason);
}
@ -458,7 +454,7 @@ public class AnalyticsCollector
@Override
public void onIsPlayingChanged(boolean isPlaying) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onIsPlayingChanged(eventTime, isPlaying);
}
@ -466,7 +462,7 @@ public class AnalyticsCollector
@Override
public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onRepeatModeChanged(eventTime, repeatMode);
}
@ -474,7 +470,7 @@ public class AnalyticsCollector
@Override
public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);
}
@ -482,7 +478,7 @@ public class AnalyticsCollector
@Override
public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error);
}
@ -490,8 +486,7 @@ public class AnalyticsCollector
@Override
public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
mediaPeriodQueueTracker.onPositionDiscontinuity(reason);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPositionDiscontinuity(eventTime, reason);
}
@ -499,7 +494,7 @@ public class AnalyticsCollector
@Override
public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlaybackParametersChanged(eventTime, playbackParameters);
}
@ -509,7 +504,7 @@ public class AnalyticsCollector
public final void onSeekProcessed() {
if (mediaPeriodQueueTracker.isSeeking()) {
mediaPeriodQueueTracker.onSeekProcessed();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onSeekProcessed(eventTime);
}
@ -570,7 +565,7 @@ public class AnalyticsCollector
@Override
public final void onDrmSessionReleased() {
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmSessionReleased(eventTime);
}
@ -594,7 +589,8 @@ public class AnalyticsCollector
long realtimeMs = clock.elapsedRealtime();
long eventPositionMs;
boolean isInCurrentWindow =
timeline == player.getCurrentTimeline() && windowIndex == player.getCurrentWindowIndex();
timeline.equals(player.getCurrentTimeline())
&& windowIndex == player.getCurrentWindowIndex();
if (mediaPeriodId != null && mediaPeriodId.isAd()) {
boolean isCurrentAd =
isInCurrentWindow
@ -636,8 +632,8 @@ public class AnalyticsCollector
mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
}
private EventTime generateLastReportedPlayingMediaPeriodEventTime() {
return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod());
private EventTime generateCurrentPlayerMediaPeriodEventTime() {
return generateEventTime(mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod());
}
private EventTime generatePlayingMediaPeriodEventTime() {
@ -677,8 +673,7 @@ public class AnalyticsCollector
private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo;
private final Period period;
@Nullable private MediaPeriodInfo lastPlayingMediaPeriod;
@Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod;
@Nullable private MediaPeriodInfo playingMediaPeriod;
@Nullable private MediaPeriodInfo readingMediaPeriod;
private Timeline timeline;
private boolean isSeeking;
@ -691,34 +686,34 @@ public class AnalyticsCollector
}
/**
* Returns the {@link MediaPeriodInfo} of the media period in the front of the queue. This is
* the playing media period unless the player hasn't started playing yet (in which case it is
* the loading media period or null). While the player is seeking or preparing, this method will
* always return null to reflect the uncertainty about the current playing period. May also be
* null, if the timeline is empty or no media period is active yet.
* Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of
* the player.
*
* <p>May be null if no matching media period has been created yet or the player is currently
* masking its state.
*/
@Nullable
public MediaPeriodInfo getPlayingMediaPeriod() {
public MediaPeriodInfo getCurrentPlayerMediaPeriod() {
return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking
? null
: mediaPeriodInfoQueue.get(0);
}
/**
* Returns the {@link MediaPeriodInfo} of the currently playing media period. This is the
* publicly reported period which should always match {@link Player#getCurrentPeriodIndex()}
* unless the player is currently seeking or being prepared in which case the previous period is
* reported until the seek or preparation is processed. May be null, if no media period is
* active yet.
* Returns the {@link MediaPeriodInfo} of the media period at the front of the queue. If the
* queue is empty, this is the last media period which was at the front of the queue.
*
* <p>May be null, if no media period has been created yet.
*/
@Nullable
public MediaPeriodInfo getLastReportedPlayingMediaPeriod() {
return lastReportedPlayingMediaPeriod;
public MediaPeriodInfo getPlayingMediaPeriod() {
return playingMediaPeriod;
}
/**
* Returns the {@link MediaPeriodInfo} of the media period currently being read by the player.
* May be null, if the player is not reading a media period.
*
* <p>May be null, if the player is not reading a media period.
*/
@Nullable
public MediaPeriodInfo getReadingMediaPeriod() {
@ -727,8 +722,9 @@ public class AnalyticsCollector
/**
* Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is
* currently loading or will be the next one loading. May be null, if no media period is active
* yet.
* currently loading or will be the next one loading.
*
* <p>May be null, if no media period is active yet.
*/
@Nullable
public MediaPeriodInfo getLoadingMediaPeriod() {
@ -770,11 +766,6 @@ public class AnalyticsCollector
return match;
}
/** Updates the queue with a reported position discontinuity . */
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
/** Updates the queue with a reported timeline change. */
public void onTimelineChanged(Timeline timeline) {
for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
@ -786,8 +777,10 @@ public class AnalyticsCollector
if (readingMediaPeriod != null) {
readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline);
}
if (!mediaPeriodInfoQueue.isEmpty()) {
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
}
this.timeline = timeline;
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
/** Updates the queue with a reported start of seek. */
@ -798,7 +791,6 @@ public class AnalyticsCollector
/** Updates the queue with a reported processed seek. */
public void onSeekProcessed() {
isSeeking = false;
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
/** Updates the queue with a newly created media period. */
@ -812,10 +804,7 @@ public class AnalyticsCollector
isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
mediaPeriodInfoQueue.add(mediaPeriodInfo);
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) {
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
}
/**
@ -833,7 +822,7 @@ public class AnalyticsCollector
readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0);
}
if (!mediaPeriodInfoQueue.isEmpty()) {
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
}
return true;
}

View File

@ -434,7 +434,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1Seq2);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))
.containsExactly(period0, period0, period1Seq2);
.containsExactly(period0, period1Seq2, period1Seq2);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period1Seq2);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
.containsExactly(period0, period1Seq2);