diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index 63de8cf581..6ea6f08321 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -33,6 +33,10 @@ import com.google.android.exoplayer2.util.Util; * fallback values set with {@link Builder#setFallbackMinPlaybackSpeed(float)} and {@link * Builder#setFallbackMaxPlaybackSpeed(float)} or the {@link #DEFAULT_FALLBACK_MIN_PLAYBACK_SPEED * minimum} and {@link #DEFAULT_FALLBACK_MAX_PLAYBACK_SPEED maximum} fallback default values. + * + *
When the player rebuffers, the target live offset {@link + * Builder#setTargetLiveOffsetIncrementOnRebufferMs(long) is increased} to adjust to the reduced + * network capabilities. */ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedControl { @@ -60,6 +64,12 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC */ public static final float DEFAULT_PROPORTIONAL_CONTROL_FACTOR = 0.05f; + /** + * The default increment applied to the target live offset each time the player is rebuffering, in + * milliseconds + */ + public static final long DEFAULT_TARGET_LIVE_OFFSET_INCREMENT_ON_REBUFFER_MS = 500; + /** * The maximum difference between the current live offset and the target live offset for which * unit speed (1.0f) is used. @@ -73,6 +83,7 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC private float fallbackMaxPlaybackSpeed; private long minUpdateIntervalMs; private float proportionalControlFactorUs; + private long targetLiveOffsetIncrementOnRebufferUs; /** Creates a builder. */ public Builder() { @@ -80,6 +91,8 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC fallbackMaxPlaybackSpeed = DEFAULT_FALLBACK_MAX_PLAYBACK_SPEED; minUpdateIntervalMs = DEFAULT_MIN_UPDATE_INTERVAL_MS; proportionalControlFactorUs = DEFAULT_PROPORTIONAL_CONTROL_FACTOR / C.MICROS_PER_SECOND; + targetLiveOffsetIncrementOnRebufferUs = + C.msToUs(DEFAULT_TARGET_LIVE_OFFSET_INCREMENT_ON_REBUFFER_MS); } /** @@ -145,13 +158,29 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC return this; } + /** + * Sets the increment applied to the target live offset each time the player is rebuffering, in + * milliseconds. + * + * @param targetLiveOffsetIncrementOnRebufferMs The increment applied to the target live offset + * when the player is rebuffering, in milliseconds + * @return This builder, for convenience. + */ + public Builder setTargetLiveOffsetIncrementOnRebufferMs( + long targetLiveOffsetIncrementOnRebufferMs) { + Assertions.checkArgument(targetLiveOffsetIncrementOnRebufferMs >= 0); + this.targetLiveOffsetIncrementOnRebufferUs = C.msToUs(targetLiveOffsetIncrementOnRebufferMs); + return this; + } + /** Builds an instance. */ public DefaultLivePlaybackSpeedControl build() { return new DefaultLivePlaybackSpeedControl( fallbackMinPlaybackSpeed, fallbackMaxPlaybackSpeed, minUpdateIntervalMs, - proportionalControlFactorUs); + proportionalControlFactorUs, + targetLiveOffsetIncrementOnRebufferUs); } } @@ -159,9 +188,11 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC private final float fallbackMaxPlaybackSpeed; private final long minUpdateIntervalMs; private final float proportionalControlFactor; + private final long targetLiveOffsetRebufferDeltaUs; private long mediaConfigurationTargetLiveOffsetUs; private long targetLiveOffsetOverrideUs; + private long idealTargetLiveOffsetUs; private long minTargetLiveOffsetUs; private long maxTargetLiveOffsetUs; private long currentTargetLiveOffsetUs; @@ -175,11 +206,13 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC float fallbackMinPlaybackSpeed, float fallbackMaxPlaybackSpeed, long minUpdateIntervalMs, - float proportionalControlFactor) { + float proportionalControlFactor, + long targetLiveOffsetRebufferDeltaUs) { this.fallbackMinPlaybackSpeed = fallbackMinPlaybackSpeed; this.fallbackMaxPlaybackSpeed = fallbackMaxPlaybackSpeed; this.minUpdateIntervalMs = minUpdateIntervalMs; this.proportionalControlFactor = proportionalControlFactor; + this.targetLiveOffsetRebufferDeltaUs = targetLiveOffsetRebufferDeltaUs; mediaConfigurationTargetLiveOffsetUs = C.TIME_UNSET; targetLiveOffsetOverrideUs = C.TIME_UNSET; minTargetLiveOffsetUs = C.TIME_UNSET; @@ -188,6 +221,7 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC maxPlaybackSpeed = fallbackMaxPlaybackSpeed; adjustedPlaybackSpeed = 1.0f; lastPlaybackSpeedUpdateMs = C.TIME_UNSET; + idealTargetLiveOffsetUs = C.TIME_UNSET; currentTargetLiveOffsetUs = C.TIME_UNSET; } @@ -213,6 +247,19 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC maybeResetTargetLiveOffsetUs(); } + @Override + public void notifyRebuffer() { + if (currentTargetLiveOffsetUs == C.TIME_UNSET) { + return; + } + currentTargetLiveOffsetUs += targetLiveOffsetRebufferDeltaUs; + if (maxTargetLiveOffsetUs != C.TIME_UNSET + && currentTargetLiveOffsetUs > maxTargetLiveOffsetUs) { + currentTargetLiveOffsetUs = maxTargetLiveOffsetUs; + } + lastPlaybackSpeedUpdateMs = C.TIME_UNSET; + } + @Override public float getAdjustedPlaybackSpeed(long liveOffsetUs) { if (mediaConfigurationTargetLiveOffsetUs == C.TIME_UNSET) { @@ -254,9 +301,10 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC idealOffsetUs = maxTargetLiveOffsetUs; } } - if (currentTargetLiveOffsetUs == idealOffsetUs) { + if (idealTargetLiveOffsetUs == idealOffsetUs) { return; } + idealTargetLiveOffsetUs = idealOffsetUs; currentTargetLiveOffsetUs = idealOffsetUs; lastPlaybackSpeedUpdateMs = C.TIME_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 21b7635b40..20a5201e0d 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 @@ -189,7 +189,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean released; private boolean pauseAtEndOfWindow; private boolean pendingPauseAtEndOfPeriod; - private boolean rebuffering; + private boolean isRebuffering; private boolean shouldContinueLoading; @Player.RepeatMode private int repeatMode; private boolean shuffleModeEnabled; @@ -733,7 +733,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0); playbackInfoUpdate.setPlayWhenReadyChangeReason(reason); playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); - rebuffering = false; + isRebuffering = false; if (!shouldPlayWhenReady()) { stopRenderers(); updatePlaybackPositions(); @@ -811,7 +811,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void startRenderers() throws ExoPlaybackException { - rebuffering = false; + isRebuffering = false; mediaClock.start(); for (Renderer renderer : renderers) { if (isRendererEnabled(renderer)) { @@ -868,6 +868,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // Adjust live playback speed to new position. if (playbackInfo.playWhenReady + && playbackInfo.playbackState == Player.STATE_READY && isCurrentPeriodInMovingLiveWindow() && playbackInfo.playbackParameters.speed == 1f) { float adjustedSpeed = @@ -960,8 +961,9 @@ import java.util.concurrent.atomic.AtomicBoolean; } } else if (playbackInfo.playbackState == Player.STATE_READY && !(enabledRendererCount == 0 ? isTimelineReady() : renderersAllowPlayback)) { - rebuffering = shouldPlayWhenReady(); + isRebuffering = shouldPlayWhenReady(); setState(Player.STATE_BUFFERING); + livePlaybackSpeedControl.notifyRebuffer(); stopRenderers(); } @@ -1168,7 +1170,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean forceBufferingState) throws ExoPlaybackException { stopRenderers(); - rebuffering = false; + isRebuffering = false; if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) { setState(Player.STATE_BUFFERING); } @@ -1311,7 +1313,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean releaseMediaSourceList, boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); - rebuffering = false; + isRebuffering = false; mediaClock.stop(); rendererPositionUs = 0; for (Renderer renderer : renderers) { @@ -1701,7 +1703,7 @@ import java.util.concurrent.atomic.AtomicBoolean; || loadControl.shouldStartPlayback( getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, - rebuffering, + isRebuffering, targetLiveOffsetUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java index 03aa325307..8844c62908 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java @@ -40,6 +40,14 @@ public interface LivePlaybackSpeedControl { */ void setTargetLiveOffsetOverrideUs(long liveOffsetUs); + /** + * Notifies the live playback speed control that a rebuffer occurred. + * + *
A rebuffer is defined to be caused by buffer depletion rather than a user action. Hence this
+ * method is not called during initial or when buffering as a result of a seek operation.
+ */
+ void notifyRebuffer();
+
/**
* Returns the adjusted playback speed in order get closer towards the {@link
* #getTargetLiveOffsetUs() target live offset}.
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java
index 8ec49ebabc..9c08b9999b 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java
@@ -19,7 +19,10 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem.LiveConfiguration;
+import com.google.common.collect.Iterables;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowSystemClock;
@@ -37,7 +40,7 @@ public class DefaultLivePlaybackSpeedControlTest {
}
@Test
- public void getTargetLiveOffsetUs_afterSetLiveConfiguration_usesMediaLiveOffset() {
+ public void getTargetLiveOffsetUs_afterSetLiveConfiguration_returnsMediaLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
@@ -53,7 +56,7 @@ public class DefaultLivePlaybackSpeedControlTest {
@Test
public void
- getTargetLiveOffsetUs_afterSetLiveConfigurationWithTargetGreaterThanMax_usesMaxLiveOffset() {
+ getTargetLiveOffsetUs_afterSetLiveConfigurationWithTargetGreaterThanMax_returnsMaxLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
@@ -69,7 +72,7 @@ public class DefaultLivePlaybackSpeedControlTest {
@Test
public void
- getTargetLiveOffsetUs_afterSetLiveConfigurationWithTargetLessThanMin_usesMinLiveOffset() {
+ getTargetLiveOffsetUs_afterSetLiveConfigurationWithTargetLessThanMin_returnsMinLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
@@ -84,7 +87,7 @@ public class DefaultLivePlaybackSpeedControlTest {
}
@Test
- public void getTargetLiveOffsetUs_withSetTargetLiveOffsetOverrideUs_usesOverride() {
+ public void getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideUs_returnsOverride() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
@@ -104,7 +107,7 @@ public class DefaultLivePlaybackSpeedControlTest {
@Test
public void
- getTargetLiveOffsetUs_withSetTargetLiveOffsetOverrideUsGreaterThanMax_usesMaxLiveOffset() {
+ getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideUsGreaterThanMax_returnsMaxLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
@@ -124,7 +127,7 @@ public class DefaultLivePlaybackSpeedControlTest {
@Test
public void
- getTargetLiveOffsetUs_withSetTargetLiveOffsetOverrideUsLessThanMin_usesMinLiveOffset() {
+ getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideUsLessThanMin_returnsMinLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
@@ -156,7 +159,7 @@ public class DefaultLivePlaybackSpeedControlTest {
@Test
public void
- getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideWithTimeUnset_usesMediaLiveOffset() {
+ getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideWithTimeUnset_returnsMediaLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789);
@@ -174,6 +177,153 @@ public class DefaultLivePlaybackSpeedControlTest {
assertThat(targetLiveOffsetUs).isEqualTo(42_000);
}
+ @Test
+ public void getTargetLiveOffsetUs_afterNotifyRebuffer_returnsIncreasedTargetOffset() {
+ DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
+ new DefaultLivePlaybackSpeedControl.Builder()
+ .setTargetLiveOffsetIncrementOnRebufferMs(3)
+ .build();
+ defaultLivePlaybackSpeedControl.setLiveConfiguration(
+ new LiveConfiguration(
+ /* targetLiveOffsetMs= */ 42,
+ /* minLiveOffsetMs= */ 5,
+ /* maxLiveOffsetMs= */ 400,
+ /* minPlaybackSpeed= */ 1f,
+ /* maxPlaybackSpeed= */ 1f));
+
+ long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs();
+ defaultLivePlaybackSpeedControl.notifyRebuffer();
+ long targetLiveOffsetAfterUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs();
+
+ assertThat(targetLiveOffsetAfterUs).isGreaterThan(targetLiveOffsetBeforeUs);
+ assertThat(targetLiveOffsetAfterUs - targetLiveOffsetBeforeUs).isEqualTo(3_000);
+ }
+
+ @Test
+ public void getTargetLiveOffsetUs_afterRepeatedNotifyRebuffer_returnsMaxLiveOffset() {
+ DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
+ new DefaultLivePlaybackSpeedControl.Builder()
+ .setTargetLiveOffsetIncrementOnRebufferMs(3)
+ .build();
+ defaultLivePlaybackSpeedControl.setLiveConfiguration(
+ new LiveConfiguration(
+ /* targetLiveOffsetMs= */ 42,
+ /* minLiveOffsetMs= */ 5,
+ /* maxLiveOffsetMs= */ 400,
+ /* minPlaybackSpeed= */ 1f,
+ /* maxPlaybackSpeed= */ 1f));
+
+ List