public final class

VideoFrameReleaseControl

extends java.lang.Object

 java.lang.Object

↳androidx.media3.exoplayer.video.VideoFrameReleaseControl

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.5.0-alpha01

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

Overview

Controls the releasing of video frames.

Summary

Fields
public static final intFRAME_RELEASE_DROP

Signals a frame should be dropped.

public static final intFRAME_RELEASE_IGNORE

Signals that a frame should be ignored.

public static final intFRAME_RELEASE_IMMEDIATELY

Signals a frame should be released immediately.

public static final intFRAME_RELEASE_SCHEDULED

Signals a frame should be scheduled for release.

public static final intFRAME_RELEASE_SKIP

Signals that a frame should be skipped.

public static final intFRAME_RELEASE_TRY_AGAIN_LATER

Signals that a frame should not be released and the renderer should try again later.

Constructors
publicVideoFrameReleaseControl(Context applicationContext, VideoFrameReleaseControl.FrameTimingEvaluator frameTimingEvaluator, long allowedJoiningTimeMs)

Creates an instance.

Methods
public voidallowReleaseFirstFrameBeforeStarted()

Allows the frame control to indicate the first frame can be released before this instance is started.

public intgetFrameReleaseAction(long presentationTimeUs, long positionUs, long elapsedRealtimeUs, long outputStreamStartPositionUs, boolean isLastFrame, VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo)

Returns a VideoFrameReleaseControl.FrameReleaseAction for a video frame which instructs a renderer what to do with the frame.

public booleanisReady(boolean rendererOtherwiseReady)

Whether the release control is ready to start playback.

public voidjoin(boolean renderNextFrameImmediately)

Joins the release control to a new stream.

public voidonDisabled()

Called when the renderer is disabled.

public voidonEnabled(boolean releaseFirstFrameBeforeStarted)

Called when the renderer is enabled.

public booleanonFrameReleasedIsFirstFrame()

Called when a frame has been released.

public voidonProcessedStreamChange()

Called when the renderer processed a stream change.

public voidonStarted()

Called when the renderer is started.

public voidonStopped()

Called when the renderer is stopped.

public voidreset()

Resets the release control.

public voidsetChangeFrameRateStrategy(int changeFrameRateStrategy)

Changes the used when calling .

public voidsetClock(Clock clock)

Sets the clock that will be used.

public voidsetFrameRate(float frameRate)

Sets the frame rate.

public voidsetOutputSurface(Surface outputSurface)

Called when the display surface changed.

public voidsetPlaybackSpeed(float speed)

Sets the playback speed.

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

Fields

public static final int FRAME_RELEASE_IMMEDIATELY

Signals a frame should be released immediately.

public static final int FRAME_RELEASE_SCHEDULED

Signals a frame should be scheduled for release. The release timestamp will be returned by VideoFrameReleaseControl.FrameReleaseInfo.getReleaseTimeNs().

public static final int FRAME_RELEASE_DROP

Signals a frame should be dropped.

public static final int FRAME_RELEASE_SKIP

Signals that a frame should be skipped.

public static final int FRAME_RELEASE_IGNORE

Signals that a frame should be ignored.

public static final int FRAME_RELEASE_TRY_AGAIN_LATER

Signals that a frame should not be released and the renderer should try again later.

Constructors

public VideoFrameReleaseControl(Context applicationContext, VideoFrameReleaseControl.FrameTimingEvaluator frameTimingEvaluator, long allowedJoiningTimeMs)

Creates an instance.

Parameters:

applicationContext: The application context.
frameTimingEvaluator: The VideoFrameReleaseControl.FrameTimingEvaluator that will assist in frame release actions.
allowedJoiningTimeMs: The maximum duration in milliseconds for which the renderer can attempt to seamlessly join an ongoing playback.

Methods

public void onEnabled(boolean releaseFirstFrameBeforeStarted)

Called when the renderer is enabled.

public void onDisabled()

Called when the renderer is disabled.

public void onStarted()

Called when the renderer is started.

public void onStopped()

Called when the renderer is stopped.

public void onProcessedStreamChange()

Called when the renderer processed a stream change.

public void setOutputSurface(Surface outputSurface)

Called when the display surface changed.

public void setFrameRate(float frameRate)

Sets the frame rate.

public boolean onFrameReleasedIsFirstFrame()

Called when a frame has been released.

Returns:

Whether this is the first released frame.

public void setClock(Clock clock)

Sets the clock that will be used.

public void allowReleaseFirstFrameBeforeStarted()

Allows the frame control to indicate the first frame can be released before this instance is started.

public boolean isReady(boolean rendererOtherwiseReady)

Whether the release control is ready to start playback.

The renderer should be ready if and only if the release control is ready.

Parameters:

rendererOtherwiseReady: Whether the renderer is ready except for the release control.

Returns:

Whether the release control is ready.

public void join(boolean renderNextFrameImmediately)

Joins the release control to a new stream.

The release control will pretend to be ready for a short time even if the first frame hasn't been rendered yet to avoid interrupting an ongoing playback.

Parameters:

renderNextFrameImmediately: Whether the next frame should be released as soon as possible or only at its preferred scheduled release time.

public int getFrameReleaseAction(long presentationTimeUs, long positionUs, long elapsedRealtimeUs, long outputStreamStartPositionUs, boolean isLastFrame, VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo)

Returns a VideoFrameReleaseControl.FrameReleaseAction for a video frame which instructs a renderer what to do with the frame.

Parameters:

presentationTimeUs: The presentation time of the video frame, in microseconds.
positionUs: The current playback position, in microseconds.
elapsedRealtimeUs: in microseconds, taken approximately at the time the playback position was positionUs.
outputStreamStartPositionUs: The stream's start position, in microseconds.
isLastFrame: Whether the frame is known to contain the last frame of the current stream.
frameReleaseInfo: A VideoFrameReleaseControl.FrameReleaseInfo that will be filled with detailed data only if the method returns VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY or VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED.

Returns:

A VideoFrameReleaseControl.FrameReleaseAction that should instruct the renderer whether to release the frame or not.

public void reset()

Resets the release control.

public void setChangeFrameRateStrategy(int changeFrameRateStrategy)

Changes the used when calling .

The default value is C.VIDEO_CHANGE_FRAME_RATE_STRATEGY_ONLY_IF_SEAMLESS.

public void setPlaybackSpeed(float speed)

Sets the playback speed. Called when the renderer playback speed changes.

Source

/*
 * Copyright 2023 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.video;

import static androidx.media3.common.util.Util.msToUs;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.content.Context;
import android.os.SystemClock;
import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.Renderer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Controls the releasing of video frames. */
@UnstableApi
public final class VideoFrameReleaseControl {

  /**
   * The frame release action returned by {@link #getFrameReleaseAction(long, long, long, long,
   * boolean, FrameReleaseInfo)}.
   *
   * <p>One of {@link #FRAME_RELEASE_IMMEDIATELY}, {@link #FRAME_RELEASE_SCHEDULED}, {@link
   * #FRAME_RELEASE_DROP}, {@link #FRAME_RELEASE_IGNORE}, {@link ##FRAME_RELEASE_SKIP} or {@link
   * #FRAME_RELEASE_TRY_AGAIN_LATER}.
   */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @UnstableApi
  @IntDef({
    FRAME_RELEASE_IMMEDIATELY,
    FRAME_RELEASE_SCHEDULED,
    FRAME_RELEASE_DROP,
    FRAME_RELEASE_SKIP,
    FRAME_RELEASE_IGNORE,
    FRAME_RELEASE_TRY_AGAIN_LATER
  })
  public @interface FrameReleaseAction {}

  /** Signals a frame should be released immediately. */
  public static final int FRAME_RELEASE_IMMEDIATELY = 0;

  /**
   * Signals a frame should be scheduled for release. The release timestamp will be returned by
   * {@link FrameReleaseInfo#getReleaseTimeNs()}.
   */
  public static final int FRAME_RELEASE_SCHEDULED = 1;

  /** Signals a frame should be dropped. */
  public static final int FRAME_RELEASE_DROP = 2;

  /** Signals that a frame should be skipped. */
  public static final int FRAME_RELEASE_SKIP = 3;

  /** Signals that a frame should be ignored. */
  public static final int FRAME_RELEASE_IGNORE = 4;

  /** Signals that a frame should not be released and the renderer should try again later. */
  public static final int FRAME_RELEASE_TRY_AGAIN_LATER = 5;

  /** Per {@link FrameReleaseAction} metadata. */
  public static class FrameReleaseInfo {
    private long earlyUs;
    private long releaseTimeNs;

    /** Resets this instances state. */
    public FrameReleaseInfo() {
      earlyUs = C.TIME_UNSET;
      releaseTimeNs = C.TIME_UNSET;
    }

    /**
     * Returns this frame's early time compared to the playback position, before any release time
     * adjustment to the screen vsync slots.
     */
    public long getEarlyUs() {
      return earlyUs;
    }

    /**
     * Returns the release time for the frame, in nanoseconds, or {@link C#TIME_UNSET} if the frame
     * should not be released yet.
     */
    public long getReleaseTimeNs() {
      return releaseTimeNs;
    }

    private void reset() {
      earlyUs = C.TIME_UNSET;
      releaseTimeNs = C.TIME_UNSET;
    }
  }

  /** Decides whether a frame should be forced to be released, or dropped. */
  public interface FrameTimingEvaluator {
    /**
     * Whether a frame should be forced for release.
     *
     * @param earlyUs The time until the buffer should be presented in microseconds. A negative
     *     value indicates that the buffer is late.
     * @param elapsedSinceLastReleaseUs The elapsed time since the last frame was released, in
     *     microseconds.
     * @return Whether the video frame should be force released.
     */
    boolean shouldForceReleaseFrame(long earlyUs, long elapsedSinceLastReleaseUs);

    /**
     * Returns whether the frame should be dropped.
     *
     * @param earlyUs The time until the buffer should be presented in microseconds. A negative
     *     value indicates that the buffer is late.
     * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
     *     measured at the start of the current iteration of the rendering loop.
     * @param isLastFrame Whether the buffer is the last buffer in the current stream.
     */
    boolean shouldDropFrame(long earlyUs, long elapsedRealtimeUs, boolean isLastFrame);

    /**
     * Returns whether this frame should be ignored.
     *
     * @param earlyUs The time until the buffer should be presented in microseconds. A negative
     *     value indicates that the buffer is late.
     * @param positionUs The playback position, in microseconds.
     * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
     *     measured at the start of the current iteration of the rendering loop.
     * @param isLastFrame Whether the buffer is the last buffer in the current stream.
     * @param treatDroppedBuffersAsSkipped Whether dropped buffers should be treated as
     *     intentionally skipped.
     * @return Whether this frame should be ignored.
     * @throws ExoPlaybackException If an error occurs.
     */
    boolean shouldIgnoreFrame(
        long earlyUs,
        long positionUs,
        long elapsedRealtimeUs,
        boolean isLastFrame,
        boolean treatDroppedBuffersAsSkipped)
        throws ExoPlaybackException;
  }

  /** The maximum earliest time, in microseconds, to release a frame on the surface. */
  private static final long MAX_EARLY_US_THRESHOLD = 50_000;

  private final FrameTimingEvaluator frameTimingEvaluator;
  private final VideoFrameReleaseHelper frameReleaseHelper;
  private final long allowedJoiningTimeMs;

  private boolean started;
  private @C.FirstFrameState int firstFrameState;
  private long initialPositionUs;
  private long lastReleaseRealtimeUs;
  private long lastPresentationTimeUs;
  private long joiningDeadlineMs;
  private boolean joiningRenderNextFrameImmediately;
  private float playbackSpeed;
  private Clock clock;

  /**
   * Creates an instance.
   *
   * @param applicationContext The application context.
   * @param frameTimingEvaluator The {@link FrameTimingEvaluator} that will assist in {@linkplain
   *     #getFrameReleaseAction(long, long, long, long, boolean, FrameReleaseInfo) frame release
   *     actions}.
   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which the renderer can
   *     attempt to seamlessly join an ongoing playback.
   */
  public VideoFrameReleaseControl(
      Context applicationContext,
      FrameTimingEvaluator frameTimingEvaluator,
      long allowedJoiningTimeMs) {
    this.frameTimingEvaluator = frameTimingEvaluator;
    this.allowedJoiningTimeMs = allowedJoiningTimeMs;
    frameReleaseHelper = new VideoFrameReleaseHelper(applicationContext);
    firstFrameState = C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
    initialPositionUs = C.TIME_UNSET;
    lastPresentationTimeUs = C.TIME_UNSET;
    joiningDeadlineMs = C.TIME_UNSET;
    playbackSpeed = 1f;
    clock = Clock.DEFAULT;
  }

  /** Called when the renderer is enabled. */
  public void onEnabled(boolean releaseFirstFrameBeforeStarted) {
    firstFrameState =
        releaseFirstFrameBeforeStarted
            ? C.FIRST_FRAME_NOT_RENDERED
            : C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
  }

  /** Called when the renderer is disabled. */
  public void onDisabled() {
    lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED);
  }

  /** Called when the renderer is started. */
  public void onStarted() {
    started = true;
    lastReleaseRealtimeUs = msToUs(clock.elapsedRealtime());
    frameReleaseHelper.onStarted();
  }

  /** Called when the renderer is stopped. */
  public void onStopped() {
    started = false;
    joiningDeadlineMs = C.TIME_UNSET;
    frameReleaseHelper.onStopped();
  }

  /** Called when the renderer processed a stream change. */
  public void onProcessedStreamChange() {
    lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED_AFTER_STREAM_CHANGE);
  }

  /** Called when the display surface changed. */
  public void setOutputSurface(@Nullable Surface outputSurface) {
    frameReleaseHelper.onSurfaceChanged(outputSurface);
    lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
  }

  /** Sets the frame rate. */
  public void setFrameRate(float frameRate) {
    frameReleaseHelper.onFormatChanged(frameRate);
  }

  /**
   * Called when a frame has been released.
   *
   * @return Whether this is the first released frame.
   */
  public boolean onFrameReleasedIsFirstFrame() {
    boolean firstFrame = firstFrameState != C.FIRST_FRAME_RENDERED;
    firstFrameState = C.FIRST_FRAME_RENDERED;
    lastReleaseRealtimeUs = msToUs(clock.elapsedRealtime());
    return firstFrame;
  }

  /** Sets the clock that will be used. */
  public void setClock(Clock clock) {
    this.clock = clock;
  }

  /**
   * Allows the frame control to indicate the first frame can be released before this instance is
   * started.
   */
  public void allowReleaseFirstFrameBeforeStarted() {
    if (firstFrameState == C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED) {
      firstFrameState = C.FIRST_FRAME_NOT_RENDERED;
    }
  }

  /**
   * Whether the release control is ready to start playback.
   *
   * <p>The renderer should be {@linkplain Renderer#isReady() ready} if and only if the release
   * control is ready.
   *
   * @param rendererOtherwiseReady Whether the renderer is ready except for the release control.
   * @return Whether the release control is ready.
   */
  public boolean isReady(boolean rendererOtherwiseReady) {
    if (rendererOtherwiseReady && firstFrameState == C.FIRST_FRAME_RENDERED) {
      // Ready. If we were joining then we've now joined, so clear the joining deadline.
      joiningDeadlineMs = C.TIME_UNSET;
      return true;
    } else if (joiningDeadlineMs == C.TIME_UNSET) {
      // Not joining.
      return false;
    } else if (clock.elapsedRealtime() < joiningDeadlineMs) {
      // Joining and still withing the deadline.
      return true;
    } else {
      // The joining deadline has been exceeded. Give up and clear the deadline.
      joiningDeadlineMs = C.TIME_UNSET;
      return false;
    }
  }

  /**
   * Joins the release control to a new stream.
   *
   * <p>The release control will pretend to be {@linkplain #isReady ready} for a short time even if
   * the first frame hasn't been rendered yet to avoid interrupting an ongoing playback.
   *
   * @param renderNextFrameImmediately Whether the next frame should be released as soon as possible
   *     or only at its preferred scheduled release time.
   */
  public void join(boolean renderNextFrameImmediately) {
    joiningRenderNextFrameImmediately = renderNextFrameImmediately;
    joiningDeadlineMs =
        allowedJoiningTimeMs > 0 ? (clock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
  }

  /**
   * Returns a {@link FrameReleaseAction} for a video frame which instructs a renderer what to do
   * with the frame.
   *
   * @param presentationTimeUs The presentation time of the video frame, in microseconds.
   * @param positionUs The current playback position, in microseconds.
   * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
   *     taken approximately at the time the playback position was {@code positionUs}.
   * @param outputStreamStartPositionUs The stream's start position, in microseconds.
   * @param isLastFrame Whether the frame is known to contain the last frame of the current stream.
   * @param frameReleaseInfo A {@link FrameReleaseInfo} that will be filled with detailed data only
   *     if the method returns {@link #FRAME_RELEASE_IMMEDIATELY} or {@link
   *     #FRAME_RELEASE_SCHEDULED}.
   * @return A {@link FrameReleaseAction} that should instruct the renderer whether to release the
   *     frame or not.
   */
  public @FrameReleaseAction int getFrameReleaseAction(
      long presentationTimeUs,
      long positionUs,
      long elapsedRealtimeUs,
      long outputStreamStartPositionUs,
      boolean isLastFrame,
      FrameReleaseInfo frameReleaseInfo)
      throws ExoPlaybackException {
    frameReleaseInfo.reset();

    if (initialPositionUs == C.TIME_UNSET) {
      initialPositionUs = positionUs;
    }
    if (lastPresentationTimeUs != presentationTimeUs) {
      frameReleaseHelper.onNextFrame(presentationTimeUs);
      lastPresentationTimeUs = presentationTimeUs;
    }

    frameReleaseInfo.earlyUs =
        calculateEarlyTimeUs(positionUs, elapsedRealtimeUs, presentationTimeUs);

    if (shouldForceRelease(positionUs, frameReleaseInfo.earlyUs, outputStreamStartPositionUs)) {
      return FRAME_RELEASE_IMMEDIATELY;
    }
    if (!started || positionUs == initialPositionUs) {
      return FRAME_RELEASE_TRY_AGAIN_LATER;
    }

    // Calculate release time and adjust earlyUs to screen vsync.
    long systemTimeNs = clock.nanoTime();
    frameReleaseInfo.releaseTimeNs =
        frameReleaseHelper.adjustReleaseTime(systemTimeNs + (frameReleaseInfo.earlyUs * 1_000));
    frameReleaseInfo.earlyUs = (frameReleaseInfo.releaseTimeNs - systemTimeNs) / 1_000;
    // While joining, late frames are skipped while we catch up with the playback position.
    boolean treatDropAsSkip =
        joiningDeadlineMs != C.TIME_UNSET && !joiningRenderNextFrameImmediately;
    if (frameTimingEvaluator.shouldIgnoreFrame(
        frameReleaseInfo.earlyUs, positionUs, elapsedRealtimeUs, isLastFrame, treatDropAsSkip)) {
      return FRAME_RELEASE_IGNORE;
    } else if (frameTimingEvaluator.shouldDropFrame(
        frameReleaseInfo.earlyUs, elapsedRealtimeUs, isLastFrame)) {
      // While joining, dropped buffers are considered skipped.
      return treatDropAsSkip ? FRAME_RELEASE_SKIP : FRAME_RELEASE_DROP;
    } else if (frameReleaseInfo.earlyUs > MAX_EARLY_US_THRESHOLD) {
      return FRAME_RELEASE_TRY_AGAIN_LATER;
    }
    return FRAME_RELEASE_SCHEDULED;
  }

  /** Resets the release control. */
  public void reset() {
    frameReleaseHelper.onPositionReset();
    lastPresentationTimeUs = C.TIME_UNSET;
    initialPositionUs = C.TIME_UNSET;
    lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
    joiningDeadlineMs = C.TIME_UNSET;
  }

  /**
   * Changes the {@link C.VideoChangeFrameRateStrategy} used when calling {@link
   * Surface#setFrameRate}.
   *
   * <p>The default value is {@link C#VIDEO_CHANGE_FRAME_RATE_STRATEGY_ONLY_IF_SEAMLESS}.
   */
  public void setChangeFrameRateStrategy(
      @C.VideoChangeFrameRateStrategy int changeFrameRateStrategy) {
    frameReleaseHelper.setChangeFrameRateStrategy(changeFrameRateStrategy);
  }

  /** Sets the playback speed. Called when the renderer playback speed changes. */
  public void setPlaybackSpeed(float speed) {
    if (speed == playbackSpeed) {
      return;
    }
    this.playbackSpeed = speed;
    frameReleaseHelper.onPlaybackSpeed(speed);
  }

  private void lowerFirstFrameState(@C.FirstFrameState int firstFrameState) {
    this.firstFrameState = min(this.firstFrameState, firstFrameState);
  }

  /**
   * Calculates the time interval between the current player position and the frame presentation
   * time.
   *
   * @param positionUs The current media time in microseconds, measured at the start of the current
   *     iteration of the rendering loop.
   * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the
   *     start of the current iteration of the rendering loop.
   * @param framePresentationTimeUs The presentation time of the frame in microseconds.
   * @return The calculated early time, in microseconds.
   */
  private long calculateEarlyTimeUs(
      long positionUs, long elapsedRealtimeUs, long framePresentationTimeUs) {
    // Calculate how early we are. In other words, the realtime duration that needs to elapse whilst
    // the renderer is started before the frame should be rendered. A negative value means that
    // we're already late.
    // Note: Use of double rather than float is intentional for accuracy in the calculations below.
    long earlyUs = (long) ((framePresentationTimeUs - positionUs) / (double) playbackSpeed);
    if (started) {
      // Account for the elapsed time since the start of this iteration of the rendering loop.
      earlyUs -= Util.msToUs(clock.elapsedRealtime()) - elapsedRealtimeUs;
    }

    return earlyUs;
  }

  /** Returns whether a frame should be force released. */
  private boolean shouldForceRelease(
      long positionUs, long earlyUs, long outputStreamStartPositionUs) {
    if (joiningDeadlineMs != C.TIME_UNSET && !joiningRenderNextFrameImmediately) {
      // No force releasing of the initial or late frames during joining unless requested.
      return false;
    }
    switch (firstFrameState) {
      case C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED:
        return started;
      case C.FIRST_FRAME_NOT_RENDERED:
        return true;
      case C.FIRST_FRAME_NOT_RENDERED_AFTER_STREAM_CHANGE:
        return positionUs >= outputStreamStartPositionUs;
      case C.FIRST_FRAME_RENDERED:
        long elapsedTimeSinceLastReleaseUs =
            msToUs(clock.elapsedRealtime()) - lastReleaseRealtimeUs;
        return started
            && frameTimingEvaluator.shouldForceReleaseFrame(earlyUs, elapsedTimeSinceLastReleaseUs);
      default:
        throw new IllegalStateException();
    }
  }
}