Simplify first frame logic

We currently use 3 different booleans to track the state of the first
frame rendering, which implies that there are 8 distinct possible
overall states. However, this is actually a staged process and there
are only 3 different overall states in the current code. This means
it's clearer and easier to reason about if the variables are combined
to a single state value. Overall, this should be a complete no-op.

State mapping:
 - rFFAReset=false, rFFAEnable=false, mayRenderFFAEINS=false
   => FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED
 - rFFAReset=false and/or rFFAEnable=false, mayRenderFFAEINS=any
   => FIRST_FRAME_NOT_RENDERED
 - rFFAReset=true, rFFAEnable=true, mayRenderFFAEINS=any
   => FIRST_FRAME_RENDERED

PiperOrigin-RevId: 552857802
This commit is contained in:
tonihei 2023-08-01 18:05:50 +00:00 committed by Tianyi Feng
parent 11648e6c8e
commit 79e05ad049
3 changed files with 109 additions and 54 deletions

View file

@ -1506,6 +1506,36 @@ public final class C {
*/ */
@UnstableApi public static final int FORMAT_UNSUPPORTED_TYPE = 0b000; @UnstableApi public static final int FORMAT_UNSUPPORTED_TYPE = 0b000;
/**
* State of the first frame in a video renderer.
*
* <p>One of {@link #FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED}, {@link
* #FIRST_FRAME_NOT_RENDERED} or {@link #FIRST_FRAME_RENDERED}. The stages are ordered and
* comparable, i.e., a value implies that all stages with higher values are not reached yet.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@UnstableApi
@IntDef({
FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED,
FIRST_FRAME_NOT_RENDERED,
FIRST_FRAME_RENDERED
})
public @interface FirstFrameState {}
/**
* The first frame was not rendered yet, and is only allowed to be rendered if the renderer is
* started.
*/
@UnstableApi public static final int FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED = 0;
/** The first frame was not rendered after the last reset, output surface or stream change. */
@UnstableApi public static final int FIRST_FRAME_NOT_RENDERED = 1;
/** The first frame was rendered. */
@UnstableApi public static final int FIRST_FRAME_RENDERED = 2;
/** /**
* @deprecated Use {@link Util#usToMs(long)}. * @deprecated Use {@link Util#usToMs(long)}.
*/ */

View file

@ -21,6 +21,7 @@ import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_RE
import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT; import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Handler; import android.os.Handler;
@ -136,9 +137,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
private @ReinitializationState int decoderReinitializationState; private @ReinitializationState int decoderReinitializationState;
private boolean decoderReceivedBuffers; private boolean decoderReceivedBuffers;
private boolean renderedFirstFrameAfterReset; private @C.FirstFrameState int firstFrameState;
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
private long initialPositionUs; private long initialPositionUs;
private long joiningDeadlineMs; private long joiningDeadlineMs;
private boolean waitingForFirstSampleInFormat; private boolean waitingForFirstSampleInFormat;
@ -181,6 +180,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
outputMode = C.VIDEO_OUTPUT_MODE_NONE; outputMode = C.VIDEO_OUTPUT_MODE_NONE;
firstFrameState = C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
} }
// BaseRenderer implementation. // BaseRenderer implementation.
@ -238,7 +238,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
public boolean isReady() { public boolean isReady() {
if (inputFormat != null if (inputFormat != null
&& (isSourceReady() || outputBuffer != null) && (isSourceReady() || outputBuffer != null)
&& (renderedFirstFrameAfterReset || !hasOutput())) { && (firstFrameState == C.FIRST_FRAME_RENDERED || !hasOutput())) {
// Ready. If we were joining then we've now joined, so clear the joining deadline. // Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET;
return true; return true;
@ -276,20 +276,24 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
throws ExoPlaybackException { throws ExoPlaybackException {
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream; firstFrameState =
renderedFirstFrameAfterEnable = false; mayRenderStartOfStream
? C.FIRST_FRAME_NOT_RENDERED
: C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
} }
@Override @Override
public void enableMayRenderStartOfStream() { public void enableMayRenderStartOfStream() {
mayRenderFirstFrameAfterEnableIfNotStarted = true; if (firstFrameState == C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED) {
firstFrameState = C.FIRST_FRAME_NOT_RENDERED;
}
} }
@Override @Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
initialPositionUs = C.TIME_UNSET; initialPositionUs = C.TIME_UNSET;
consecutiveDroppedFrameCount = 0; consecutiveDroppedFrameCount = 0;
if (decoder != null) { if (decoder != null) {
@ -320,7 +324,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
protected void onDisabled() { protected void onDisabled() {
inputFormat = null; inputFormat = null;
clearReportedVideoSize(); clearReportedVideoSize();
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED);
try { try {
setSourceDrmSession(null); setSourceDrmSession(null);
releaseDecoder(); releaseDecoder();
@ -854,20 +858,12 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
outputFormat = format; outputFormat = format;
} }
long elapsedRealtimeNowUs = msToUs(SystemClock.elapsedRealtime()); if (shouldForceRender(earlyUs)) {
long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs;
boolean isStarted = getState() == STATE_STARTED;
boolean shouldRenderFirstFrame =
!renderedFirstFrameAfterEnable
? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted)
: !renderedFirstFrameAfterReset;
// TODO: We shouldn't force render while we are joining an ongoing playback.
if (shouldRenderFirstFrame
|| (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))) {
renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat);
return true; return true;
} }
boolean isStarted = getState() == STATE_STARTED;
if (!isStarted || positionUs == initialPositionUs) { if (!isStarted || positionUs == initialPositionUs) {
return false; return false;
} }
@ -889,6 +885,23 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
return false; return false;
} }
/** Returns whether a buffer or a processed frame should be force rendered. */
private boolean shouldForceRender(long earlyUs) {
// TODO: We shouldn't force render while we are joining an ongoing playback.
boolean isStarted = getState() == STATE_STARTED;
switch (firstFrameState) {
case C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED:
return isStarted;
case C.FIRST_FRAME_NOT_RENDERED:
return true;
case C.FIRST_FRAME_RENDERED:
long elapsedSinceLastRenderUs = msToUs(SystemClock.elapsedRealtime()) - lastRenderTimeUs;
return isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs);
default:
throw new IllegalStateException();
}
}
private boolean hasOutput() { private boolean hasOutput() {
return outputMode != C.VIDEO_OUTPUT_MODE_NONE; return outputMode != C.VIDEO_OUTPUT_MODE_NONE;
} }
@ -897,7 +910,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
// If we know the video size, report it again immediately. // If we know the video size, report it again immediately.
maybeRenotifyVideoSizeChanged(); maybeRenotifyVideoSizeChanged();
// We haven't rendered to the new output yet. // We haven't rendered to the new output yet.
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
if (getState() == STATE_STARTED) { if (getState() == STATE_STARTED) {
setJoiningDeadlineMs(); setJoiningDeadlineMs();
} }
@ -905,7 +918,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
private void onOutputRemoved() { private void onOutputRemoved() {
clearReportedVideoSize(); clearReportedVideoSize();
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
} }
private void onOutputReset() { private void onOutputReset() {
@ -922,20 +935,19 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
: C.TIME_UNSET; : C.TIME_UNSET;
} }
private void clearRenderedFirstFrame() { private void lowerFirstFrameState(@C.FirstFrameState int firstFrameState) {
renderedFirstFrameAfterReset = false; this.firstFrameState = min(this.firstFrameState, firstFrameState);
} }
private void maybeNotifyRenderedFirstFrame() { private void maybeNotifyRenderedFirstFrame() {
renderedFirstFrameAfterEnable = true; if (firstFrameState != C.FIRST_FRAME_RENDERED) {
if (!renderedFirstFrameAfterReset) { firstFrameState = C.FIRST_FRAME_RENDERED;
renderedFirstFrameAfterReset = true;
eventDispatcher.renderedFirstFrame(output); eventDispatcher.renderedFirstFrame(output);
} }
} }
private void maybeRenotifyRenderedFirstFrame() { private void maybeRenotifyRenderedFirstFrame() {
if (renderedFirstFrameAfterReset) { if (firstFrameState == C.FIRST_FRAME_RENDERED) {
eventDispatcher.renderedFirstFrame(output); eventDispatcher.renderedFirstFrame(output);
} }
} }

View file

@ -166,9 +166,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Nullable private PlaceholderSurface placeholderSurface; @Nullable private PlaceholderSurface placeholderSurface;
private boolean haveReportedFirstFrameRenderedForCurrentSurface; private boolean haveReportedFirstFrameRenderedForCurrentSurface;
private @C.VideoScalingMode int scalingMode; private @C.VideoScalingMode int scalingMode;
private boolean renderedFirstFrameAfterReset; private @C.FirstFrameState int firstFrameState;
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
private long initialPositionUs; private long initialPositionUs;
private long joiningDeadlineMs; private long joiningDeadlineMs;
private long droppedFrameAccumulationStartTimeMs; private long droppedFrameAccumulationStartTimeMs;
@ -412,6 +410,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
scalingMode = C.VIDEO_SCALING_MODE_DEFAULT; scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
decodedVideoSize = VideoSize.UNKNOWN; decodedVideoSize = VideoSize.UNKNOWN;
tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET;
firstFrameState = C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
clearReportedVideoSize(); clearReportedVideoSize();
} }
@ -601,13 +600,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
releaseCodec(); releaseCodec();
} }
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream; firstFrameState =
renderedFirstFrameAfterEnable = false; mayRenderStartOfStream
? C.FIRST_FRAME_NOT_RENDERED
: C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
} }
@Override @Override
public void enableMayRenderStartOfStream() { public void enableMayRenderStartOfStream() {
mayRenderFirstFrameAfterEnableIfNotStarted = true; if (firstFrameState == C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED) {
firstFrameState = C.FIRST_FRAME_NOT_RENDERED;
}
} }
@Override @Override
@ -616,7 +619,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (videoFrameProcessorManager.isEnabled()) { if (videoFrameProcessorManager.isEnabled()) {
videoFrameProcessorManager.flush(); videoFrameProcessorManager.flush();
} }
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
frameReleaseHelper.onPositionReset(); frameReleaseHelper.onPositionReset();
lastBufferPresentationTimeUs = C.TIME_UNSET; lastBufferPresentationTimeUs = C.TIME_UNSET;
initialPositionUs = C.TIME_UNSET; initialPositionUs = C.TIME_UNSET;
@ -641,7 +644,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
public boolean isReady() { public boolean isReady() {
if (super.isReady() if (super.isReady()
&& (!videoFrameProcessorManager.isEnabled() || videoFrameProcessorManager.isReady()) && (!videoFrameProcessorManager.isEnabled() || videoFrameProcessorManager.isReady())
&& (renderedFirstFrameAfterReset && (firstFrameState == C.FIRST_FRAME_RENDERED
|| (placeholderSurface != null && displaySurface == placeholderSurface) || (placeholderSurface != null && displaySurface == placeholderSurface)
|| getCodec() == null || getCodec() == null
|| tunneling)) { || tunneling)) {
@ -685,7 +688,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
clearReportedVideoSize(); clearReportedVideoSize();
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED);
haveReportedFirstFrameRenderedForCurrentSurface = false; haveReportedFirstFrameRenderedForCurrentSurface = false;
tunnelingOnFrameRenderedListener = null; tunnelingOnFrameRenderedListener = null;
try { try {
@ -801,7 +804,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// If we know the video size, report it again immediately. // If we know the video size, report it again immediately.
maybeRenotifyVideoSizeChanged(); maybeRenotifyVideoSizeChanged();
// We haven't rendered to the new display surface yet. // We haven't rendered to the new display surface yet.
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
if (state == STATE_STARTED) { if (state == STATE_STARTED) {
// Set joining deadline to report MediaCodecVideoRenderer is ready. // Set joining deadline to report MediaCodecVideoRenderer is ready.
setJoiningDeadlineMs(); setJoiningDeadlineMs();
@ -814,7 +817,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} else { } else {
// The display surface has been removed. // The display surface has been removed.
clearReportedVideoSize(); clearReportedVideoSize();
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
if (videoFrameProcessorManager.isEnabled()) { if (videoFrameProcessorManager.isEnabled()) {
videoFrameProcessorManager.clearOutputSurfaceInfo(); videoFrameProcessorManager.clearOutputSurfaceInfo();
} }
@ -1334,17 +1337,28 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
/** Returns whether a buffer or a processed frame should be force rendered. */ /** Returns whether a buffer or a processed frame should be force rendered. */
private boolean shouldForceRender(long positionUs, long earlyUs) { private boolean shouldForceRender(long positionUs, long earlyUs) {
if (joiningDeadlineMs != C.TIME_UNSET) {
// No force rendering during joining.
return false;
}
if (positionUs < getOutputStreamOffsetUs()) {
// No force rendering if we haven't reached the stream start position.
// TODO: b/160461756 - This is a bug because it compares against the offset and not the start
// position and also should only be applied when transitioning streams, not after every reset.
return false;
}
boolean isStarted = getState() == STATE_STARTED; boolean isStarted = getState() == STATE_STARTED;
boolean shouldRenderFirstFrame = switch (firstFrameState) {
!renderedFirstFrameAfterEnable case C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED:
? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted) return isStarted;
: !renderedFirstFrameAfterReset; case C.FIRST_FRAME_NOT_RENDERED:
long elapsedSinceLastRenderUs = msToUs(getClock().elapsedRealtime()) - lastRenderRealtimeUs; return true;
// Don't force output until we joined and the position reached the current stream. case C.FIRST_FRAME_RENDERED:
return joiningDeadlineMs == C.TIME_UNSET long elapsedSinceLastRenderUs = msToUs(getClock().elapsedRealtime()) - lastRenderRealtimeUs;
&& positionUs >= getOutputStreamOffsetUs() return isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs);
&& (shouldRenderFirstFrame default:
|| (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))); throw new IllegalStateException();
}
} }
/** /**
@ -1417,7 +1431,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override @Override
protected void onProcessedStreamChange() { protected void onProcessedStreamChange() {
super.onProcessedStreamChange(); super.onProcessedStreamChange();
clearRenderedFirstFrame(); lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
} }
/** /**
@ -1701,8 +1715,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
: C.TIME_UNSET; : C.TIME_UNSET;
} }
private void clearRenderedFirstFrame() { private void lowerFirstFrameState(@C.FirstFrameState int firstFrameState) {
renderedFirstFrameAfterReset = false; this.firstFrameState = min(this.firstFrameState, firstFrameState);
// The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for
// non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and
// OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and
@ -1717,9 +1731,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
/* package */ void maybeNotifyRenderedFirstFrame() { /* package */ void maybeNotifyRenderedFirstFrame() {
renderedFirstFrameAfterEnable = true; if (firstFrameState != C.FIRST_FRAME_RENDERED) {
if (!renderedFirstFrameAfterReset) { firstFrameState = C.FIRST_FRAME_RENDERED;
renderedFirstFrameAfterReset = true;
eventDispatcher.renderedFirstFrame(displaySurface); eventDispatcher.renderedFirstFrame(displaySurface);
haveReportedFirstFrameRenderedForCurrentSurface = true; haveReportedFirstFrameRenderedForCurrentSurface = true;
} }