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 new file mode 100644 index 00000000000..2ba7887bd14 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -0,0 +1,237 @@ +/* + * Copyright 2020 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; + +import android.os.SystemClock; +import com.google.android.exoplayer2.MediaItem.LiveConfiguration; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * A {@link LivePlaybackSpeedControl} that adjusts the playback speed using a proportional + * controller. + * + *

The control mechanism calculates the adjusted speed as {@code 1.0 + proportionalControlFactor + * x (currentLiveOffsetSec - targetLiveOffsetSec)}. Unit speed (1.0f) is used, if the {@code + * currentLiveOffsetSec} is marginally close to {@code targetLiveOffsetSec}, i.e. {@code + * |currentLiveOffsetSec - targetLiveOffsetSec| <= MAXIMUM_LIVE_OFFSET_ERROR_US_FOR_UNIT_SPEED}. + * + *

The resulting speed is clamped to a minimum and maximum speed defined by the media, the + * 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. + */ +public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedControl { + + /** + * The default minimum playback speed that should be used if no minimum playback speed is defined + * by the media. + */ + public static final float DEFAULT_FALLBACK_MIN_PLAYBACK_SPEED = 0.97f; + + /** + * The default maximum playback speed that should be used if no maximum playback speed is defined + * by the media. + */ + public static final float DEFAULT_FALLBACK_MAX_PLAYBACK_SPEED = 1.03f; + + /** + * The default {@link Builder#setMinUpdateIntervalMs(long) minimum interval} between playback + * speed changes, in milliseconds. + */ + public static final long DEFAULT_MIN_UPDATE_INTERVAL_MS = 500; + + /** + * The default {@link Builder#setProportionalControlFactor(float) proportional control factor} + * used to adjust the playback speed. + */ + public static final float DEFAULT_PROPORTIONAL_CONTROL_FACTOR = 0.05f; + + /** + * The maximum difference between the current live offset and the target live offset for which + * unit speed (1.0f) is used. + */ + public static final long MAXIMUM_LIVE_OFFSET_ERROR_US_FOR_UNIT_SPEED = 5_000; + + /** Builder for a {@link DefaultLivePlaybackSpeedControl}. */ + public static final class Builder { + + private float fallbackMinPlaybackSpeed; + private float fallbackMaxPlaybackSpeed; + private long minUpdateIntervalMs; + private float proportionalControlFactorUs; + + /** Creates a builder. */ + public Builder() { + fallbackMinPlaybackSpeed = DEFAULT_FALLBACK_MIN_PLAYBACK_SPEED; + fallbackMaxPlaybackSpeed = DEFAULT_FALLBACK_MAX_PLAYBACK_SPEED; + minUpdateIntervalMs = DEFAULT_MIN_UPDATE_INTERVAL_MS; + proportionalControlFactorUs = DEFAULT_PROPORTIONAL_CONTROL_FACTOR / C.MICROS_PER_SECOND; + } + + /** + * Sets the minimum playback speed that should be used if no minimum playback speed is defined + * by the media. + * + *

The default is {@link #DEFAULT_FALLBACK_MIN_PLAYBACK_SPEED}. + * + * @param fallbackMinPlaybackSpeed The fallback minimum playback speed. + * @return This builder, for convenience. + */ + public Builder setFallbackMinPlaybackSpeed(float fallbackMinPlaybackSpeed) { + Assertions.checkArgument(0 < fallbackMinPlaybackSpeed && fallbackMinPlaybackSpeed <= 1f); + this.fallbackMinPlaybackSpeed = fallbackMinPlaybackSpeed; + return this; + } + + /** + * Sets the maximum playback speed that should be used if no maximum playback speed is defined + * by the media. + * + *

The default is {@link #DEFAULT_FALLBACK_MAX_PLAYBACK_SPEED}. + * + * @param fallbackMaxPlaybackSpeed The fallback maximum playback speed. + * @return This builder, for convenience. + */ + public Builder setFallbackMaxPlaybackSpeed(float fallbackMaxPlaybackSpeed) { + Assertions.checkArgument(fallbackMaxPlaybackSpeed >= 1f); + this.fallbackMaxPlaybackSpeed = fallbackMaxPlaybackSpeed; + return this; + } + + /** + * Sets the minimum interval between playback speed changes, in milliseconds. + * + *

The default is {@link #DEFAULT_MIN_UPDATE_INTERVAL_MS}. + * + * @param minUpdateIntervalMs The minimum interval between playback speed changes, in + * milliseconds. + * @return This builder, for convenience. + */ + public Builder setMinUpdateIntervalMs(long minUpdateIntervalMs) { + Assertions.checkArgument(minUpdateIntervalMs >= 0); + this.minUpdateIntervalMs = minUpdateIntervalMs; + return this; + } + + /** + * Sets the proportional control factor used to adjust the playback speed. + * + *

The adjusted playback speed is calculated as {@code 1.0 + proportionalControlFactor x + * (currentLiveOffsetSec - targetLiveOffsetSec)}. + * + *

The default is {@link #DEFAULT_PROPORTIONAL_CONTROL_FACTOR}. + * + * @param proportionalControlFactor The proportional control factor used to adjust the playback + * speed. + * @return This builder, for convenience. + */ + public Builder setProportionalControlFactor(float proportionalControlFactor) { + Assertions.checkArgument(proportionalControlFactor > 0); + this.proportionalControlFactorUs = proportionalControlFactor / C.MICROS_PER_SECOND; + return this; + } + + /** Builds an instance. */ + public DefaultLivePlaybackSpeedControl build() { + return new DefaultLivePlaybackSpeedControl( + fallbackMinPlaybackSpeed, + fallbackMaxPlaybackSpeed, + minUpdateIntervalMs, + proportionalControlFactorUs); + } + } + + private final float fallbackMinPlaybackSpeed; + private final float fallbackMaxPlaybackSpeed; + private final long minUpdateIntervalMs; + private final float proportionalControlFactor; + + private LiveConfiguration mediaConfiguration; + private long targetLiveOffsetOverrideUs; + private float adjustedPlaybackSpeed; + private long lastPlaybackSpeedUpdateMs; + + private DefaultLivePlaybackSpeedControl( + float fallbackMinPlaybackSpeed, + float fallbackMaxPlaybackSpeed, + long minUpdateIntervalMs, + float proportionalControlFactor) { + this.fallbackMinPlaybackSpeed = fallbackMinPlaybackSpeed; + this.fallbackMaxPlaybackSpeed = fallbackMaxPlaybackSpeed; + this.minUpdateIntervalMs = minUpdateIntervalMs; + this.proportionalControlFactor = proportionalControlFactor; + mediaConfiguration = LiveConfiguration.UNSET; + targetLiveOffsetOverrideUs = C.TIME_UNSET; + adjustedPlaybackSpeed = 1.0f; + lastPlaybackSpeedUpdateMs = C.TIME_UNSET; + } + + @Override + public void updateLiveConfiguration(LiveConfiguration liveConfiguration) { + this.mediaConfiguration = liveConfiguration; + lastPlaybackSpeedUpdateMs = C.TIME_UNSET; + } + + @Override + public void overrideTargetLiveOffsetUs(long liveOffsetUs) { + this.targetLiveOffsetOverrideUs = liveOffsetUs; + lastPlaybackSpeedUpdateMs = C.TIME_UNSET; + } + + @Override + public float adjustPlaybackSpeed(long liveOffsetUs) { + long targetLiveOffsetUs = getTargetLiveOffsetUs(); + if (targetLiveOffsetUs == C.TIME_UNSET) { + return 1f; + } + if (lastPlaybackSpeedUpdateMs != C.TIME_UNSET + && SystemClock.elapsedRealtime() - lastPlaybackSpeedUpdateMs < minUpdateIntervalMs) { + return adjustedPlaybackSpeed; + } + lastPlaybackSpeedUpdateMs = SystemClock.elapsedRealtime(); + + long liveOffsetErrorUs = liveOffsetUs - targetLiveOffsetUs; + if (Math.abs(liveOffsetErrorUs) < MAXIMUM_LIVE_OFFSET_ERROR_US_FOR_UNIT_SPEED) { + adjustedPlaybackSpeed = 1f; + } else { + float calculatedSpeed = 1f + proportionalControlFactor * liveOffsetErrorUs; + adjustedPlaybackSpeed = + Util.constrainValue(calculatedSpeed, getMinPlaybackSpeed(), getMaxPlaybackSpeed()); + } + return adjustedPlaybackSpeed; + } + + @Override + public long getTargetLiveOffsetUs() { + return targetLiveOffsetOverrideUs != C.TIME_UNSET + && mediaConfiguration.targetLiveOffsetMs != C.TIME_UNSET + ? targetLiveOffsetOverrideUs + : C.msToUs(mediaConfiguration.targetLiveOffsetMs); + } + + private float getMinPlaybackSpeed() { + return mediaConfiguration.minPlaybackSpeed != C.RATE_UNSET + ? mediaConfiguration.minPlaybackSpeed + : fallbackMinPlaybackSpeed; + } + + private float getMaxPlaybackSpeed() { + return mediaConfiguration.maxPlaybackSpeed != C.RATE_UNSET + ? mediaConfiguration.maxPlaybackSpeed + : fallbackMaxPlaybackSpeed; + } +} 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 new file mode 100644 index 00000000000..e2f63df4b30 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 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; + +import com.google.android.exoplayer2.MediaItem.LiveConfiguration; + +/** + * Controls the playback speed while playing live content in order to maintain a steady target live + * offset. + */ +public interface LivePlaybackSpeedControl { + + /** + * Updates the live configuration defined by the media. + * + * @param liveConfiguration The {@link LiveConfiguration} as defined by the media. + */ + void updateLiveConfiguration(LiveConfiguration liveConfiguration); + + /** + * Overrides the {@link #updateLiveConfiguration configured} target live offset in microseconds, + * or {@code C.TIME_UNSET} to delete a previous override. + * + *

If no target live offset is configured by {@link #updateLiveConfiguration}, this override + * has no effect. + */ + void overrideTargetLiveOffsetUs(long liveOffsetUs); + + /** + * Returns the adjusted playback speed in order get closer towards the {@link + * #getTargetLiveOffsetUs() target live offset}. + * + * @param liveOffsetUs The current live offset, in microseconds. + * @return The adjusted playback speed. + */ + float adjustPlaybackSpeed(long liveOffsetUs); + + /** + * Returns the current target live offset, in microseconds, or {@link C#TIME_UNSET} if no target + * live offset is defined for the current media. + */ + long getTargetLiveOffsetUs(); +} 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 new file mode 100644 index 00000000000..57155b9a18c --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java @@ -0,0 +1,303 @@ +/* + * Copyright 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem.LiveConfiguration; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowSystemClock; + +/** Unit test for {@link DefaultLivePlaybackSpeedControl}. */ +@RunWith(AndroidJUnit4.class) +public class DefaultLivePlaybackSpeedControlTest { + + @Test + public void getTargetLiveOffsetUs_returnsUnset() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + + assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(C.TIME_UNSET); + } + + @Test + public void getTargetLiveOffsetUs_afterUpdateLiveConfiguration_usesMediaLiveOffset() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 42, /* minPlaybackSpeed= */ 1f, /* maxPlaybackSpeed= */ 1f)); + + assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(42_000); + } + + @Test + public void getTargetLiveOffsetUs_withOverrideTargetLiveOffsetUs_usesOverride() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + + defaultLivePlaybackSpeedControl.overrideTargetLiveOffsetUs(123_456_789); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 42, /* minPlaybackSpeed= */ 1f, /* maxPlaybackSpeed= */ 1f)); + + long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); + + assertThat(targetLiveOffsetUs).isEqualTo(123_456_789); + } + + @Test + public void + getTargetLiveOffsetUs_afterOverrideTargetLiveOffset_withoutMediaConfiguration_returnsUnset() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.overrideTargetLiveOffsetUs(123_456_789); + + long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); + + assertThat(targetLiveOffsetUs).isEqualTo(C.TIME_UNSET); + } + + @Test + public void + getTargetLiveOffsetUs_afterOverrideTargetLiveOffsetUsWithTimeUnset_usesMediaLiveOffset() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.overrideTargetLiveOffsetUs(123_456_789); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 42, /* minPlaybackSpeed= */ 1f, /* maxPlaybackSpeed= */ 1f)); + defaultLivePlaybackSpeedControl.overrideTargetLiveOffsetUs(C.TIME_UNSET); + + long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); + + assertThat(targetLiveOffsetUs).isEqualTo(42_000); + } + + @Test + public void adjustPlaybackSpeed_liveOffsetMatchesTargetOffset_returnsUnitSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 2_000_000); + + assertThat(adjustedSpeed).isEqualTo(1f); + } + + @Test + public void adjustPlaybackSpeed_liveOffsetWithinAcceptableErrorMargin_returnsUnitSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeedJustAboveLowerErrorMargin = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed( + /* liveOffsetUs= */ 2_000_000 + - DefaultLivePlaybackSpeedControl.MAXIMUM_LIVE_OFFSET_ERROR_US_FOR_UNIT_SPEED + + 1); + float adjustedSpeedJustBelowUpperErrorMargin = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed( + /* liveOffsetUs= */ 2_000_000 + + DefaultLivePlaybackSpeedControl.MAXIMUM_LIVE_OFFSET_ERROR_US_FOR_UNIT_SPEED + - 1); + + assertThat(adjustedSpeedJustAboveLowerErrorMargin).isEqualTo(1f); + assertThat(adjustedSpeedJustBelowUpperErrorMargin).isEqualTo(1f); + } + + @Test + public void adjustPlaybackSpeed_withLiveOffsetGreaterThanTargetOffset_returnsAdjustedSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setProportionalControlFactor(0.01f).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 2_500_000); + + float expectedSpeedAccordingToDocumentation = 1f + 0.01f * (2.5f - 2f); + assertThat(adjustedSpeed).isEqualTo(expectedSpeedAccordingToDocumentation); + assertThat(adjustedSpeed).isGreaterThan(1f); + } + + @Test + public void adjustPlaybackSpeed_withLiveOffsetLowerThanTargetOffset_returnsAdjustedSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setProportionalControlFactor(0.01f).build(); + defaultLivePlaybackSpeedControl.overrideTargetLiveOffsetUs(2_000_000); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 1_500_000); + + float expectedSpeedAccordingToDocumentation = 1f + 0.01f * (1.5f - 2f); + assertThat(adjustedSpeed).isEqualTo(expectedSpeedAccordingToDocumentation); + assertThat(adjustedSpeed).isLessThan(1f); + } + + @Test + public void + adjustPlaybackSpeed_withLiveOffsetGreaterThanTargetOffset_clampedToFallbackMaximumSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setFallbackMaxPlaybackSpeed(1.5f).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 999_999_999_999L); + + assertThat(adjustedSpeed).isEqualTo(1.5f); + } + + @Test + public void + adjustPlaybackSpeed_withLiveOffsetLowerThanTargetOffset_clampedToFallbackMinimumSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setFallbackMinPlaybackSpeed(0.5f).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ -999_999_999_999L); + + assertThat(adjustedSpeed).isEqualTo(0.5f); + } + + @Test + public void + adjustPlaybackSpeed_andMediaProvidedMaxSpeedWithLiveOffsetGreaterThanTargetOffset_clampedToMediaMaxSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setFallbackMaxPlaybackSpeed(1.5f).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ 2f)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 999_999_999_999L); + + assertThat(adjustedSpeed).isEqualTo(2f); + } + + @Test + public void + adjustPlaybackSpeed_andMediaProvidedMinSpeedWithLiveOffsetLowerThanTargetOffset_clampedToMediaMinSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setFallbackMinPlaybackSpeed(0.5f).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ 0.2f, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ -999_999_999_999L); + + assertThat(adjustedSpeed).isEqualTo(0.2f); + } + + @Test + public void adjustPlaybackSpeed_repeatedCallWithinMinUpdateInterval_returnsSameAdjustedSpeed() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed1 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 1_500_000); + ShadowSystemClock.advanceBy(Duration.ofMillis(122)); + float adjustedSpeed2 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 2_500_000); + ShadowSystemClock.advanceBy(Duration.ofMillis(2)); + float adjustedSpeed3 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 2_500_000); + + assertThat(adjustedSpeed1).isEqualTo(adjustedSpeed2); + assertThat(adjustedSpeed3).isNotEqualTo(adjustedSpeed2); + } + + @Test + public void adjustPlaybackSpeed_repeatedCallAfterUpdateLiveConfiguration_updatesSpeedAgain() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed1 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 1_500_000); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + float adjustedSpeed2 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 2_500_000); + + assertThat(adjustedSpeed1).isNotEqualTo(adjustedSpeed2); + } + + @Test + public void adjustPlaybackSpeed_repeatedCallAfterNewTargetLiveOffset_updatesSpeedAgain() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build(); + defaultLivePlaybackSpeedControl.updateLiveConfiguration( + new LiveConfiguration( + /* targetLiveOffsetMs= */ 2_000, + /* minPlaybackSpeed= */ C.RATE_UNSET, + /* maxPlaybackSpeed= */ C.RATE_UNSET)); + + float adjustedSpeed1 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 1_500_000); + defaultLivePlaybackSpeedControl.overrideTargetLiveOffsetUs(2_000_001); + float adjustedSpeed2 = + defaultLivePlaybackSpeedControl.adjustPlaybackSpeed(/* liveOffsetUs= */ 2_500_000); + + assertThat(adjustedSpeed1).isNotEqualTo(adjustedSpeed2); + } +}