Commit 23ff4efd by olly Committed by Oliver Woodman

Tell LoadControl whether playback can start

- This gives LoadControl enough information in shouldContinueLoading
  to know whether returning false will result in a terminal non-playback
  state.
- DefaultLoadControl will always return true when returning false will
  result in a terminal non-playback state, unless the target buffer size
  is exceeded. This can help to avoid getting stuck in the case that a
  MediaPeriod is providing samples from an unexpected starting time.
- Make the terminal state actually terminal. Previously the player would
  end up in an indefinite buffering state. We now fail with an error.
- Also remove the opportunity for LoadControl implementations to livelock
  playback. No sane LoadControl should simultaneously tell the player that
  it doesn't want to load anything and that it doesn't want to start
  playback. So this change removes the opportunity and starts playback in
  EPII instead in this case.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=183114797
parent 35dad90b
......@@ -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}.
......
......@@ -88,25 +88,32 @@ 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.
* @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
* buffer depletion rather than a user action. Hence this parameter is false during initial
* buffering and when buffering as a result of a seek operation.
* @return Whether playback should be allowed to start or resume.
* @return Whether the loading should continue.
*/
boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering);
boolean shouldContinueLoading(
boolean canStartPlayback, long bufferedDurationUs, float playbackSpeed);
/**
* Called by the player to determine whether it should continue to load the source.
* 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.
* @return Whether the loading should continue.
* @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
* buffer depletion rather than a user action. Hence this parameter is false during initial
* buffering and when buffering as a result of a seek operation.
* @return Whether playback should be allowed to start or resume.
*/
boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed);
boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering);
}
......@@ -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;
}
......
/*
* 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 {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment