Skip to content

Commit

Permalink
Advance playing period even if the next one isn't prepared yet.
Browse files Browse the repository at this point in the history
This solves various issues around event association for buffering and
error throwing around period discontinuities.

The main changes are:
 - Logic around being "ready" at the end of a period no longer checks if the
   next period is prepared.
 - Advancing the playing period no longer checks if the next one is prepared.
 - Prepare errors are always thrown for the playing period.

This changes the semantics and assumptions about the "playing" period:
 1. The playing period can no longer assumed to be prepared.
 2. We no longer have a case where the queue is non-empty and the playing or
    reading periods are unassigned (=null).
Most other code changes ensure that these changed assumptions are handled.

Issue:#5407
PiperOrigin-RevId: 263776304
  • Loading branch information
tonihei committed Aug 23, 2019
1 parent 14f77cb commit 652c2f9
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 141 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
`ExoPlayer.Builder`.
* Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`
([#5619](https://github.com/google/ExoPlayer/issues/5619)).
* Fix issue where player errors are thrown too early at playlist transitions
([#5407](https://github.com/google/ExoPlayer/issues/5407)).

### 2.10.4 ###

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;

private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
private static final int ACTIVE_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;

private final Renderer[] renderers;
Expand Down Expand Up @@ -514,22 +513,25 @@ private void stopRenderers() throws ExoPlaybackException {
}

private void updatePlaybackPositions() throws ExoPlaybackException {
if (!queue.hasPlayingPeriod()) {
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
if (playingPeriodHolder == null) {
return;
}

// Update the playback position.
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();
if (periodPositionUs != C.TIME_UNSET) {
resetRendererPosition(periodPositionUs);
long discontinuityPositionUs =
playingPeriodHolder.prepared
? playingPeriodHolder.mediaPeriod.readDiscontinuity()
: C.TIME_UNSET;
if (discontinuityPositionUs != C.TIME_UNSET) {
resetRendererPosition(discontinuityPositionUs);
// A MediaPeriod may report a discontinuity at the current playback position to ensure the
// renderers are flushed. Only report the discontinuity externally if the position changed.
if (periodPositionUs != playbackInfo.positionUs) {
if (discontinuityPositionUs != playbackInfo.positionUs) {
playbackInfo =
playbackInfo.copyWithNewPosition(
playbackInfo.periodId,
periodPositionUs,
discontinuityPositionUs,
playbackInfo.contentPositionUs,
getTotalBufferedDurationUs());
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
Expand All @@ -538,7 +540,7 @@ private void updatePlaybackPositions() throws ExoPlaybackException {
rendererPositionUs =
mediaClock.syncAndGetPositionUs(
/* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
playbackInfo.positionUs = periodPositionUs;
}
Expand All @@ -552,60 +554,71 @@ private void updatePlaybackPositions() throws ExoPlaybackException {
private void doSomeWork() throws ExoPlaybackException, IOException {
long operationStartTimeMs = clock.uptimeMillis();
updatePeriods();
if (!queue.hasPlayingPeriod()) {
// We're still waiting for the first period to be prepared.
maybeThrowPeriodPrepareError();
scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);

MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
if (playingPeriodHolder == null) {
// We're still waiting until the playing period is available.
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
return;
}
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();

TraceUtil.beginSection("doSomeWork");

updatePlaybackPositions();
long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;

playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs,
retainBackBufferFromKeyframe);

boolean renderersEnded = true;
boolean renderersReadyOrEnded = true;
for (Renderer renderer : enabledRenderers) {
// TODO: Each renderer should return the maximum delay before which it wishes to be called
// again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
renderersEnded = renderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). We override to assume the renderer is
// ready if it needs the next sample stream. This is necessary to avoid getting stuck if
// tracks in the current period have uneven durations. See:
// https://github.com/google/ExoPlayer/issues/1874
boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded()
|| rendererWaitingForNextStream(renderer);
if (!rendererReadyOrEnded) {
renderer.maybeThrowStreamError();
boolean renderersAllowPlayback = true;
if (playingPeriodHolder.prepared) {
long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
playingPeriodHolder.mediaPeriod.discardBuffer(
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
if (renderer.getState() == Renderer.STATE_DISABLED) {
continue;
}
// TODO: Each renderer should return the maximum delay before which it wishes to be called
// again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
renderersEnded = renderersEnded && renderer.isEnded();
// Determine whether the renderer allows playback to continue. Playback can continue if the
// renderer is ready or ended. Also continue playback if the renderer is reading ahead into
// the next stream or is waiting for the next stream. This is to avoid getting stuck if
// tracks in the current period have uneven durations and are still being read by another
// renderer. See: https://github.com/google/ExoPlayer/issues/1874.
boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
boolean isWaitingForNextStream =
!isReadingAhead
&& playingPeriodHolder.getNext() != null
&& renderer.hasReadStreamToEnd();
boolean allowsPlayback =
isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
if (!allowsPlayback) {
renderer.maybeThrowStreamError();
}
}
renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded;
}
if (!renderersReadyOrEnded) {
maybeThrowPeriodPrepareError();
} else {
playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
}

long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
if (renderersEnded
&& playingPeriodHolder.prepared
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playingPeriodDurationUs <= playbackInfo.positionUs)
&& playingPeriodHolder.info.isFinal) {
setState(Player.STATE_ENDED);
stopRenderers();
} else if (playbackInfo.playbackState == Player.STATE_BUFFERING
&& shouldTransitionToReadyState(renderersReadyOrEnded)) {
&& shouldTransitionToReadyState(renderersAllowPlayback)) {
setState(Player.STATE_READY);
if (playWhenReady) {
startRenderers();
}
} else if (playbackInfo.playbackState == Player.STATE_READY
&& !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) {
&& !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) {
rebuffering = playWhenReady;
setState(Player.STATE_BUFFERING);
stopRenderers();
Expand All @@ -619,7 +632,7 @@ && shouldTransitionToReadyState(renderersReadyOrEnded)) {

if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY)
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
} else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else {
Expand Down Expand Up @@ -681,7 +694,9 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti
long newPeriodPositionUs = periodPositionUs;
if (periodId.equals(playbackInfo.periodId)) {
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
if (playingPeriodHolder != null && newPeriodPositionUs != 0) {
if (playingPeriodHolder != null
&& playingPeriodHolder.prepared
&& newPeriodPositionUs != 0) {
newPeriodPositionUs =
playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs(
newPeriodPositionUs, seekParameters);
Expand Down Expand Up @@ -771,10 +786,11 @@ private long seekToPeriodPosition(
}

private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod();
rendererPositionUs =
!queue.hasPlayingPeriod()
playingMediaPeriod == null
? periodPositionUs
: queue.getPlayingPeriod().toRendererTime(periodPositionUs);
: playingMediaPeriod.toRendererTime(periodPositionUs);
mediaClock.resetPosition(rendererPositionUs);
for (Renderer renderer : enabledRenderers) {
renderer.resetPosition(rendererPositionUs);
Expand Down Expand Up @@ -1092,10 +1108,6 @@ private void disableRenderer(Renderer renderer) throws ExoPlaybackException {
}

private void reselectTracksInternal() throws ExoPlaybackException {
if (!queue.hasPlayingPeriod()) {
// We don't have tracks yet, so we don't care.
return;
}
float playbackSpeed = mediaClock.getPlaybackParameters().speed;
// Reselect tracks on each period in turn, until the selection changes.
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
Expand Down Expand Up @@ -1182,8 +1194,8 @@ private void reselectTracksInternal() throws ExoPlaybackException {
}

private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
while (periodHolder != null && periodHolder.prepared) {
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
while (periodHolder != null) {
TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
for (TrackSelection trackSelection : trackSelections) {
if (trackSelection != null) {
Expand All @@ -1195,7 +1207,7 @@ private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
}

private void notifyTrackSelectionDiscontinuity() {
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
while (periodHolder != null) {
TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
for (TrackSelection trackSelection : trackSelections) {
Expand Down Expand Up @@ -1230,12 +1242,10 @@ private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) {

private boolean isTimelineReady() {
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
MediaPeriodHolder nextPeriodHolder = playingPeriodHolder.getNext();
long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
return playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
|| (nextPeriodHolder != null
&& (nextPeriodHolder.prepared || nextPeriodHolder.info.id.isAd()));
return playingPeriodHolder.prepared
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs);
}

private void maybeThrowSourceInfoRefreshError() throws IOException {
Expand All @@ -1251,21 +1261,6 @@ private void maybeThrowSourceInfoRefreshError() throws IOException {
mediaSource.maybeThrowSourceInfoRefreshError();
}

private void maybeThrowPeriodPrepareError() throws IOException {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (loadingPeriodHolder != null
&& !loadingPeriodHolder.prepared
&& (readingPeriodHolder == null || readingPeriodHolder.getNext() == loadingPeriodHolder)) {
for (Renderer renderer : enabledRenderers) {
if (!renderer.hasReadStreamToEnd()) {
return;
}
}
loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
}
}

private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
throws ExoPlaybackException {
if (sourceRefreshInfo.source != mediaSource) {
Expand Down Expand Up @@ -1335,7 +1330,7 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
}
} else {
// Something changed. Seek to new start position.
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
if (periodHolder != null) {
// Update the new playing media period info if it already exists.
while (periodHolder.getNext() != null) {
Expand All @@ -1361,6 +1356,9 @@ private long getMaxRendererReadPositionUs() {
return 0;
}
long maxReadPositionUs = readingHolder.getRendererOffset();
if (!readingHolder.prepared) {
return maxReadPositionUs;
}
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getState() == Renderer.STATE_DISABLED
|| renderers[i].getStream() != readingHolder.sampleStreams[i]) {
Expand Down Expand Up @@ -1494,23 +1492,26 @@ private void updatePeriods() throws ExoPlaybackException, IOException {
maybeUpdatePlayingPeriod();
}

private void maybeUpdateLoadingPeriod() throws IOException {
private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException {
queue.reevaluateBuffer(rendererPositionUs);
if (queue.shouldLoadNextMediaPeriod()) {
MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
if (info == null) {
maybeThrowSourceInfoRefreshError();
} else {
MediaPeriod mediaPeriod =
queue.enqueueNextMediaPeriod(
MediaPeriodHolder mediaPeriodHolder =
queue.enqueueNextMediaPeriodHolder(
rendererCapabilities,
trackSelector,
loadControl.getAllocator(),
mediaSource,
info,
emptyTrackSelectorResult);
mediaPeriod.prepare(this, info.startPositionUs);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
setIsLoading(true);
if (queue.getPlayingPeriod() == mediaPeriodHolder) {
resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime());
}
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
}
}
Expand All @@ -1522,7 +1523,7 @@ private void maybeUpdateLoadingPeriod() throws IOException {
}
}

private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException {
private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (readingPeriodHolder == null) {
return;
Expand Down Expand Up @@ -1552,7 +1553,6 @@ private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException

if (!readingPeriodHolder.getNext().prepared) {
// The successor is not prepared yet.
maybeThrowPeriodPrepareError();
return;
}

Expand Down Expand Up @@ -1607,6 +1607,11 @@ private void maybeUpdatePlayingPeriod() throws ExoPlaybackException {
maybeNotifyPlaybackInfoChanged();
}
MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
if (oldPlayingPeriodHolder == queue.getReadingPeriod()) {
// The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams
// anymore and need to re-enable the renderers. Set all current streams final to do that.
setAllRendererStreamsFinal();
}
MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
playbackInfo =
Expand All @@ -1633,17 +1638,22 @@ private boolean shouldAdvancePlayingPeriod() {
if (playingPeriodHolder == null) {
return false;
}
MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();
if (nextPlayingPeriodHolder == null) {
return false;
}
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (playingPeriodHolder == readingPeriodHolder) {
if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) {
return false;
}
MediaPeriodHolder nextPlayingPeriodHolder =
Assertions.checkNotNull(playingPeriodHolder.getNext());
return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime();
}

private boolean hasReadingPeriodFinishedReading() {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (!readingPeriodHolder.prepared) {
return false;
}
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
Expand Down Expand Up @@ -1674,10 +1684,9 @@ private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackExc
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
updateLoadControlTrackSelection(
loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());
if (!queue.hasPlayingPeriod()) {
// This is the first prepared period, so start playing it.
MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod();
resetRendererPosition(playingPeriodHolder.info.startPositionUs);
if (loadingPeriodHolder == queue.getPlayingPeriod()) {
// This is the first prepared period, so update the position and the renderers.
resetRendererPosition(loadingPeriodHolder.info.startPositionUs);
updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null);
}
maybeContinueLoading();
Expand Down Expand Up @@ -1805,12 +1814,6 @@ private void enableRenderer(
}
}

private boolean rendererWaitingForNextStream(Renderer renderer) {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
MediaPeriodHolder nextPeriodHolder = readingPeriodHolder.getNext();
return nextPeriodHolder != null && nextPeriodHolder.prepared && renderer.hasReadStreamToEnd();
}

private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) {
MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod();
MediaPeriodId loadingMediaPeriodId =
Expand Down
Loading

0 comments on commit 652c2f9

Please sign in to comment.