Add targetLiveOffsetUs parameter to LoadControl.shouldStartPlayback

This allows a LoadControl to start playback earlier if the target
live offset is very low.

Issue: #4904
PiperOrigin-RevId: 336863824
This commit is contained in:
christosts 2020-10-13 14:27:45 +01:00 committed by kim-vde
parent 76b7f76437
commit 8fdadade7b
6 changed files with 170 additions and 36 deletions

View File

@ -3,6 +3,8 @@
### dev-v2 (not yet released) ### dev-v2 (not yet released)
* Core library: * Core library:
* `LoadControl`:
* Add a `targetLiveOffsetUs` parameter to `shouldStartPlayback`.
* Fix bug where streams with highly uneven durations may get stuck in a * Fix bug where streams with highly uneven durations may get stuck in a
buffering state buffering state
([#7943](https://github.com/google/ExoPlayer/issues/7943)). ([#7943](https://github.com/google/ExoPlayer/issues/7943)).
@ -12,8 +14,8 @@
([#4463](https://github.com/google/ExoPlayer/issues/4463)). ([#4463](https://github.com/google/ExoPlayer/issues/4463)).
* Add a getter and callback for static metadata to the player * Add a getter and callback for static metadata to the player
([#7266](https://github.com/google/ExoPlayer/issues/7266)). ([#7266](https://github.com/google/ExoPlayer/issues/7266)).
* Time out on release to prevent ANRs if the underlying platform call * Time out on release to prevent ANRs if the underlying platform call is
is stuck ([#4352](https://github.com/google/ExoPlayer/issues/4352)). stuck ([#4352](https://github.com/google/ExoPlayer/issues/4352)).
* Time out when detaching a surface to prevent ANRs if the underlying * Time out when detaching a surface to prevent ANRs if the underlying
platform call is stuck platform call is stuck
([#5887](https://github.com/google/ExoPlayer/issues/5887)). ([#5887](https://github.com/google/ExoPlayer/issues/5887)).
@ -48,8 +50,8 @@
([#7949](https://github.com/google/ExoPlayer/issues/7949)). ([#7949](https://github.com/google/ExoPlayer/issues/7949)).
* Fix regression for Ogg files with packets that span multiple pages * Fix regression for Ogg files with packets that span multiple pages
([#7992](https://github.com/google/ExoPlayer/issues/7992)). ([#7992](https://github.com/google/ExoPlayer/issues/7992)).
* Add TS extractor parameter to configure the number of bytes in which * Add TS extractor parameter to configure the number of bytes in which to
to search for a timestamp to determine the duration and to seek. search for a timestamp to determine the duration and to seek.
([#7988](https://github.com/google/ExoPlayer/issues/7988)). ([#7988](https://github.com/google/ExoPlayer/issues/7988)).
* Ignore negative payload size in PES packets * Ignore negative payload size in PES packets
([#8005](https://github.com/google/ExoPlayer/issues/8005)). ([#8005](https://github.com/google/ExoPlayer/issues/8005)).
@ -64,9 +66,9 @@
* Allow apps to specify a `VideoAdPlayerCallback` * Allow apps to specify a `VideoAdPlayerCallback`
([#7944](https://github.com/google/ExoPlayer/issues/7944)). ([#7944](https://github.com/google/ExoPlayer/issues/7944)).
* Accept ad tags via the `AdsMediaSource` constructor and deprecate * Accept ad tags via the `AdsMediaSource` constructor and deprecate
passing them via the `ImaAdsLoader` constructor/builders. Passing the passing them via the `ImaAdsLoader` constructor/builders. Passing the ad
ad tag via media item playback properties continues to be supported. tag via media item playback properties continues to be supported. This
This is in preparation for supporting ads in playlists is in preparation for supporting ads in playlists
([#3750](https://github.com/google/ExoPlayer/issues/3750)). ([#3750](https://github.com/google/ExoPlayer/issues/3750)).
* UI: * UI:

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -129,7 +130,7 @@ public class DefaultLoadControl implements LoadControl {
* @throws IllegalStateException If {@link #build()} has already been called. * @throws IllegalStateException If {@link #build()} has already been called.
*/ */
public Builder setAllocator(DefaultAllocator allocator) { public Builder setAllocator(DefaultAllocator allocator) {
Assertions.checkState(!buildCalled); checkState(!buildCalled);
this.allocator = allocator; this.allocator = allocator;
return this; return this;
} }
@ -154,7 +155,7 @@ public class DefaultLoadControl implements LoadControl {
int maxBufferMs, int maxBufferMs,
int bufferForPlaybackMs, int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs) { int bufferForPlaybackAfterRebufferMs) {
Assertions.checkState(!buildCalled); checkState(!buildCalled);
assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0");
assertGreaterOrEqual( assertGreaterOrEqual(
bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0");
@ -181,7 +182,7 @@ public class DefaultLoadControl implements LoadControl {
* @throws IllegalStateException If {@link #build()} has already been called. * @throws IllegalStateException If {@link #build()} has already been called.
*/ */
public Builder setTargetBufferBytes(int targetBufferBytes) { public Builder setTargetBufferBytes(int targetBufferBytes) {
Assertions.checkState(!buildCalled); checkState(!buildCalled);
this.targetBufferBytes = targetBufferBytes; this.targetBufferBytes = targetBufferBytes;
return this; return this;
} }
@ -196,7 +197,7 @@ public class DefaultLoadControl implements LoadControl {
* @throws IllegalStateException If {@link #build()} has already been called. * @throws IllegalStateException If {@link #build()} has already been called.
*/ */
public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) { public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) {
Assertions.checkState(!buildCalled); checkState(!buildCalled);
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
return this; return this;
} }
@ -212,7 +213,7 @@ public class DefaultLoadControl implements LoadControl {
* @throws IllegalStateException If {@link #build()} has already been called. * @throws IllegalStateException If {@link #build()} has already been called.
*/ */
public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {
Assertions.checkState(!buildCalled); checkState(!buildCalled);
assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0");
this.backBufferDurationMs = backBufferDurationMs; this.backBufferDurationMs = backBufferDurationMs;
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
@ -227,7 +228,7 @@ public class DefaultLoadControl implements LoadControl {
/** Creates a {@link DefaultLoadControl}. */ /** Creates a {@link DefaultLoadControl}. */
public DefaultLoadControl build() { public DefaultLoadControl build() {
Assertions.checkState(!buildCalled); checkState(!buildCalled);
buildCalled = true; buildCalled = true;
if (allocator == null) { if (allocator == null) {
allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE); allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
@ -257,7 +258,7 @@ public class DefaultLoadControl implements LoadControl {
private final boolean retainBackBufferFromKeyframe; private final boolean retainBackBufferFromKeyframe;
private int targetBufferBytes; private int targetBufferBytes;
private boolean isBuffering; private boolean isLoading;
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -394,23 +395,26 @@ public class DefaultLoadControl implements LoadControl {
// Prevent playback from getting stuck if minBufferUs is too small. // Prevent playback from getting stuck if minBufferUs is too small.
minBufferUs = max(minBufferUs, 500_000); minBufferUs = max(minBufferUs, 500_000);
if (bufferedDurationUs < minBufferUs) { if (bufferedDurationUs < minBufferUs) {
isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
if (!isBuffering && bufferedDurationUs < 500_000) { if (!isLoading && bufferedDurationUs < 500_000) {
Log.w( Log.w(
"DefaultLoadControl", "DefaultLoadControl",
"Target buffer size reached with less than 500ms of buffered media data."); "Target buffer size reached with less than 500ms of buffered media data.");
} }
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isBuffering = false; isLoading = false;
} // Else don't change the buffering state } // Else don't change the loading state.
return isBuffering; return isLoading;
} }
@Override @Override
public boolean shouldStartPlayback( public boolean shouldStartPlayback(
long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { long bufferedDurationUs, float playbackSpeed, boolean rebuffering, long targetLiveOffsetUs) {
bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed);
long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
if (targetLiveOffsetUs != C.TIME_UNSET) {
minBufferDurationUs = min(targetLiveOffsetUs / 2, minBufferDurationUs);
}
return minBufferDurationUs <= 0 return minBufferDurationUs <= 0
|| bufferedDurationUs >= minBufferDurationUs || bufferedDurationUs >= minBufferDurationUs
|| (!prioritizeTimeOverSizeThresholds || (!prioritizeTimeOverSizeThresholds
@ -441,7 +445,7 @@ public class DefaultLoadControl implements LoadControl {
targetBufferBytesOverwrite == C.LENGTH_UNSET targetBufferBytesOverwrite == C.LENGTH_UNSET
? DEFAULT_MIN_BUFFER_SIZE ? DEFAULT_MIN_BUFFER_SIZE
: targetBufferBytesOverwrite; : targetBufferBytesOverwrite;
isBuffering = false; isLoading = false;
if (resetAllocator) { if (resetAllocator) {
allocator.reset(); allocator.reset();
} }

View File

@ -1647,10 +1647,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
// Renderers are ready and we're loading. Ask the LoadControl whether to transition. // Renderers are ready and we're loading. Ask the LoadControl whether to transition.
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
int windowIndex =
playbackInfo.timeline.getPeriodByUid(queue.getPlayingPeriod().uid, period).windowIndex;
playbackInfo.timeline.getWindow(windowIndex, window);
long targetLiveOffsetUs =
window.isLive && window.isDynamic
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
: C.TIME_UNSET;
boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
return bufferedToEnd return bufferedToEnd
|| loadControl.shouldStartPlayback( || loadControl.shouldStartPlayback(
getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering); getTotalBufferedDurationUs(),
mediaClock.getPlaybackParameters().speed,
rebuffering,
targetLiveOffsetUs);
} }
private boolean isTimelineReady() { private boolean isTimelineReady() {

View File

@ -25,9 +25,7 @@ import com.google.android.exoplayer2.upstream.Allocator;
*/ */
public interface LoadControl { public interface LoadControl {
/** /** Called by the player when prepared with a new source. */
* Called by the player when prepared with a new source.
*/
void onPrepared(); void onPrepared();
/** /**
@ -113,7 +111,11 @@ public interface LoadControl {
* @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by * @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 * 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. * buffering and when buffering as a result of a seek operation.
* @param targetLiveOffsetUs The desired playback position offset to the live edge in
* microseconds, or {@link C#TIME_UNSET} if the media is not a live stream or no offset is
* configured.
* @return Whether playback should be allowed to start or resume. * @return Whether playback should be allowed to start or resume.
*/ */
boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); boolean shouldStartPlayback(
long bufferedDurationUs, float playbackSpeed, boolean rebuffering, long targetLiveOffsetUs);
} }

View File

@ -174,6 +174,17 @@ public class DefaultLoadControlTest {
.isTrue(); .isTrue();
} }
@Test
public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() {
loadControl = builder.build();
loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new TrackSelectionArray());
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f))
.isTrue();
}
@Test @Test
public void shouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() { public void shouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() {
build(); build();
@ -185,21 +196,117 @@ public class DefaultLoadControlTest {
} }
@Test @Test
public void startsPlayback_whenMinBufferSizeReached() { public void shouldStartPlayback_whenMinBufferSizeReached_returnsTrue() {
build(); build();
assertThat(loadControl.shouldStartPlayback(MIN_BUFFER_US, SPEED, /* rebuffering= */ false)) assertThat(
loadControl.shouldStartPlayback(
MIN_BUFFER_US,
SPEED,
/* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isTrue(); .isTrue();
} }
@Test @Test
public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() { public void
loadControl = builder.build(); shouldStartPlayback_withoutTargetLiveOffset_returnsTrueWhenBufferForPlaybackReached() {
loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new TrackSelectionArray()); builder.setBufferDurationsMs(
/* minBufferMs= */ 5_000,
/* maxBufferMs= */ 20_000,
/* bufferForPlaybackMs= */ 3_000,
/* bufferForPlaybackAfterRebufferMs= */ 4_000);
build();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldStartPlayback(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f)) /* bufferedDurationUs= */ 2_999_999,
SPEED,
/* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 3_000_000,
SPEED,
/* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isTrue();
}
@Test
public void shouldStartPlayback_withTargetLiveOffset_returnsTrueWhenHalfLiveOffsetReached() {
builder.setBufferDurationsMs(
/* minBufferMs= */ 5_000,
/* maxBufferMs= */ 20_000,
/* bufferForPlaybackMs= */ 3_000,
/* bufferForPlaybackAfterRebufferMs= */ 4_000);
build();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 499_999,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 500_000,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
.isTrue();
}
@Test
public void
shouldStartPlayback_afterRebuffer_withoutTargetLiveOffset_whenBufferForPlaybackAfterRebufferReached() {
builder.setBufferDurationsMs(
/* minBufferMs= */ 5_000,
/* maxBufferMs= */ 20_000,
/* bufferForPlaybackMs= */ 3_000,
/* bufferForPlaybackAfterRebufferMs= */ 4_000);
build();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 3_999_999,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 4_000_000,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isTrue();
}
@Test
public void shouldStartPlayback_afterRebuffer_withTargetLiveOffset_whenHalfLiveOffsetReached() {
builder.setBufferDurationsMs(
/* minBufferMs= */ 5_000,
/* maxBufferMs= */ 20_000,
/* bufferForPlaybackMs= */ 3_000,
/* bufferForPlaybackAfterRebufferMs= */ 4_000);
build();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 499_999,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
/* bufferedDurationUs= */ 500_000,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
.isTrue(); .isTrue();
} }

View File

@ -4605,7 +4605,10 @@ public final class ExoPlayerTest {
@Override @Override
public boolean shouldStartPlayback( public boolean shouldStartPlayback(
long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { long bufferedDurationUs,
float playbackSpeed,
boolean rebuffering,
long targetLiveOffsetUs) {
return true; return true;
} }
}; };
@ -4649,7 +4652,10 @@ public final class ExoPlayerTest {
@Override @Override
public boolean shouldStartPlayback( public boolean shouldStartPlayback(
long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { long bufferedDurationUs,
float playbackSpeed,
boolean rebuffering,
long targetLiveOffsetUs) {
return true; return true;
} }
}; };
@ -4724,7 +4730,10 @@ public final class ExoPlayerTest {
@Override @Override
public boolean shouldStartPlayback( public boolean shouldStartPlayback(
long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { long bufferedDurationUs,
float playbackSpeed,
boolean rebuffering,
long targetLiveOffsetUs) {
return false; return false;
} }
}; };