diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index af610a8165..4cc2d90c90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -214,22 +214,8 @@ public class DefaultLoadControl implements LoadControl { } @Override - public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, - boolean rebuffering) { - if (bufferedDurationUs >= minBufferUs) { - // It's possible that we're not loading, so allow playback to start unconditionally. - return true; - } - bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); - long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 - || bufferedDurationUs >= minBufferDurationUs - || (!prioritizeTimeOverSizeThresholds - && allocator.getTotalBytesAllocated() >= targetBufferSize); - } - - @Override - public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { + public boolean shouldContinueLoading( + boolean canStartPlayback, long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; if (prioritizeTimeOverSizeThresholds) { @@ -244,6 +230,9 @@ public class DefaultLoadControl implements LoadControl { && (bufferedDurationUs < minBufferUs // below low watermark || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks } + if (!isBuffering && !canStartPlayback && !targetBufferSizeReached) { + isBuffering = true; + } if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -254,6 +243,17 @@ public class DefaultLoadControl implements LoadControl { return isBuffering; } + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); + long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; + return minBufferDurationUs <= 0 + || bufferedDurationUs >= minBufferDurationUs + || (!prioritizeTimeOverSizeThresholds + && allocator.getTotalBytesAllocated() >= targetBufferSize); + } + /** * Calculate target buffer size in bytes based on the selected tracks. The player will try not to * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}. 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 560af6db54..92763704e5 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 @@ -126,6 +126,7 @@ import java.util.Collections; private boolean released; private boolean playWhenReady; private boolean rebuffering; + private boolean renderersReadyOrEnded; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; @@ -520,10 +521,10 @@ import java.util.Collections; } // Update the buffered position. - long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE - : playingPeriodHolder.mediaPeriod.getBufferedPositionUs(); - playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE - ? playingPeriodHolder.info.durationUs : bufferedPositionUs; + playbackInfo.bufferedPositionUs = + enabledRenderers.length == 0 + ? playingPeriodHolder.info.durationUs + : playingPeriodHolder.getBufferedPositionUs(/* convertEosToDuration= */ true); } private void doSomeWork() throws ExoPlaybackException, IOException { @@ -545,15 +546,14 @@ import java.util.Collections; playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); - boolean allRenderersEnded = true; - boolean allRenderersReadyOrEnded = true; - + 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); - allRenderersEnded = allRenderersEnded && renderer.isEnded(); + 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: @@ -563,43 +563,43 @@ import java.util.Collections; if (!rendererReadyOrEnded) { renderer.maybeThrowStreamError(); } - allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; + renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded; } - if (!allRenderersReadyOrEnded) { + this.renderersReadyOrEnded = renderersReadyOrEnded; + if (!renderersReadyOrEnded) { maybeThrowPeriodPrepareError(); } long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; - if (allRenderersEnded + if (renderersEnded && (playingPeriodDurationUs == C.TIME_UNSET - || playingPeriodDurationUs <= playbackInfo.positionUs) + || playingPeriodDurationUs <= playbackInfo.positionUs) && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { - float playbackSpeed = mediaClock.getPlaybackParameters().speed; - boolean isNewlyReady = - enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded - && queue - .getLoadingPeriod() - .haveSufficientBuffer(rendererPositionUs, playbackSpeed, rebuffering)) - : isTimelineReady(playingPeriodDurationUs); - if (isNewlyReady) { + boolean shouldStartPlayback = isReady(); + if (shouldStartPlayback && playbackInfo.isLoading && enabledRenderers.length != 0) { + MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); + long bufferedPositionUs = loadingHolder.getBufferedPositionUs(!loadingHolder.info.isFinal); + shouldStartPlayback = + bufferedPositionUs == C.TIME_END_OF_SOURCE + || loadControl.shouldStartPlayback( + bufferedPositionUs - loadingHolder.toPeriodTime(rendererPositionUs), + mediaClock.getPlaybackParameters().speed, + rebuffering); + } + if (shouldStartPlayback) { setState(Player.STATE_READY); if (playWhenReady) { startRenderers(); } } - } else if (playbackInfo.playbackState == Player.STATE_READY) { - boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded - : isTimelineReady(playingPeriodDurationUs); - if (!isStillReady) { - rebuffering = playWhenReady; - setState(Player.STATE_BUFFERING); - stopRenderers(); - } + } else if (playbackInfo.playbackState == Player.STATE_READY && !isReady()) { + rebuffering = playWhenReady; + setState(Player.STATE_BUFFERING); + stopRenderers(); } if (playbackInfo.playbackState == Player.STATE_BUFFERING) { @@ -706,6 +706,7 @@ import java.util.Collections; throws ExoPlaybackException { stopRenderers(); rebuffering = false; + renderersReadyOrEnded = false; setState(Player.STATE_BUFFERING); // Clear the timeline, but keep the requested period if it is already prepared. @@ -813,6 +814,7 @@ import java.util.Collections; boolean releaseMediaSource, boolean resetPosition, boolean resetState) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; + renderersReadyOrEnded = false; mediaClock.stop(); rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { @@ -1053,8 +1055,10 @@ import java.util.Collections; boolean recreateStreams = queue.removeAfter(playingPeriodHolder); boolean[] streamResetFlags = new boolean[renderers.length]; - long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( - playbackInfo.positionUs, recreateStreams, streamResetFlags); + long periodPositionUs = + playingPeriodHolder.applyTrackSelection( + playbackInfo.positionUs, recreateStreams, streamResetFlags); + updateLoadControlTrackSelection(playingPeriodHolder); if (playbackInfo.playbackState != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, @@ -1092,7 +1096,8 @@ import java.util.Collections; long loadingPeriodPositionUs = Math.max( periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); - periodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); + periodHolder.applyTrackSelection(loadingPeriodPositionUs, false); + updateLoadControlTrackSelection(periodHolder); } } if (playbackInfo.playbackState != Player.STATE_ENDED) { @@ -1102,6 +1107,12 @@ import java.util.Collections; } } + private void updateLoadControlTrackSelection(MediaPeriodHolder periodHolder) { + TrackSelectorResult trackSelectorResult = periodHolder.trackSelectorResult; + loadControl.onTracksSelected( + renderers, trackSelectorResult.groups, trackSelectorResult.selections); + } + private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { MediaPeriodHolder periodHolder = queue.getFrontPeriod(); while (periodHolder != null) { @@ -1117,12 +1128,17 @@ import java.util.Collections; } } - private boolean isTimelineReady(long playingPeriodDurationUs) { + private boolean isReady() { + if (enabledRenderers.length != 0) { + return renderersReadyOrEnded; + } + // Determine whether we're ready based on the timeline. MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs || (playingPeriodHolder.next != null - && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd())); + && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd())); } private void maybeThrowPeriodPrepareError() throws IOException { @@ -1551,11 +1567,10 @@ import java.util.Collections; Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( - renderers, rendererCapabilities, rendererPositionOffsetUs, trackSelector, - loadControl, + loadControl.getAllocator(), mediaSource, uid, info); @@ -1571,6 +1586,7 @@ import java.util.Collections; return; } loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); + updateLoadControlTrackSelection(loadingPeriodHolder); if (!queue.hasPlayingPeriod()) { // This is the first prepared period, so start playing it. MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); @@ -1592,8 +1608,20 @@ import java.util.Collections; private void maybeContinueLoading() { MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - boolean continueLoading = loadingPeriodHolder.shouldContinueLoading( - rendererPositionUs, mediaClock.getPlaybackParameters().speed); + long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { + setIsLoading(false); + return; + } + boolean canStartPlayback = playbackInfo.playbackState == Player.STATE_READY || isReady(); + long bufferedDurationUs = + nextLoadPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs); + boolean continueLoading = + loadControl.shouldContinueLoading( + canStartPlayback, bufferedDurationUs, mediaClock.getPlaybackParameters().speed); + if (!canStartPlayback && !continueLoading) { + throw new StuckBufferingException(); + } setIsLoading(continueLoading); if (continueLoading) { loadingPeriodHolder.continueLoading(rendererPositionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java index ee4775d048..97f6038965 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -88,8 +88,25 @@ public interface LoadControl { boolean retainBackBufferFromKeyframe(); /** - * Called by the player to determine whether sufficient media is buffered for playback to be - * started or resumed. + * Called by the player to determine whether it should continue to load the source. + * + * @param canStartPlayback Whether the player has the minimum amount of data necessary to start + * playback. If {@code false}, this method must return {@code true} or playback will fail. + * Hence {@code true} should be returned in this case, unless some hard upper limit (e.g. on + * the amount of memory that the control will permit to be allocated) has been exceeded. + * Always true if playback is currently started. + * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. + * @return Whether the loading should continue. + */ + boolean shouldContinueLoading( + boolean canStartPlayback, long bufferedDurationUs, float playbackSpeed); + + /** + * Called repeatedly by the player when it's loading the source, has yet to start playback, and + * has the minimum amount of data necessary for playback to be started. The value returned + * determines whether playback is actually started. The load control may opt to return {@code + * false} until some condition has been met (e.g. a certain amount of media is buffered). * * @param bufferedDurationUs The duration of media that's currently buffered. * @param playbackSpeed The current playback speed. @@ -99,14 +116,4 @@ public interface LoadControl { * @return Whether playback should be allowed to start or resume. */ boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); - - /** - * Called by the player to determine whether it should continue to load the source. - * - * @param bufferedDurationUs The duration of media that's currently buffered. - * @param playbackSpeed The current playback speed. - * @return Whether the loading should continue. - */ - boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index dd4f06bf16..b46155a6d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; /** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ @@ -39,40 +40,35 @@ import com.google.android.exoplayer2.util.Assertions; public final boolean[] mayRetainStreamFlags; public long rendererPositionOffsetUs; - public MediaPeriodInfo info; public boolean prepared; public boolean hasEnabledTracks; + public MediaPeriodInfo info; public MediaPeriodHolder next; public TrackSelectorResult trackSelectorResult; - private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; - private final LoadControl loadControl; private final MediaSource mediaSource; private TrackSelectorResult periodTrackSelectorResult; public MediaPeriodHolder( - Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, - LoadControl loadControl, + Allocator allocator, MediaSource mediaSource, Object periodUid, MediaPeriodInfo info) { - this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; this.trackSelector = trackSelector; - this.loadControl = loadControl; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); this.info = info; - sampleStreams = new SampleStream[renderers.length]; - mayRetainStreamFlags = new boolean[renderers.length]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, loadControl.getAllocator()); + sampleStreams = new SampleStream[rendererCapabilities.length]; + mayRetainStreamFlags = new boolean[rendererCapabilities.length]; + MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator); if (info.endPositionUs != C.TIME_END_OF_SOURCE) { ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true); clippingMediaPeriod.setClipping(0, info.endPositionUs); @@ -98,24 +94,37 @@ import com.google.android.exoplayer2.util.Assertions; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } - public boolean haveSufficientBuffer( - long rendererPositionUs, float playbackSpeed, boolean rebuffering) { - long bufferedPositionUs = - !prepared ? info.startPositionUs : mediaPeriod.getBufferedPositionUs(); - if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { - if (info.isFinal) { - return true; - } - bufferedPositionUs = info.durationUs; + public long getDurationUs() { + return info.durationUs; + } + + /** + * Returns the buffered position in microseconds. If the period is buffered to the end then + * {@link C#TIME_END_OF_SOURCE} is returned unless {@code convertEosToDuration} is true, in which + * case the period duration is returned. + * + * @param convertEosToDuration Whether to return the period duration rather than + * {@link C#TIME_END_OF_SOURCE} if the period is fully buffered. + * @return The buffered position in microseconds. + */ + public long getBufferedPositionUs(boolean convertEosToDuration) { + if (!prepared) { + return info.startPositionUs; } - return loadControl.shouldStartPlayback( - bufferedPositionUs - toPeriodTime(rendererPositionUs), playbackSpeed, rebuffering); + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + return bufferedPositionUs == C.TIME_END_OF_SOURCE && convertEosToDuration + ? info.durationUs + : bufferedPositionUs; + } + + public long getNextLoadPositionUs() { + return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); } public void handlePrepared(float playbackSpeed) throws ExoPlaybackException { prepared = true; selectTracks(playbackSpeed); - long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); + long newStartPositionUs = applyTrackSelection(info.startPositionUs, false); rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); } @@ -126,16 +135,6 @@ import com.google.android.exoplayer2.util.Assertions; } } - public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) { - long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { - return false; - } else { - long bufferedDurationUs = nextLoadPositionUs - toPeriodTime(rendererPositionUs); - return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); - } - } - public void continueLoading(long rendererPositionUs) { long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); mediaPeriod.continueLoading(loadingPeriodPositionUs); @@ -156,12 +155,12 @@ import com.google.android.exoplayer2.util.Assertions; return true; } - public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) { - return updatePeriodTrackSelection( - positionUs, forceRecreateStreams, new boolean[renderers.length]); + public long applyTrackSelection(long positionUs, boolean forceRecreateStreams) { + return applyTrackSelection( + positionUs, forceRecreateStreams, new boolean[rendererCapabilities.length]); } - public long updatePeriodTrackSelection( + public long applyTrackSelection( long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) { TrackSelectionArray trackSelections = trackSelectorResult.selections; for (int i = 0; i < trackSelections.length; i++) { @@ -196,8 +195,6 @@ import com.google.android.exoplayer2.util.Assertions; Assertions.checkState(trackSelections.get(i) == null); } } - // The track selection has changed. - loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections); return positionUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/StuckBufferingException.java b/library/core/src/main/java/com/google/android/exoplayer2/StuckBufferingException.java new file mode 100644 index 0000000000..b20cf9a48d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/StuckBufferingException.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +/** + * Thrown when the player is stuck in a state where it has insufficient media to start playback, but + * its {@link LoadControl} is indicating that no further media should be loaded. + */ +public final class StuckBufferingException extends IllegalStateException {}