/* * 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.primitives.Longs.max; import static java.lang.Math.abs; import static java.lang.Math.max; 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; import com.google.errorprone.annotations.CanIgnoreReturnValue; /** * 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 closer to {@code targetLiveOffsetSec} than the value set with {@link * Builder#setMaxLiveOffsetErrorMsForUnitSpeed(long)}. * *

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. * *

When the player rebuffers, the target live offset {@link * Builder#setTargetLiveOffsetIncrementOnRebufferMs(long) is increased} to adjust to the reduced * network capabilities. The live playback speed control also {@link * Builder#setMinPossibleLiveOffsetSmoothingFactor(float) keeps track} of the minimum possible live * offset to decrease the target live offset again if conditions improve. The minimum possible live * offset is derived from the current offset and the duration of buffered media. */ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedControl { /** * The default minimum factor by which playback can be sped up 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 factor by which playback can be sped up 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 = 1_000; /** * The default {@link Builder#setProportionalControlFactor(float) proportional control factor} * used to adjust the playback speed. */ public static final float DEFAULT_PROPORTIONAL_CONTROL_FACTOR = 0.1f; /** * 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 default smoothing factor when smoothing the minimum possible live offset that can be * achieved during playback. */ public static final float DEFAULT_MIN_POSSIBLE_LIVE_OFFSET_SMOOTHING_FACTOR = 0.999f; /** * The default maximum difference between the current live offset and the target live offset, in * milliseconds, for which unit speed (1.0f) is used. */ public static final long DEFAULT_MAX_LIVE_OFFSET_ERROR_MS_FOR_UNIT_SPEED = 20; /** Builder for a {@link DefaultLivePlaybackSpeedControl}. */ public static final class Builder { private float fallbackMinPlaybackSpeed; private float fallbackMaxPlaybackSpeed; private long minUpdateIntervalMs; private float proportionalControlFactorUs; private long maxLiveOffsetErrorUsForUnitSpeed; private long targetLiveOffsetIncrementOnRebufferUs; private float minPossibleLiveOffsetSmoothingFactor; /** 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; maxLiveOffsetErrorUsForUnitSpeed = Util.msToUs(DEFAULT_MAX_LIVE_OFFSET_ERROR_MS_FOR_UNIT_SPEED); targetLiveOffsetIncrementOnRebufferUs = Util.msToUs(DEFAULT_TARGET_LIVE_OFFSET_INCREMENT_ON_REBUFFER_MS); minPossibleLiveOffsetSmoothingFactor = DEFAULT_MIN_POSSIBLE_LIVE_OFFSET_SMOOTHING_FACTOR; } /** * 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 factor by which playback can be sped up. * @return This builder, for convenience. */ @CanIgnoreReturnValue 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 factor by which playback can be sped up. * @return This builder, for convenience. */ @CanIgnoreReturnValue 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. */ @CanIgnoreReturnValue 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 factor by which playback will be sped up 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. */ @CanIgnoreReturnValue public Builder setProportionalControlFactor(float proportionalControlFactor) { Assertions.checkArgument(proportionalControlFactor > 0); this.proportionalControlFactorUs = proportionalControlFactor / C.MICROS_PER_SECOND; return this; } /** * Sets the maximum difference between the current live offset and the target live offset, in * milliseconds, for which unit speed (1.0f) is used. * *

The default is {@link #DEFAULT_MAX_LIVE_OFFSET_ERROR_MS_FOR_UNIT_SPEED}. * * @param maxLiveOffsetErrorMsForUnitSpeed The maximum live offset error for which unit speed is * used, in milliseconds. * @return This builder, for convenience. */ @CanIgnoreReturnValue public Builder setMaxLiveOffsetErrorMsForUnitSpeed(long maxLiveOffsetErrorMsForUnitSpeed) { Assertions.checkArgument(maxLiveOffsetErrorMsForUnitSpeed > 0); this.maxLiveOffsetErrorUsForUnitSpeed = Util.msToUs(maxLiveOffsetErrorMsForUnitSpeed); 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. */ @CanIgnoreReturnValue public Builder setTargetLiveOffsetIncrementOnRebufferMs( long targetLiveOffsetIncrementOnRebufferMs) { Assertions.checkArgument(targetLiveOffsetIncrementOnRebufferMs >= 0); this.targetLiveOffsetIncrementOnRebufferUs = Util.msToUs(targetLiveOffsetIncrementOnRebufferMs); return this; } /** * Sets the smoothing factor when smoothing the minimum possible live offset that can be * achieved during playback. * *

The live playback speed control keeps track of the minimum possible live offset achievable * during playback to know whether it can reduce the current target live offset. The minimum * possible live offset is defined as {@code currentLiveOffset - bufferedDuration}. As the * minimum possible live offset is constantly changing, it is smoothed over recent samples by * applying exponential smoothing: {@code smoothedMinPossibleOffset = smoothingFactor x * smoothedMinPossibleOffset + (1-smoothingFactor) x currentMinPossibleOffset}. * * @param minPossibleLiveOffsetSmoothingFactor The smoothing factor. Must be ≥ 0 and < 1. * @return This builder, for convenience. */ @CanIgnoreReturnValue public Builder setMinPossibleLiveOffsetSmoothingFactor( float minPossibleLiveOffsetSmoothingFactor) { Assertions.checkArgument( minPossibleLiveOffsetSmoothingFactor >= 0 && minPossibleLiveOffsetSmoothingFactor < 1f); this.minPossibleLiveOffsetSmoothingFactor = minPossibleLiveOffsetSmoothingFactor; return this; } /** Builds an instance. */ public DefaultLivePlaybackSpeedControl build() { return new DefaultLivePlaybackSpeedControl( fallbackMinPlaybackSpeed, fallbackMaxPlaybackSpeed, minUpdateIntervalMs, proportionalControlFactorUs, maxLiveOffsetErrorUsForUnitSpeed, targetLiveOffsetIncrementOnRebufferUs, minPossibleLiveOffsetSmoothingFactor); } } private final float fallbackMinPlaybackSpeed; private final float fallbackMaxPlaybackSpeed; private final long minUpdateIntervalMs; private final float proportionalControlFactor; private final long maxLiveOffsetErrorUsForUnitSpeed; private final long targetLiveOffsetRebufferDeltaUs; private final float minPossibleLiveOffsetSmoothingFactor; private long mediaConfigurationTargetLiveOffsetUs; private long targetLiveOffsetOverrideUs; private long idealTargetLiveOffsetUs; private long minTargetLiveOffsetUs; private long maxTargetLiveOffsetUs; private long currentTargetLiveOffsetUs; private float maxPlaybackSpeed; private float minPlaybackSpeed; private float adjustedPlaybackSpeed; private long lastPlaybackSpeedUpdateMs; private long smoothedMinPossibleLiveOffsetUs; private long smoothedMinPossibleLiveOffsetDeviationUs; private DefaultLivePlaybackSpeedControl( float fallbackMinPlaybackSpeed, float fallbackMaxPlaybackSpeed, long minUpdateIntervalMs, float proportionalControlFactor, long maxLiveOffsetErrorUsForUnitSpeed, long targetLiveOffsetRebufferDeltaUs, float minPossibleLiveOffsetSmoothingFactor) { this.fallbackMinPlaybackSpeed = fallbackMinPlaybackSpeed; this.fallbackMaxPlaybackSpeed = fallbackMaxPlaybackSpeed; this.minUpdateIntervalMs = minUpdateIntervalMs; this.proportionalControlFactor = proportionalControlFactor; this.maxLiveOffsetErrorUsForUnitSpeed = maxLiveOffsetErrorUsForUnitSpeed; this.targetLiveOffsetRebufferDeltaUs = targetLiveOffsetRebufferDeltaUs; this.minPossibleLiveOffsetSmoothingFactor = minPossibleLiveOffsetSmoothingFactor; mediaConfigurationTargetLiveOffsetUs = C.TIME_UNSET; targetLiveOffsetOverrideUs = C.TIME_UNSET; minTargetLiveOffsetUs = C.TIME_UNSET; maxTargetLiveOffsetUs = C.TIME_UNSET; minPlaybackSpeed = fallbackMinPlaybackSpeed; maxPlaybackSpeed = fallbackMaxPlaybackSpeed; adjustedPlaybackSpeed = 1.0f; lastPlaybackSpeedUpdateMs = C.TIME_UNSET; idealTargetLiveOffsetUs = C.TIME_UNSET; currentTargetLiveOffsetUs = C.TIME_UNSET; smoothedMinPossibleLiveOffsetUs = C.TIME_UNSET; smoothedMinPossibleLiveOffsetDeviationUs = C.TIME_UNSET; } @Override public void setLiveConfiguration(LiveConfiguration liveConfiguration) { mediaConfigurationTargetLiveOffsetUs = Util.msToUs(liveConfiguration.targetOffsetMs); minTargetLiveOffsetUs = Util.msToUs(liveConfiguration.minOffsetMs); maxTargetLiveOffsetUs = Util.msToUs(liveConfiguration.maxOffsetMs); minPlaybackSpeed = liveConfiguration.minPlaybackSpeed != C.RATE_UNSET ? liveConfiguration.minPlaybackSpeed : fallbackMinPlaybackSpeed; maxPlaybackSpeed = liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET ? liveConfiguration.maxPlaybackSpeed : fallbackMaxPlaybackSpeed; if (minPlaybackSpeed == 1f && maxPlaybackSpeed == 1f) { // Don't bother calculating adjustments if it's not possible to change the speed. mediaConfigurationTargetLiveOffsetUs = C.TIME_UNSET; } maybeResetTargetLiveOffsetUs(); } @Override public void setTargetLiveOffsetOverrideUs(long liveOffsetUs) { targetLiveOffsetOverrideUs = liveOffsetUs; 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, long bufferedDurationUs) { if (mediaConfigurationTargetLiveOffsetUs == C.TIME_UNSET) { return 1f; } updateSmoothedMinPossibleLiveOffsetUs(liveOffsetUs, bufferedDurationUs); if (lastPlaybackSpeedUpdateMs != C.TIME_UNSET && SystemClock.elapsedRealtime() - lastPlaybackSpeedUpdateMs < minUpdateIntervalMs) { return adjustedPlaybackSpeed; } lastPlaybackSpeedUpdateMs = SystemClock.elapsedRealtime(); adjustTargetLiveOffsetUs(liveOffsetUs); long liveOffsetErrorUs = liveOffsetUs - currentTargetLiveOffsetUs; if (Math.abs(liveOffsetErrorUs) < maxLiveOffsetErrorUsForUnitSpeed) { adjustedPlaybackSpeed = 1f; } else { float calculatedSpeed = 1f + proportionalControlFactor * liveOffsetErrorUs; adjustedPlaybackSpeed = Util.constrainValue(calculatedSpeed, minPlaybackSpeed, maxPlaybackSpeed); } return adjustedPlaybackSpeed; } @Override public long getTargetLiveOffsetUs() { return currentTargetLiveOffsetUs; } private void maybeResetTargetLiveOffsetUs() { long idealOffsetUs = C.TIME_UNSET; if (mediaConfigurationTargetLiveOffsetUs != C.TIME_UNSET) { idealOffsetUs = targetLiveOffsetOverrideUs != C.TIME_UNSET ? targetLiveOffsetOverrideUs : mediaConfigurationTargetLiveOffsetUs; if (minTargetLiveOffsetUs != C.TIME_UNSET && idealOffsetUs < minTargetLiveOffsetUs) { idealOffsetUs = minTargetLiveOffsetUs; } if (maxTargetLiveOffsetUs != C.TIME_UNSET && idealOffsetUs > maxTargetLiveOffsetUs) { idealOffsetUs = maxTargetLiveOffsetUs; } } if (idealTargetLiveOffsetUs == idealOffsetUs) { return; } idealTargetLiveOffsetUs = idealOffsetUs; currentTargetLiveOffsetUs = idealOffsetUs; smoothedMinPossibleLiveOffsetUs = C.TIME_UNSET; smoothedMinPossibleLiveOffsetDeviationUs = C.TIME_UNSET; lastPlaybackSpeedUpdateMs = C.TIME_UNSET; } private void updateSmoothedMinPossibleLiveOffsetUs(long liveOffsetUs, long bufferedDurationUs) { long minPossibleLiveOffsetUs = liveOffsetUs - bufferedDurationUs; if (smoothedMinPossibleLiveOffsetUs == C.TIME_UNSET) { smoothedMinPossibleLiveOffsetUs = minPossibleLiveOffsetUs; smoothedMinPossibleLiveOffsetDeviationUs = 0; } else { // Use the maximum here to ensure we keep track of the upper bound of what is safely possible, // not the average. smoothedMinPossibleLiveOffsetUs = max( minPossibleLiveOffsetUs, smooth( smoothedMinPossibleLiveOffsetUs, minPossibleLiveOffsetUs, minPossibleLiveOffsetSmoothingFactor)); long minPossibleLiveOffsetDeviationUs = abs(minPossibleLiveOffsetUs - smoothedMinPossibleLiveOffsetUs); smoothedMinPossibleLiveOffsetDeviationUs = smooth( smoothedMinPossibleLiveOffsetDeviationUs, minPossibleLiveOffsetDeviationUs, minPossibleLiveOffsetSmoothingFactor); } } private void adjustTargetLiveOffsetUs(long liveOffsetUs) { // Stay in a safe distance (3 standard deviations = >99%) to the minimum possible live offset. long safeOffsetUs = smoothedMinPossibleLiveOffsetUs + 3 * smoothedMinPossibleLiveOffsetDeviationUs; if (currentTargetLiveOffsetUs > safeOffsetUs) { // There is room for decreasing the target offset towards the ideal or safe offset (whichever // is larger). We want to limit the decrease so that the playback speed delta we achieve is // the same as the maximum delta when slowing down towards the target. long minUpdateIntervalUs = Util.msToUs(minUpdateIntervalMs); long decrementToOffsetCurrentSpeedUs = (long) ((adjustedPlaybackSpeed - 1f) * minUpdateIntervalUs); long decrementToIncreaseSpeedUs = (long) ((maxPlaybackSpeed - 1f) * minUpdateIntervalUs); long maxDecrementUs = decrementToOffsetCurrentSpeedUs + decrementToIncreaseSpeedUs; currentTargetLiveOffsetUs = max(safeOffsetUs, idealTargetLiveOffsetUs, currentTargetLiveOffsetUs - maxDecrementUs); } else { // We'd like to reach a stable condition where the current live offset stays just below the // safe offset. But don't increase the target offset to more than what would allow us to slow // down gradually from the current offset. long offsetWhenSlowingDownNowUs = liveOffsetUs - (long) (max(0f, adjustedPlaybackSpeed - 1f) / proportionalControlFactor); currentTargetLiveOffsetUs = Util.constrainValue(offsetWhenSlowingDownNowUs, currentTargetLiveOffsetUs, safeOffsetUs); if (maxTargetLiveOffsetUs != C.TIME_UNSET && currentTargetLiveOffsetUs > maxTargetLiveOffsetUs) { currentTargetLiveOffsetUs = maxTargetLiveOffsetUs; } } } private static long smooth(long smoothedValue, long newValue, float smoothingFactor) { return (long) (smoothingFactor * smoothedValue + (1f - smoothingFactor) * newValue); } }