public final class

DefaultLivePlaybackSpeedControl

extends java.lang.Object

implements LivePlaybackSpeedControl

 java.lang.Object

↳androidx.media3.exoplayer.DefaultLivePlaybackSpeedControl

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

A LivePlaybackSpeedControl that adjusts the playback speed using a proportional controller.

The control mechanism calculates the adjusted speed as 1.0 + proportionalControlFactor x (currentLiveOffsetSec - targetLiveOffsetSec). Unit speed (1.0f) is used, if the currentLiveOffsetSec is closer to targetLiveOffsetSec than the value set with DefaultLivePlaybackSpeedControl.Builder.setMaxLiveOffsetErrorMsForUnitSpeed(long).

The resulting speed is clamped to a minimum and maximum speed defined by the media, the fallback values set with DefaultLivePlaybackSpeedControl.Builder.setFallbackMinPlaybackSpeed(float) and DefaultLivePlaybackSpeedControl.Builder.setFallbackMaxPlaybackSpeed(float) or the minimum and maximum fallback default values.

When the player rebuffers, the target live offset is increased to adjust to the reduced network capabilities. The live playback speed control also 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.

Summary

Fields
public static final floatDEFAULT_FALLBACK_MAX_PLAYBACK_SPEED

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 floatDEFAULT_FALLBACK_MIN_PLAYBACK_SPEED

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 longDEFAULT_MAX_LIVE_OFFSET_ERROR_MS_FOR_UNIT_SPEED

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 floatDEFAULT_MIN_POSSIBLE_LIVE_OFFSET_SMOOTHING_FACTOR

The default smoothing factor when smoothing the minimum possible live offset that can be achieved during playback.

public static final longDEFAULT_MIN_UPDATE_INTERVAL_MS

The default minimum interval between playback speed changes, in milliseconds.

public static final floatDEFAULT_PROPORTIONAL_CONTROL_FACTOR

The default proportional control factor used to adjust the playback speed.

public static final longDEFAULT_TARGET_LIVE_OFFSET_INCREMENT_ON_REBUFFER_MS

The default increment applied to the target live offset each time the player is rebuffering, in milliseconds

Methods
public floatgetAdjustedPlaybackSpeed(long liveOffsetUs, long bufferedDurationUs)

public longgetTargetLiveOffsetUs()

public voidnotifyRebuffer()

public voidsetLiveConfiguration(MediaItem.LiveConfiguration liveConfiguration)

public voidsetTargetLiveOffsetOverrideUs(long liveOffsetUs)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final float DEFAULT_FALLBACK_MIN_PLAYBACK_SPEED

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_MAX_PLAYBACK_SPEED

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 long DEFAULT_MIN_UPDATE_INTERVAL_MS

The default minimum interval between playback speed changes, in milliseconds.

public static final float DEFAULT_PROPORTIONAL_CONTROL_FACTOR

The default proportional control factor used to adjust the playback speed.

public static final long DEFAULT_TARGET_LIVE_OFFSET_INCREMENT_ON_REBUFFER_MS

The default increment applied to the target live offset each time the player is rebuffering, in milliseconds

public static final float DEFAULT_MIN_POSSIBLE_LIVE_OFFSET_SMOOTHING_FACTOR

The default smoothing factor when smoothing the minimum possible live offset that can be achieved during playback.

public static final long DEFAULT_MAX_LIVE_OFFSET_ERROR_MS_FOR_UNIT_SPEED

The default maximum difference between the current live offset and the target live offset, in milliseconds, for which unit speed (1.0f) is used.

Methods

public void setLiveConfiguration(MediaItem.LiveConfiguration liveConfiguration)

public void setTargetLiveOffsetOverrideUs(long liveOffsetUs)

public void notifyRebuffer()

public float getAdjustedPlaybackSpeed(long liveOffsetUs, long bufferedDurationUs)

public long getTargetLiveOffsetUs()

Source

/*
 * 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 androidx.media3.exoplayer;

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 androidx.media3.common.C;
import androidx.media3.common.MediaItem.LiveConfiguration;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;

/**
 * A {@link LivePlaybackSpeedControl} that adjusts the playback speed using a proportional
 * controller.
 *
 * <p>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)}.
 *
 * <p>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.
 *
 * <p>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.
 */
@UnstableApi
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.
     *
     * <p>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.
     */
    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.
     *
     * <p>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.
     */
    public Builder setFallbackMaxPlaybackSpeed(float fallbackMaxPlaybackSpeed) {
      Assertions.checkArgument(fallbackMaxPlaybackSpeed >= 1f);
      this.fallbackMaxPlaybackSpeed = fallbackMaxPlaybackSpeed;
      return this;
    }

    /**
     * Sets the minimum interval between playback speed changes, in milliseconds.
     *
     * <p>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.
     *
     * <p>The factor by which playback will be sped up is calculated as {@code 1.0 +
     * proportionalControlFactor x (currentLiveOffsetSec - targetLiveOffsetSec)}.
     *
     * <p>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;
    }

    /**
     * Sets the maximum difference between the current live offset and the target live offset, in
     * milliseconds, for which unit speed (1.0f) is used.
     *
     * <p>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.
     */
    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.
     */
    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.
     *
     * <p>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 &ge; 0 and &lt; 1.
     * @return This builder, for convenience.
     */
    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);
  }
}