From 8fdadade7bae596b66d38513e1259dc749b8c4ab Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 13 Oct 2020 14:27:45 +0100 Subject: [PATCH] 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 --- RELEASENOTES.md | 16 ++- .../exoplayer2/DefaultLoadControl.java | 32 +++-- .../exoplayer2/ExoPlayerImplInternal.java | 12 +- .../android/exoplayer2/LoadControl.java | 10 +- .../exoplayer2/DefaultLoadControlTest.java | 121 +++++++++++++++++- .../android/exoplayer2/ExoPlayerTest.java | 15 ++- 6 files changed, 170 insertions(+), 36 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 71371e2ac5..8eae3298c2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,8 @@ ### dev-v2 (not yet released) * Core library: + * `LoadControl`: + * Add a `targetLiveOffsetUs` parameter to `shouldStartPlayback`. * Fix bug where streams with highly uneven durations may get stuck in a buffering state ([#7943](https://github.com/google/ExoPlayer/issues/7943)). @@ -12,8 +14,8 @@ ([#4463](https://github.com/google/ExoPlayer/issues/4463)). * Add a getter and callback for static metadata to the player ([#7266](https://github.com/google/ExoPlayer/issues/7266)). - * Time out on release to prevent ANRs if the underlying platform call - is stuck ([#4352](https://github.com/google/ExoPlayer/issues/4352)). + * Time out on release to prevent ANRs if the underlying platform call is + stuck ([#4352](https://github.com/google/ExoPlayer/issues/4352)). * Time out when detaching a surface to prevent ANRs if the underlying platform call is stuck ([#5887](https://github.com/google/ExoPlayer/issues/5887)). @@ -48,8 +50,8 @@ ([#7949](https://github.com/google/ExoPlayer/issues/7949)). * Fix regression for Ogg files with packets that span multiple pages ([#7992](https://github.com/google/ExoPlayer/issues/7992)). - * Add TS extractor parameter to configure the number of bytes in which - to search for a timestamp to determine the duration and to seek. + * Add TS extractor parameter to configure the number of bytes in which to + search for a timestamp to determine the duration and to seek. ([#7988](https://github.com/google/ExoPlayer/issues/7988)). * Ignore negative payload size in PES packets ([#8005](https://github.com/google/ExoPlayer/issues/8005)). @@ -64,9 +66,9 @@ * Allow apps to specify a `VideoAdPlayerCallback` ([#7944](https://github.com/google/ExoPlayer/issues/7944)). * Accept ad tags via the `AdsMediaSource` constructor and deprecate - passing them via the `ImaAdsLoader` constructor/builders. Passing the - ad tag via media item playback properties continues to be supported. - This is in preparation for supporting ads in playlists + passing them via the `ImaAdsLoader` constructor/builders. Passing the ad + tag via media item playback properties continues to be supported. This + is in preparation for supporting ads in playlists ([#3750](https://github.com/google/ExoPlayer/issues/3750)). * UI: 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 2b72fc6c09..f80edcf334 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 @@ -15,6 +15,7 @@ */ 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.min; @@ -129,7 +130,7 @@ public class DefaultLoadControl implements LoadControl { * @throws IllegalStateException If {@link #build()} has already been called. */ public Builder setAllocator(DefaultAllocator allocator) { - Assertions.checkState(!buildCalled); + checkState(!buildCalled); this.allocator = allocator; return this; } @@ -154,7 +155,7 @@ public class DefaultLoadControl implements LoadControl { int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs) { - Assertions.checkState(!buildCalled); + checkState(!buildCalled); assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); assertGreaterOrEqual( bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); @@ -181,7 +182,7 @@ public class DefaultLoadControl implements LoadControl { * @throws IllegalStateException If {@link #build()} has already been called. */ public Builder setTargetBufferBytes(int targetBufferBytes) { - Assertions.checkState(!buildCalled); + checkState(!buildCalled); this.targetBufferBytes = targetBufferBytes; return this; } @@ -196,7 +197,7 @@ public class DefaultLoadControl implements LoadControl { * @throws IllegalStateException If {@link #build()} has already been called. */ public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) { - Assertions.checkState(!buildCalled); + checkState(!buildCalled); this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; return this; } @@ -212,7 +213,7 @@ public class DefaultLoadControl implements LoadControl { * @throws IllegalStateException If {@link #build()} has already been called. */ public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { - Assertions.checkState(!buildCalled); + checkState(!buildCalled); assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); this.backBufferDurationMs = backBufferDurationMs; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; @@ -227,7 +228,7 @@ public class DefaultLoadControl implements LoadControl { /** Creates a {@link DefaultLoadControl}. */ public DefaultLoadControl build() { - Assertions.checkState(!buildCalled); + checkState(!buildCalled); buildCalled = true; if (allocator == null) { allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE); @@ -257,7 +258,7 @@ public class DefaultLoadControl implements LoadControl { private final boolean retainBackBufferFromKeyframe; private int targetBufferBytes; - private boolean isBuffering; + private boolean isLoading; /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ @SuppressWarnings("deprecation") @@ -394,23 +395,26 @@ public class DefaultLoadControl implements LoadControl { // Prevent playback from getting stuck if minBufferUs is too small. minBufferUs = max(minBufferUs, 500_000); if (bufferedDurationUs < minBufferUs) { - isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; - if (!isBuffering && bufferedDurationUs < 500_000) { + isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; + if (!isLoading && bufferedDurationUs < 500_000) { Log.w( "DefaultLoadControl", "Target buffer size reached with less than 500ms of buffered media data."); } } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { - isBuffering = false; - } // Else don't change the buffering state - return isBuffering; + isLoading = false; + } // Else don't change the loading state. + return isLoading; } @Override public boolean shouldStartPlayback( - long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + long bufferedDurationUs, float playbackSpeed, boolean rebuffering, long targetLiveOffsetUs) { bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; + if (targetLiveOffsetUs != C.TIME_UNSET) { + minBufferDurationUs = min(targetLiveOffsetUs / 2, minBufferDurationUs); + } return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds @@ -441,7 +445,7 @@ public class DefaultLoadControl implements LoadControl { targetBufferBytesOverwrite == C.LENGTH_UNSET ? DEFAULT_MIN_BUFFER_SIZE : targetBufferBytesOverwrite; - isBuffering = false; + isLoading = false; if (resetAllocator) { allocator.reset(); } 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 9ee50e1f89..65eff24499 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 @@ -1647,10 +1647,20 @@ import java.util.concurrent.atomic.AtomicBoolean; } // Renderers are ready and we're loading. Ask the LoadControl whether to transition. 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; return bufferedToEnd || loadControl.shouldStartPlayback( - getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering); + getTotalBufferedDurationUs(), + mediaClock.getPlaybackParameters().speed, + rebuffering, + targetLiveOffsetUs); } private boolean isTimelineReady() { 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 94f61bb618..2f3a665f75 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 @@ -25,9 +25,7 @@ import com.google.android.exoplayer2.upstream.Allocator; */ 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(); /** @@ -113,7 +111,11 @@ public interface LoadControl { * @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. + * @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. */ - boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); + boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering, long targetLiveOffsetUs); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java index b00da4390a..241da059ab 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java @@ -174,6 +174,17 @@ public class DefaultLoadControlTest { .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 public void shouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() { build(); @@ -185,21 +196,117 @@ public class DefaultLoadControlTest { } @Test - public void startsPlayback_whenMinBufferSizeReached() { + public void shouldStartPlayback_whenMinBufferSizeReached_returnsTrue() { build(); - assertThat(loadControl.shouldStartPlayback(MIN_BUFFER_US, SPEED, /* rebuffering= */ false)) + assertThat( + loadControl.shouldStartPlayback( + MIN_BUFFER_US, + SPEED, + /* rebuffering= */ false, + /* targetLiveOffsetUs= */ C.TIME_UNSET)) .isTrue(); } @Test - public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() { - loadControl = builder.build(); - loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new TrackSelectionArray()); + public void + shouldStartPlayback_withoutTargetLiveOffset_returnsTrueWhenBufferForPlaybackReached() { + builder.setBufferDurationsMs( + /* minBufferMs= */ 5_000, + /* maxBufferMs= */ 20_000, + /* bufferForPlaybackMs= */ 3_000, + /* bufferForPlaybackAfterRebufferMs= */ 4_000); + build(); assertThat( - loadControl.shouldContinueLoading( - /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f)) + loadControl.shouldStartPlayback( + /* 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(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 1e4804421f..47fb993299 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -4605,7 +4605,10 @@ public final class ExoPlayerTest { @Override public boolean shouldStartPlayback( - long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + long bufferedDurationUs, + float playbackSpeed, + boolean rebuffering, + long targetLiveOffsetUs) { return true; } }; @@ -4649,7 +4652,10 @@ public final class ExoPlayerTest { @Override public boolean shouldStartPlayback( - long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + long bufferedDurationUs, + float playbackSpeed, + boolean rebuffering, + long targetLiveOffsetUs) { return true; } }; @@ -4724,7 +4730,10 @@ public final class ExoPlayerTest { @Override public boolean shouldStartPlayback( - long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + long bufferedDurationUs, + float playbackSpeed, + boolean rebuffering, + long targetLiveOffsetUs) { return false; } };