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:
parent
76b7f76437
commit
8fdadade7b
@ -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:
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user