DefaultMediaClock.java

/*
 * Copyright (C) 2017 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 androidx.annotation.Nullable;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;

/**
 * Default {@link MediaClock} which uses a renderer media clock and falls back to a {@link
 * StandaloneMediaClock} if necessary.
 */
/* package */ final class DefaultMediaClock implements MediaClock {

  /** Listener interface to be notified of changes to the active playback parameters. */
  public interface PlaybackParametersListener {

    /**
     * Called when the active playback parameters changed. Will not be called for {@link
     * #setPlaybackParameters(PlaybackParameters)}.
     *
     * @param newPlaybackParameters The newly active playback parameters.
     */
    void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);
  }

  private final StandaloneMediaClock standaloneClock;
  private final PlaybackParametersListener listener;

  @Nullable private Renderer rendererClockSource;
  @Nullable private MediaClock rendererClock;
  private boolean isUsingStandaloneClock;
  private boolean standaloneClockIsStarted;

  /**
   * Creates a new instance with a listener for playback parameters changes and a {@link Clock} to
   * use for the standalone clock implementation.
   *
   * @param listener A {@link PlaybackParametersListener} to listen for playback parameters changes.
   * @param clock A {@link Clock}.
   */
  public DefaultMediaClock(PlaybackParametersListener listener, Clock clock) {
    this.listener = listener;
    this.standaloneClock = new StandaloneMediaClock(clock);
    isUsingStandaloneClock = true;
  }

  /** Starts the standalone fallback clock. */
  public void start() {
    standaloneClockIsStarted = true;
    standaloneClock.start();
  }

  /** Stops the standalone fallback clock. */
  public void stop() {
    standaloneClockIsStarted = false;
    standaloneClock.stop();
  }

  /**
   * Resets the position of the standalone fallback clock.
   *
   * @param positionUs The position to set in microseconds.
   */
  public void resetPosition(long positionUs) {
    standaloneClock.resetPosition(positionUs);
  }

  /**
   * Notifies the media clock that a renderer has been enabled. Starts using the media clock of the
   * provided renderer if available.
   *
   * @param renderer The renderer which has been enabled.
   * @throws ExoPlaybackException If the renderer provides a media clock and another renderer media
   *     clock is already provided.
   */
  public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException {
    @Nullable MediaClock rendererMediaClock = renderer.getMediaClock();
    if (rendererMediaClock != null && rendererMediaClock != rendererClock) {
      if (rendererClock != null) {
        throw ExoPlaybackException.createForUnexpected(
            new IllegalStateException("Multiple renderer media clocks enabled."));
      }
      this.rendererClock = rendererMediaClock;
      this.rendererClockSource = renderer;
      rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());
    }
  }

  /**
   * Notifies the media clock that a renderer has been disabled. Stops using the media clock of this
   * renderer if used.
   *
   * @param renderer The renderer which has been disabled.
   */
  public void onRendererDisabled(Renderer renderer) {
    if (renderer == rendererClockSource) {
      this.rendererClock = null;
      this.rendererClockSource = null;
      isUsingStandaloneClock = true;
    }
  }

  /**
   * Syncs internal clock if needed and returns current clock position in microseconds.
   *
   * @param isReadingAhead Whether the renderers are reading ahead.
   */
  public long syncAndGetPositionUs(boolean isReadingAhead) {
    syncClocks(isReadingAhead);
    return getPositionUs();
  }

  // MediaClock implementation.

  @Override
  public long getPositionUs() {
    return isUsingStandaloneClock
        ? standaloneClock.getPositionUs()
        : Assertions.checkNotNull(rendererClock).getPositionUs();
  }

  @Override
  public void setPlaybackParameters(PlaybackParameters playbackParameters) {
    if (rendererClock != null) {
      rendererClock.setPlaybackParameters(playbackParameters);
      playbackParameters = rendererClock.getPlaybackParameters();
    }
    standaloneClock.setPlaybackParameters(playbackParameters);
  }

  @Override
  public PlaybackParameters getPlaybackParameters() {
    return rendererClock != null
        ? rendererClock.getPlaybackParameters()
        : standaloneClock.getPlaybackParameters();
  }

  private void syncClocks(boolean isReadingAhead) {
    if (shouldUseStandaloneClock(isReadingAhead)) {
      isUsingStandaloneClock = true;
      if (standaloneClockIsStarted) {
        standaloneClock.start();
      }
      return;
    }
    // We are either already using the renderer clock or switching from the standalone to the
    // renderer clock, so it must be non-null.
    MediaClock rendererClock = Assertions.checkNotNull(this.rendererClock);
    long rendererClockPositionUs = rendererClock.getPositionUs();
    if (isUsingStandaloneClock) {
      // Ensure enabling the renderer clock doesn't jump backwards in time.
      if (rendererClockPositionUs < standaloneClock.getPositionUs()) {
        standaloneClock.stop();
        return;
      }
      isUsingStandaloneClock = false;
      if (standaloneClockIsStarted) {
        standaloneClock.start();
      }
    }
    // Continuously sync stand-alone clock to renderer clock so that it can take over if needed.
    standaloneClock.resetPosition(rendererClockPositionUs);
    PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();
    if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) {
      standaloneClock.setPlaybackParameters(playbackParameters);
      listener.onPlaybackParametersChanged(playbackParameters);
    }
  }

  private boolean shouldUseStandaloneClock(boolean isReadingAhead) {
    // Use the standalone clock if the clock providing renderer is not set or has ended. Also use
    // the standalone clock if the renderer is not ready and we have finished reading the stream or
    // are reading ahead to avoid getting stuck if tracks in the current period have uneven
    // durations. See: https://github.com/google/ExoPlayer/issues/1874.
    return rendererClockSource == null
        || rendererClockSource.isEnded()
        || (!rendererClockSource.isReady()
            && (isReadingAhead || rendererClockSource.hasReadStreamToEnd()));
  }
}