Move video frame release logic to VideoFrameReleaseControl

This change moves the release timestamp adjustment logic out of
MediaCodecVideoRenderer and into a standalone component, the
VideoFrameReleaseControl. The plan for VideoFrameReleaseControl is to use
it:
- from MediaCodecVideoRenderer, when ExoPlayer plays video in standalone
  mode.
- from the CompositionPlayer's DefaultVideoSink, when CompositionPlayer
  supports multiple sequences.
- (temporarily) from the CompositionPlayer's custom ImageRenderer while
  we are implementing single-sequence preview, which is an intermediate
  milestone for composition preview.
PiperOrigin-RevId: 574420427
This commit is contained in:
christosts 2023-10-18 03:14:21 -07:00 committed by Copybara-Service
parent ff330bd8e9
commit eafe2e35f0
7 changed files with 1247 additions and 410 deletions

View File

@ -92,6 +92,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
this.index = index; this.index = index;
this.playerId = playerId; this.playerId = playerId;
this.clock = clock; this.clock = clock;
onInit();
} }
@Override @Override
@ -263,6 +264,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Methods to be overridden by subclasses. // Methods to be overridden by subclasses.
/** Called when the renderer is initialized. */
protected void onInit() {
// Do nothing
}
/** /**
* Called when the renderer is enabled. * Called when the renderer is enabled.
* *

View File

@ -40,6 +40,7 @@ import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoGraph; import androidx.media3.common.VideoGraph;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.LongArrayQueue; import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.Size; import androidx.media3.common.util.Size;
import androidx.media3.common.util.TimedValueQueue; import androidx.media3.common.util.TimedValueQueue;
@ -62,7 +63,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Context context; private final Context context;
private final PreviewingVideoGraph.Factory previewingVideoGraphFactory; private final PreviewingVideoGraph.Factory previewingVideoGraphFactory;
private final VideoSink.RenderControl renderControl; private final VideoFrameReleaseControl videoFrameReleaseControl;
@Nullable private VideoSinkImpl videoSinkImpl; @Nullable private VideoSinkImpl videoSinkImpl;
@Nullable private List<Effect> videoEffects; @Nullable private List<Effect> videoEffects;
@ -73,7 +74,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public CompositingVideoSinkProvider( public CompositingVideoSinkProvider(
Context context, Context context,
VideoFrameProcessor.Factory videoFrameProcessorFactory, VideoFrameProcessor.Factory videoFrameProcessorFactory,
VideoSink.RenderControl renderControl) { VideoFrameReleaseControl renderControl) {
this( this(
context, context,
new ReflectivePreviewingSingleInputVideoGraphFactory(videoFrameProcessorFactory), new ReflectivePreviewingSingleInputVideoGraphFactory(videoFrameProcessorFactory),
@ -84,10 +85,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ CompositingVideoSinkProvider( /* package */ CompositingVideoSinkProvider(
Context context, Context context,
PreviewingVideoGraph.Factory previewingVideoGraphFactory, PreviewingVideoGraph.Factory previewingVideoGraphFactory,
VideoSink.RenderControl renderControl) { VideoFrameReleaseControl releaseControl) {
this.context = context; this.context = context;
this.previewingVideoGraphFactory = previewingVideoGraphFactory; this.previewingVideoGraphFactory = previewingVideoGraphFactory;
this.renderControl = renderControl; this.videoFrameReleaseControl = releaseControl;
} }
@Override @Override
@ -97,7 +98,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try { try {
videoSinkImpl = videoSinkImpl =
new VideoSinkImpl(context, previewingVideoGraphFactory, renderControl, sourceFormat); new VideoSinkImpl(
context, previewingVideoGraphFactory, videoFrameReleaseControl, sourceFormat);
} catch (VideoFrameProcessingException e) { } catch (VideoFrameProcessingException e) {
throw new VideoSink.VideoSinkException(e, sourceFormat); throw new VideoSink.VideoSinkException(e, sourceFormat);
} }
@ -173,7 +175,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final class VideoSinkImpl implements VideoSink, VideoGraph.Listener { private static final class VideoSinkImpl implements VideoSink, VideoGraph.Listener {
private final Context context; private final Context context;
private final VideoSink.RenderControl renderControl; private final VideoFrameReleaseControl videoFrameReleaseControl;
private final VideoFrameReleaseControl.FrameReleaseInfo videoFrameReleaseInfo;
private final VideoFrameProcessor videoFrameProcessor; private final VideoFrameProcessor videoFrameProcessor;
private final LongArrayQueue processedFramesBufferTimestampsUs; private final LongArrayQueue processedFramesBufferTimestampsUs;
private final TimedValueQueue<Long> streamOffsets; private final TimedValueQueue<Long> streamOffsets;
@ -207,11 +210,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private VideoSize processedFrameSize; private VideoSize processedFrameSize;
private VideoSize reportedVideoSize; private VideoSize reportedVideoSize;
private boolean pendingVideoSizeChange; private boolean pendingVideoSizeChange;
private boolean renderedFirstFrame;
private long inputStreamOffsetUs; private long inputStreamOffsetUs;
private boolean pendingInputStreamOffsetChange; private boolean pendingInputStreamOffsetChange;
private long outputStreamOffsetUs; private long outputStreamOffsetUs;
private float playbackSpeed;
// TODO b/292111083 - Remove the field and trigger the callback on every video size change. // TODO b/292111083 - Remove the field and trigger the callback on every video size change.
private boolean onVideoSizeChangedCalled; private boolean onVideoSizeChangedCalled;
@ -220,11 +221,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public VideoSinkImpl( public VideoSinkImpl(
Context context, Context context,
PreviewingVideoGraph.Factory previewingVideoGraphFactory, PreviewingVideoGraph.Factory previewingVideoGraphFactory,
RenderControl renderControl, VideoFrameReleaseControl videoFrameReleaseControl,
Format sourceFormat) Format sourceFormat)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
this.context = context; this.context = context;
this.renderControl = renderControl; this.videoFrameReleaseControl = videoFrameReleaseControl;
videoFrameReleaseInfo = new VideoFrameReleaseControl.FrameReleaseInfo();
processedFramesBufferTimestampsUs = new LongArrayQueue(); processedFramesBufferTimestampsUs = new LongArrayQueue();
streamOffsets = new TimedValueQueue<>(); streamOffsets = new TimedValueQueue<>();
videoSizeChanges = new TimedValueQueue<>(); videoSizeChanges = new TimedValueQueue<>();
@ -237,7 +239,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
lastCodecBufferPresentationTimestampUs = C.TIME_UNSET; lastCodecBufferPresentationTimestampUs = C.TIME_UNSET;
processedFrameSize = VideoSize.UNKNOWN; processedFrameSize = VideoSize.UNKNOWN;
reportedVideoSize = VideoSize.UNKNOWN; reportedVideoSize = VideoSize.UNKNOWN;
playbackSpeed = 1f;
// Playback thread handler. // Playback thread handler.
handler = Util.createHandlerForCurrentLooper(); handler = Util.createHandlerForCurrentLooper();
@ -293,7 +294,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
processedFramesBufferTimestampsUs.clear(); processedFramesBufferTimestampsUs.clear();
streamOffsets.clear(); streamOffsets.clear();
handler.removeCallbacksAndMessages(/* token= */ null); handler.removeCallbacksAndMessages(/* token= */ null);
renderedFirstFrame = false; videoFrameReleaseControl.reset();
if (registeredLastFrame) { if (registeredLastFrame) {
registeredLastFrame = false; registeredLastFrame = false;
processedLastFrame = false; processedLastFrame = false;
@ -303,7 +304,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public boolean isReady() { public boolean isReady() {
return renderedFirstFrame; return videoFrameReleaseControl.isReady(/* rendererReady= */ true);
} }
@Override @Override
@ -385,47 +386,50 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long bufferPresentationTimeUs = processedFramesBufferTimestampsUs.element(); long bufferPresentationTimeUs = processedFramesBufferTimestampsUs.element();
// check whether this buffer comes with a new stream offset. // check whether this buffer comes with a new stream offset.
if (maybeUpdateOutputStreamOffset(bufferPresentationTimeUs)) { if (maybeUpdateOutputStreamOffset(bufferPresentationTimeUs)) {
renderedFirstFrame = false; videoFrameReleaseControl.onProcessedStreamChange();
} }
long framePresentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; long framePresentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
boolean isLastFrame = processedLastFrame && processedFramesBufferTimestampsUs.size() == 1; boolean isLastFrame = processedLastFrame && processedFramesBufferTimestampsUs.size() == 1;
long frameRenderTimeNs = @VideoFrameReleaseControl.FrameReleaseAction
renderControl.getFrameRenderTimeNs( int frameReleaseAction =
bufferPresentationTimeUs, positionUs, elapsedRealtimeUs, playbackSpeed); videoFrameReleaseControl.getFrameReleaseAction(
if (frameRenderTimeNs == RenderControl.RENDER_TIME_TRY_AGAIN_LATER) { bufferPresentationTimeUs,
return; positionUs,
} else if (framePresentationTimeUs == RenderControl.RENDER_TIME_DROP) { elapsedRealtimeUs,
// TODO b/293873191 - Handle very late buffers and drop to key frame. Need to flush outputStreamOffsetUs,
// VideoFrameProcessor input frames in this case. isLastFrame,
releaseProcessedFrameInternal(VideoFrameProcessor.DROP_OUTPUT_FRAME, isLastFrame); videoFrameReleaseInfo);
continue; switch (frameReleaseAction) {
case VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER:
return;
case VideoFrameReleaseControl.FRAME_RELEASE_SKIP:
case VideoFrameReleaseControl.FRAME_RELEASE_SKIP_TO_KEYFRAME:
// Skipped frames are dropped.
case VideoFrameReleaseControl.FRAME_RELEASE_DROP:
case VideoFrameReleaseControl.FRAME_RELEASE_DROP_TO_KEYFRAME:
// TODO b/293873191 - Handle very late buffers and drop to key frame. Need to flush
// VideoFrameProcessor input frames in this case.
dropFrame(isLastFrame);
break;
case VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY:
case VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED:
renderFrame(
framePresentationTimeUs, bufferPresentationTimeUs, frameReleaseAction, isLastFrame);
break;
default:
throw new IllegalStateException(String.valueOf(frameReleaseAction));
} }
renderControl.onNextFrame(bufferPresentationTimeUs);
if (videoFrameMetadataListener != null) {
videoFrameMetadataListener.onVideoFrameAboutToBeRendered(
framePresentationTimeUs,
frameRenderTimeNs == RenderControl.RENDER_TIME_IMMEDIATELY
? System.nanoTime()
: frameRenderTimeNs,
checkNotNull(inputFormat),
/* mediaFormat= */ null);
}
releaseProcessedFrameInternal(
frameRenderTimeNs == RenderControl.RENDER_TIME_IMMEDIATELY
? VideoFrameProcessor.RENDER_OUTPUT_FRAME_IMMEDIATELY
: frameRenderTimeNs,
isLastFrame);
maybeNotifyVideoSizeChanged(bufferPresentationTimeUs);
} }
} }
@Override @Override
public void setPlaybackSpeed(float speed) { public void setPlaybackSpeed(float speed) {
checkArgument(speed >= 0.0); checkArgument(speed >= 0.0);
this.playbackSpeed = speed; videoFrameReleaseControl.setPlaybackSpeed(speed);
} }
// VideoGraph.Listener methods
@Override @Override
public void onOutputSizeChanged(int width, int height) { public void onOutputSizeChanged(int width, int height) {
VideoSize newVideoSize = new VideoSize(width, height); VideoSize newVideoSize = new VideoSize(width, height);
@ -477,12 +481,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new IllegalStateException(); throw new IllegalStateException();
} }
// Other methods
public void release() { public void release() {
videoFrameProcessor.release(); videoFrameProcessor.release();
handler.removeCallbacksAndMessages(/* token= */ null); handler.removeCallbacksAndMessages(/* token= */ null);
streamOffsets.clear(); streamOffsets.clear();
processedFramesBufferTimestampsUs.clear(); processedFramesBufferTimestampsUs.clear();
renderedFirstFrame = false;
} }
/** Sets the {@linkplain Effect video effects} to apply immediately. */ /** Sets the {@linkplain Effect video effects} to apply immediately. */
@ -541,18 +546,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
&& currentSurfaceAndSize.second.equals(outputResolution)) { && currentSurfaceAndSize.second.equals(outputResolution)) {
return; return;
} }
renderedFirstFrame = videoFrameReleaseControl.setOutputSurface(outputSurface);
currentSurfaceAndSize == null || currentSurfaceAndSize.first.equals(outputSurface);
currentSurfaceAndSize = Pair.create(outputSurface, outputResolution); currentSurfaceAndSize = Pair.create(outputSurface, outputResolution);
videoFrameProcessor.setOutputSurfaceInfo( videoFrameProcessor.setOutputSurfaceInfo(
new SurfaceInfo( new SurfaceInfo(
outputSurface, outputResolution.getWidth(), outputResolution.getHeight())); outputSurface, outputResolution.getWidth(), outputResolution.getHeight()));
} }
/** Clears the output surface info. */
public void clearOutputSurfaceInfo() { public void clearOutputSurfaceInfo() {
videoFrameProcessor.setOutputSurfaceInfo(null); videoFrameProcessor.setOutputSurfaceInfo(null);
currentSurfaceAndSize = null; currentSurfaceAndSize = null;
renderedFirstFrame = false;
} }
private boolean maybeUpdateOutputStreamOffset(long bufferPresentationTimeUs) { private boolean maybeUpdateOutputStreamOffset(long bufferPresentationTimeUs) {
@ -565,21 +569,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return updatedOffset; return updatedOffset;
} }
private void dropFrame(boolean isLastFrame) {
if (listenerExecutor != null) {
listenerExecutor.execute(
() -> {
if (listener != null) {
listener.onFrameDropped(this);
}
});
}
releaseProcessedFrameInternal(VideoFrameProcessor.DROP_OUTPUT_FRAME, isLastFrame);
}
private void renderFrame(
long framePresentationTimeUs,
long bufferPresentationTimeUs,
@VideoFrameReleaseControl.FrameReleaseAction int frameReleaseAction,
boolean isLastFrame) {
if (videoFrameMetadataListener != null) {
videoFrameMetadataListener.onVideoFrameAboutToBeRendered(
framePresentationTimeUs,
frameReleaseAction == VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY
? Clock.DEFAULT.nanoTime()
: videoFrameReleaseInfo.getReleaseTimeNs(),
checkNotNull(inputFormat),
/* mediaFormat= */ null);
}
if (videoFrameReleaseControl.onFrameReleasedIsFirstFrame() && listenerExecutor != null) {
listenerExecutor.execute(
() -> {
if (listener != null) {
listener.onFirstFrameRendered(/* videoSink= */ this);
}
});
}
releaseProcessedFrameInternal(
frameReleaseAction == VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY
? VideoFrameProcessor.RENDER_OUTPUT_FRAME_IMMEDIATELY
: videoFrameReleaseInfo.getReleaseTimeNs(),
isLastFrame);
maybeNotifyVideoSizeChanged(bufferPresentationTimeUs);
}
private void releaseProcessedFrameInternal(long releaseTimeNs, boolean isLastFrame) { private void releaseProcessedFrameInternal(long releaseTimeNs, boolean isLastFrame) {
videoFrameProcessor.renderOutputFrame(releaseTimeNs); videoFrameProcessor.renderOutputFrame(releaseTimeNs);
processedFramesBufferTimestampsUs.remove(); processedFramesBufferTimestampsUs.remove();
if (releaseTimeNs == VideoFrameProcessor.DROP_OUTPUT_FRAME) {
renderControl.onFrameDropped();
} else {
renderControl.onFrameRendered();
if (!renderedFirstFrame) {
if (listener != null) {
checkNotNull(listenerExecutor)
.execute(() -> checkNotNull(listener).onFirstFrameRendered(this));
}
renderedFirstFrame = true;
}
}
if (isLastFrame) { if (isLastFrame) {
releasedLastFrame = true; releasedLastFrame = true;
} }

View File

@ -18,7 +18,7 @@ package androidx.media3.exoplayer.video;
import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED; import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO;
@ -38,7 +38,6 @@ import android.media.MediaFormat;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.SystemClock;
import android.util.Pair; import android.util.Pair;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
@ -57,7 +56,6 @@ import androidx.media3.common.PlaybackException;
import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.common.util.Size; import androidx.media3.common.util.Size;
@ -88,8 +86,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* Decodes and renders video using {@link MediaCodec}. * Decodes and renders video using {@link MediaCodec}.
@ -115,7 +113,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* </ul> * </ul>
*/ */
@UnstableApi @UnstableApi
public class MediaCodecVideoRenderer extends MediaCodecRenderer implements VideoSink.RenderControl { public class MediaCodecVideoRenderer extends MediaCodecRenderer
implements VideoFrameReleaseControl.FrameTimingEvaluator {
private static final String TAG = "MediaCodecVideoRenderer"; private static final String TAG = "MediaCodecVideoRenderer";
private static final String KEY_CROP_LEFT = "crop-left"; private static final String KEY_CROP_LEFT = "crop-left";
@ -139,19 +138,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
/** The minimum input buffer size for HEVC. */ /** The minimum input buffer size for HEVC. */
private static final int HEVC_MAX_INPUT_SIZE_THRESHOLD = 2 * 1024 * 1024; private static final int HEVC_MAX_INPUT_SIZE_THRESHOLD = 2 * 1024 * 1024;
/** The maximum earliest time, in microseconds, to release a frame on the surface. */
private static final long MAX_EARLY_US_THRESHOLD = 50_000;
private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround;
private static boolean deviceNeedsSetOutputSurfaceWorkaround; private static boolean deviceNeedsSetOutputSurfaceWorkaround;
private final Context context; private final Context context;
private final VideoFrameReleaseHelper frameReleaseHelper;
private final VideoSinkProvider videoSinkProvider; private final VideoSinkProvider videoSinkProvider;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify; private final int maxDroppedFramesToNotify;
private final boolean deviceNeedsNoPostProcessWorkaround; private final boolean deviceNeedsNoPostProcessWorkaround;
private final VideoFrameReleaseControl videoFrameReleaseControl;
private final VideoFrameReleaseControl.FrameReleaseInfo videoFrameReleaseInfo;
private @MonotonicNonNull CodecMaxValues codecMaxValues; private @MonotonicNonNull CodecMaxValues codecMaxValues;
private boolean codecNeedsSetOutputSurfaceWorkaround; private boolean codecNeedsSetOutputSurfaceWorkaround;
@ -160,15 +156,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
@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 @C.FirstFrameState int firstFrameState;
private long initialPositionUs;
private long joiningDeadlineMs;
private long droppedFrameAccumulationStartTimeMs; private long droppedFrameAccumulationStartTimeMs;
private int droppedFrames; private int droppedFrames;
private int consecutiveDroppedFrameCount; private int consecutiveDroppedFrameCount;
private int buffersInCodecCount; private int buffersInCodecCount;
private long lastBufferPresentationTimeUs;
private long lastRenderRealtimeUs;
private long totalVideoFrameProcessingOffsetUs; private long totalVideoFrameProcessingOffsetUs;
private int videoFrameProcessingOffsetCount; private int videoFrameProcessingOffsetCount;
private long lastFrameReleaseTimeNs; private long lastFrameReleaseTimeNs;
@ -393,23 +384,42 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
mediaCodecSelector, mediaCodecSelector,
enableDecoderFallback, enableDecoderFallback,
assumedMinimumCodecOperatingRate); assumedMinimumCodecOperatingRate);
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
frameReleaseHelper = new VideoFrameReleaseHelper(this.context);
videoFrameReleaseControl = new VideoFrameReleaseControl(this.context, allowedJoiningTimeMs);
videoFrameReleaseInfo = new VideoFrameReleaseControl.FrameReleaseInfo();
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
@SuppressWarnings("nullness:assignment")
VideoSink.@Initialized RenderControl renderControl = this;
videoSinkProvider = videoSinkProvider =
new CompositingVideoSinkProvider(context, videoFrameProcessorFactory, renderControl); new CompositingVideoSinkProvider(
context, videoFrameProcessorFactory, videoFrameReleaseControl);
deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround(); deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
joiningDeadlineMs = C.TIME_UNSET;
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; reportedVideoSize = null;
} }
// FrameTimingEvaluator methods
@Override
public boolean shouldForceReleaseFrame(long earlyUs, long elapsedSinceLastReleaseUs) {
return shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastReleaseUs);
}
@Override
public boolean shouldDropFrame(long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
return shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastFrame);
}
@Override
public boolean shouldDropFramesToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
return shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastFrame);
}
// Renderer methods
@Override @Override
public String getName() { public String getName() {
return TAG; return TAG;
@ -523,53 +533,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
format); format);
} }
// RenderControl implementation
@Override
public long getFrameRenderTimeNs(
long presentationTimeUs, long positionUs, long elapsedRealtimeUs, float playbackSpeed) {
long earlyUs =
calculateEarlyTimeUs(
positionUs,
elapsedRealtimeUs,
presentationTimeUs,
getState() == STATE_STARTED,
playbackSpeed,
getClock());
if (isBufferLate(earlyUs)) {
return VideoSink.RenderControl.RENDER_TIME_DROP;
}
if (shouldForceRender(positionUs, earlyUs)) {
return VideoSink.RenderControl.RENDER_TIME_IMMEDIATELY;
}
if (getState() != STATE_STARTED
|| positionUs == initialPositionUs
|| earlyUs > MAX_EARLY_US_THRESHOLD) {
return VideoSink.RenderControl.RENDER_TIME_TRY_AGAIN_LATER;
}
// Compute the buffer's desired release time in nanoseconds.
long unadjustedFrameReleaseTimeNs = getClock().nanoTime() + (earlyUs * 1000);
// Apply a timestamp adjustment, if there is one.
return frameReleaseHelper.adjustReleaseTime(unadjustedFrameReleaseTimeNs);
}
@Override
public void onNextFrame(long presentationTimeUs) {
frameReleaseHelper.onNextFrame(presentationTimeUs);
}
@Override
public void onFrameRendered() {
lastRenderRealtimeUs = Util.msToUs(getClock().elapsedRealtime());
}
@Override
public void onFrameDropped() {
updateDroppedBufferCounters(
/* droppedInputBufferCount= */ 0, /* droppedDecoderBufferCount= */ 1);
}
// Other methods // Other methods
/** /**
@ -634,6 +597,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} }
} }
@Override
protected void onInit() {
super.onInit();
videoFrameReleaseControl.setFrameTimingEvaluator(/* frameTimingEvaluator= */ this);
videoFrameReleaseControl.setClock(getClock());
}
@Override @Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException { throws ExoPlaybackException {
@ -645,17 +615,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
releaseCodec(); releaseCodec();
} }
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
firstFrameState = videoFrameReleaseControl.onEnabled(mayRenderStartOfStream);
mayRenderStartOfStream
? C.FIRST_FRAME_NOT_RENDERED
: C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
} }
@Override @Override
public void enableMayRenderStartOfStream() { public void enableMayRenderStartOfStream() {
if (firstFrameState == C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED) { videoFrameReleaseControl.allowReleaseFirstFrameBeforeStarted();
firstFrameState = C.FIRST_FRAME_NOT_RENDERED;
}
} }
@Override @Override
@ -666,21 +631,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
videoSink.flush(); videoSink.flush();
} }
super.onPositionReset(positionUs, joining); super.onPositionReset(positionUs, joining);
if (videoSinkProvider.isInitialized()) { if (videoSinkProvider.isInitialized()) {
videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs()); videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs());
} }
videoFrameReleaseControl.reset();
lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
frameReleaseHelper.onPositionReset();
lastBufferPresentationTimeUs = C.TIME_UNSET;
initialPositionUs = C.TIME_UNSET;
consecutiveDroppedFrameCount = 0;
if (joining) { if (joining) {
setJoiningDeadlineMs(); videoFrameReleaseControl.join();
} else {
joiningDeadlineMs = C.TIME_UNSET;
} }
maybeUpdateOnFrameRenderedListener();
consecutiveDroppedFrameCount = 0;
} }
@Override @Override
@ -690,26 +649,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
@Override @Override
public boolean isReady() { public boolean isReady() {
if (super.isReady() boolean readyToReleaseFrames = super.isReady() && (videoSink == null || videoSink.isReady());
&& (videoSink == null || videoSink.isReady()) if (readyToReleaseFrames
&& (firstFrameState == C.FIRST_FRAME_RENDERED && ((placeholderSurface != null && displaySurface == placeholderSurface)
|| (placeholderSurface != null && displaySurface == placeholderSurface)
|| getCodec() == null || getCodec() == null
|| tunneling)) { || tunneling)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline. // Not releasing frames.
joiningDeadlineMs = C.TIME_UNSET;
return true; return true;
} else if (joiningDeadlineMs == C.TIME_UNSET) {
// Not joining.
return false;
} else if (getClock().elapsedRealtime() < joiningDeadlineMs) {
// Joining and still within the joining deadline.
return true;
} else {
// The joining deadline has been exceeded. Give up and clear the deadline.
joiningDeadlineMs = C.TIME_UNSET;
return false;
} }
return videoFrameReleaseControl.isReady(readyToReleaseFrames);
} }
@Override @Override
@ -718,25 +666,24 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
droppedFrames = 0; droppedFrames = 0;
long elapsedRealtimeMs = getClock().elapsedRealtime(); long elapsedRealtimeMs = getClock().elapsedRealtime();
droppedFrameAccumulationStartTimeMs = elapsedRealtimeMs; droppedFrameAccumulationStartTimeMs = elapsedRealtimeMs;
lastRenderRealtimeUs = msToUs(elapsedRealtimeMs);
totalVideoFrameProcessingOffsetUs = 0; totalVideoFrameProcessingOffsetUs = 0;
videoFrameProcessingOffsetCount = 0; videoFrameProcessingOffsetCount = 0;
frameReleaseHelper.onStarted(); videoFrameReleaseControl.onStarted();
} }
@Override @Override
protected void onStopped() { protected void onStopped() {
joiningDeadlineMs = C.TIME_UNSET;
maybeNotifyDroppedFrames(); maybeNotifyDroppedFrames();
maybeNotifyVideoFrameProcessingOffset(); maybeNotifyVideoFrameProcessingOffset();
frameReleaseHelper.onStopped(); videoFrameReleaseControl.onStopped();
super.onStopped(); super.onStopped();
} }
@Override @Override
protected void onDisabled() { protected void onDisabled() {
reportedVideoSize = null; reportedVideoSize = null;
lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED); videoFrameReleaseControl.onDisabled();
maybeUpdateOnFrameRenderedListener();
haveReportedFirstFrameRenderedForCurrentSurface = false; haveReportedFirstFrameRenderedForCurrentSurface = false;
tunnelingOnFrameRenderedListener = null; tunnelingOnFrameRenderedListener = null;
try { try {
@ -783,7 +730,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} }
break; break;
case MSG_SET_CHANGE_FRAME_RATE_STRATEGY: case MSG_SET_CHANGE_FRAME_RATE_STRATEGY:
frameReleaseHelper.setChangeFrameRateStrategy((int) checkNotNull(message)); videoFrameReleaseControl.setChangeFrameRateStrategy((int) checkNotNull(message));
break; break;
case MSG_SET_VIDEO_FRAME_METADATA_LISTENER: case MSG_SET_VIDEO_FRAME_METADATA_LISTENER:
frameMetadataListener = (VideoFrameMetadataListener) checkNotNull(message); frameMetadataListener = (VideoFrameMetadataListener) checkNotNull(message);
@ -843,7 +790,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
// We only need to update the codec if the display surface has changed. // We only need to update the codec if the display surface has changed.
if (this.displaySurface != displaySurface) { if (this.displaySurface != displaySurface) {
this.displaySurface = displaySurface; this.displaySurface = displaySurface;
frameReleaseHelper.onSurfaceChanged(displaySurface); videoFrameReleaseControl.setOutputSurface(displaySurface);
haveReportedFirstFrameRenderedForCurrentSurface = false; haveReportedFirstFrameRenderedForCurrentSurface = false;
@State int state = getState(); @State int state = getState();
@ -862,11 +809,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
if (displaySurface != null && displaySurface != placeholderSurface) { if (displaySurface != null && displaySurface != placeholderSurface) {
// 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.
lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
if (state == STATE_STARTED) { if (state == STATE_STARTED) {
// Set joining deadline to report MediaCodecVideoRenderer is ready. videoFrameReleaseControl.join();
setJoiningDeadlineMs();
} }
// When effects previewing is enabled, set display surface and an unknown size. // When effects previewing is enabled, set display surface and an unknown size.
if (videoSinkProvider.isInitialized()) { if (videoSinkProvider.isInitialized()) {
@ -875,11 +819,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} else { } else {
// The display surface has been removed. // The display surface has been removed.
reportedVideoSize = null; reportedVideoSize = null;
lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
if (videoSinkProvider.isInitialized()) { if (videoSinkProvider.isInitialized()) {
videoSinkProvider.clearOutputSurfaceInfo(); videoSinkProvider.clearOutputSurfaceInfo();
} }
} }
maybeUpdateOnFrameRenderedListener();
} else if (displaySurface != null && displaySurface != placeholderSurface) { } else if (displaySurface != null && displaySurface != placeholderSurface) {
// The display surface is set and unchanged. If we know the video size and/or have already // The display surface is set and unchanged. If we know the video size and/or have already
// rendered to the display surface, report these again immediately. // rendered to the display surface, report these again immediately.
@ -987,7 +931,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
public void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed) public void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed)
throws ExoPlaybackException { throws ExoPlaybackException {
super.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed); super.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed);
frameReleaseHelper.onPlaybackSpeed(currentPlaybackSpeed); videoFrameReleaseControl.setPlaybackSpeed(currentPlaybackSpeed);
if (videoSink != null) { if (videoSink != null) {
videoSink.setPlaybackSpeed(currentPlaybackSpeed); videoSink.setPlaybackSpeed(currentPlaybackSpeed);
} }
@ -1104,7 +1048,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
new VideoSink.Listener() { new VideoSink.Listener() {
@Override @Override
public void onFirstFrameRendered(VideoSink videoSink) { public void onFirstFrameRendered(VideoSink videoSink) {
maybeNotifyRenderedFirstFrame(); checkStateNotNull(displaySurface);
notifyRenderedFirstFrame();
}
@Override
public void onFrameDropped(VideoSink videoSink) {
updateDroppedBufferCounters(
/* droppedInputBufferCount= */ 0, /* droppedDecoderBufferCount= */ 1);
} }
@Override @Override
@ -1245,7 +1196,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} }
decodedVideoSize = decodedVideoSize =
new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
frameReleaseHelper.onFormatChanged(format.frameRate); videoFrameReleaseControl.setFrameRate(format.frameRate);
if (videoSink != null && mediaFormat != null) { if (videoSink != null && mediaFormat != null) {
onReadyToRegisterVideoSinkInputStream(); onReadyToRegisterVideoSinkInputStream();
@ -1319,40 +1270,34 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
throws ExoPlaybackException { throws ExoPlaybackException {
checkNotNull(codec); // Can not render video without codec checkNotNull(codec); // Can not render video without codec
if (initialPositionUs == C.TIME_UNSET) {
initialPositionUs = positionUs;
}
if (bufferPresentationTimeUs != lastBufferPresentationTimeUs) {
if (videoSink == null) {
frameReleaseHelper.onNextFrame(bufferPresentationTimeUs);
} // else, update the frameReleaseHelper when releasing the processed frames.
this.lastBufferPresentationTimeUs = bufferPresentationTimeUs;
}
long outputStreamOffsetUs = getOutputStreamOffsetUs(); long outputStreamOffsetUs = getOutputStreamOffsetUs();
long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
@VideoFrameReleaseControl.FrameReleaseAction
int frameReleaseAction =
videoFrameReleaseControl.getFrameReleaseAction(
bufferPresentationTimeUs,
positionUs,
elapsedRealtimeUs,
getOutputStreamStartPositionUs(),
isLastBuffer,
videoFrameReleaseInfo);
// Skip decode-only buffers, e.g. after seeking, immediately. This check must be performed after
// getting the release action from the video frame release control although not necessary.
// That's because the release control estimates the content frame rate from frame timestamps
// and we want to have this information known as early as possible, especially during seeking.
if (isDecodeOnlyBuffer && !isLastBuffer) { if (isDecodeOnlyBuffer && !isLastBuffer) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true; return true;
} }
boolean isStarted = getState() == STATE_STARTED; // We are not rendering on a surface, the renderer will wait until a surface is set.
long earlyUs =
calculateEarlyTimeUs(
positionUs,
elapsedRealtimeUs,
bufferPresentationTimeUs,
isStarted,
getPlaybackSpeed(),
getClock());
if (displaySurface == placeholderSurface) { if (displaySurface == placeholderSurface) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes. // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(earlyUs)) { if (videoFrameReleaseInfo.getEarlyUs() < 30_000) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
updateVideoFrameProcessingOffsetCounters(earlyUs); updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true; return true;
} }
return false; return false;
@ -1368,137 +1313,86 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
return true; return true;
} }
boolean forceRenderOutputBuffer = shouldForceRender(positionUs, earlyUs); switch (frameReleaseAction) {
if (forceRenderOutputBuffer) { case VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY:
long releaseTimeNs = getClock().nanoTime(); long releaseTimeNs = getClock().nanoTime();
notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format); notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format);
renderOutputBuffer(codec, bufferIndex, presentationTimeUs, releaseTimeNs); renderOutputBuffer(codec, bufferIndex, presentationTimeUs, releaseTimeNs);
updateVideoFrameProcessingOffsetCounters(earlyUs); updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true; return true;
} case VideoFrameReleaseControl.FRAME_RELEASE_SKIP:
if (!isStarted || positionUs == initialPositionUs) {
return false;
}
// Compute the buffer's desired release time in nanoseconds.
long systemTimeNs = getClock().nanoTime();
long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
// Apply a timestamp adjustment, if there is one.
long adjustedReleaseTimeNs = frameReleaseHelper.adjustReleaseTime(unadjustedFrameReleaseTimeNs);
earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET;
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer)
&& maybeDropBuffersToKeyframe(positionUs, treatDroppedBuffersAsSkipped)) {
return false;
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) {
if (treatDroppedBuffersAsSkipped) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
} else { updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true;
case VideoFrameReleaseControl.FRAME_RELEASE_SKIP_TO_KEYFRAME:
if (!maybeDropBuffersToKeyframe(positionUs, /* treatDroppedBuffersAsSkipped= */ true)) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true;
} else {
return false;
}
case VideoFrameReleaseControl.FRAME_RELEASE_DROP:
dropOutputBuffer(codec, bufferIndex, presentationTimeUs); dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
} updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
updateVideoFrameProcessingOffsetCounters(earlyUs); return true;
return true; case VideoFrameReleaseControl.FRAME_RELEASE_DROP_TO_KEYFRAME:
if (!maybeDropBuffersToKeyframe(positionUs, /* treatDroppedBuffersAsSkipped= */ false)) {
dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true;
} else {
return false;
}
case VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER:
return false;
case VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED:
return maybeReleaseFrame(checkStateNotNull(codec), bufferIndex, presentationTimeUs, format);
default:
throw new IllegalStateException(String.valueOf(frameReleaseAction));
} }
}
private boolean maybeReleaseFrame(
MediaCodecAdapter codec, int bufferIndex, long presentationTimeUs, Format format) {
long releaseTimeNs = videoFrameReleaseInfo.getReleaseTimeNs();
long earlyUs = videoFrameReleaseInfo.getEarlyUs();
if (Util.SDK_INT >= 21) { if (Util.SDK_INT >= 21) {
// Let the underlying framework time the release. // Let the underlying framework time the release.
if (earlyUs < MAX_EARLY_US_THRESHOLD) { if (shouldSkipBuffersWithIdenticalReleaseTime() && releaseTimeNs == lastFrameReleaseTimeNs) {
if (shouldSkipBuffersWithIdenticalReleaseTime() // This frame should be displayed on the same vsync with the previous released frame. We
&& adjustedReleaseTimeNs == lastFrameReleaseTimeNs) { // are likely rendering frames at a rate higher than the screen refresh rate. Skip
// This frame should be displayed on the same vsync with the previous released frame. We // this buffer so that it's returned to MediaCodec sooner otherwise MediaCodec may not
// are likely rendering frames at a rate higher than the screen refresh rate. Skip // be able to keep decoding with this rate [b/263454203].
// this buffer so that it's returned to MediaCodec sooner otherwise MediaCodec may not skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
// be able to keep decoding with this rate [b/263454203]. } else {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs); notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format);
} else { renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs);
notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);
}
updateVideoFrameProcessingOffsetCounters(earlyUs);
lastFrameReleaseTimeNs = adjustedReleaseTimeNs;
return true;
} }
} else { updateVideoFrameProcessingOffsetCounters(earlyUs);
lastFrameReleaseTimeNs = releaseTimeNs;
return true;
} else if (earlyUs < 30000) {
// We need to time the release ourselves. // We need to time the release ourselves.
if (earlyUs < 30000) { if (earlyUs > 11000) {
if (earlyUs > 11000) { // We're a little too early to render the frame. Sleep until the frame can be rendered.
// We're a little too early to render the frame. Sleep until the frame can be rendered. // Note: The 11ms threshold was chosen fairly arbitrarily.
// Note: The 11ms threshold was chosen fairly arbitrarily. try {
try { // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.
// Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. Thread.sleep((earlyUs - 10000) / 1000);
Thread.sleep((earlyUs - 10000) / 1000); } catch (InterruptedException e) {
} catch (InterruptedException e) { Thread.currentThread().interrupt();
Thread.currentThread().interrupt(); return false;
return false;
}
} }
notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
updateVideoFrameProcessingOffsetCounters(earlyUs);
return true;
} }
} notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format);
renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
// We're either not playing, or it's not time to render the frame yet. updateVideoFrameProcessingOffsetCounters(earlyUs);
return false; return true;
} } else {
// Too soon.
/** Returns whether a buffer or a processed frame should be force rendered. */
private boolean shouldForceRender(long positionUs, long earlyUs) {
if (joiningDeadlineMs != C.TIME_UNSET) {
// No force rendering during joining.
return false; return false;
} }
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_NOT_RENDERED_AFTER_STREAM_CHANGE:
return positionUs >= getOutputStreamStartPositionUs();
case C.FIRST_FRAME_RENDERED:
long elapsedSinceLastRenderUs = msToUs(getClock().elapsedRealtime()) - lastRenderRealtimeUs;
return isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs);
default:
throw new IllegalStateException();
}
}
/**
* Calculates the time interval between the current player position and the buffer 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 bufferPresentationTimeUs The presentation time of the output buffer in microseconds,
* with {@linkplain #getOutputStreamOffsetUs() stream offset added}.
* @param isStarted Whether the playback is in {@link #STATE_STARTED}.
* @param playbackSpeed The current playback speed.
* @param clock The {@link Clock} used by the renderer.
* @return The calculated early time, in microseconds.
*/
private static long calculateEarlyTimeUs(
long positionUs,
long elapsedRealtimeUs,
long bufferPresentationTimeUs,
boolean isStarted,
float playbackSpeed,
Clock clock) {
// 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) ((bufferPresentationTimeUs - positionUs) / (double) playbackSpeed);
if (isStarted) {
// Account for the elapsed time since the start of this iteration of the rendering loop.
earlyUs -= Util.msToUs(clock.elapsedRealtime()) - elapsedRealtimeUs;
}
return earlyUs;
} }
private void notifyFrameMetadataListener( private void notifyFrameMetadataListener(
@ -1535,7 +1429,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
@Override @Override
protected void onProcessedStreamChange() { protected void onProcessedStreamChange() {
super.onProcessedStreamChange(); super.onProcessedStreamChange();
lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED_AFTER_STREAM_CHANGE); videoFrameReleaseControl.onProcessedStreamChange();
maybeUpdateOnFrameRenderedListener();
if (videoSinkProvider.isInitialized()) { if (videoSinkProvider.isInitialized()) {
videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs()); videoSinkProvider.setStreamOffsetUs(getOutputStreamOffsetUs());
} }
@ -1552,7 +1447,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
*/ */
protected boolean shouldDropOutputBuffer( protected boolean shouldDropOutputBuffer(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
return isBufferLate(earlyUs) && !isLastBuffer; return VideoFrameReleaseControl.FrameTimingEvaluator.DEFAULT.shouldDropFrame(
earlyUs, elapsedRealtimeUs, isLastBuffer);
} }
/** /**
@ -1567,7 +1463,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
*/ */
protected boolean shouldDropBuffersToKeyframe( protected boolean shouldDropBuffersToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
return isBufferVeryLate(earlyUs) && !isLastBuffer; return VideoFrameReleaseControl.FrameTimingEvaluator.DEFAULT.shouldDropFramesToKeyframe(
earlyUs, elapsedRealtimeUs, isLastBuffer);
} }
/** /**
@ -1588,8 +1485,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
* @return Returns whether to force rendering an output buffer. * @return Returns whether to force rendering an output buffer.
*/ */
protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) {
// Force render late buffers every 100ms to avoid frozen video effect. return VideoFrameReleaseControl.FrameTimingEvaluator.DEFAULT.shouldForceReleaseFrame(
return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; earlyUs, elapsedSinceLastRenderUs);
} }
/** /**
@ -1721,7 +1618,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
decoderCounters.renderedOutputBufferCount++; decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0; consecutiveDroppedFrameCount = 0;
if (videoSink == null) { if (videoSink == null) {
lastRenderRealtimeUs = msToUs(getClock().elapsedRealtime());
maybeNotifyVideoSizeChanged(decodedVideoSize); maybeNotifyVideoSizeChanged(decodedVideoSize);
maybeNotifyRenderedFirstFrame(); maybeNotifyRenderedFirstFrame();
} }
@ -1745,7 +1641,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
decoderCounters.renderedOutputBufferCount++; decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0; consecutiveDroppedFrameCount = 0;
if (videoSink == null) { if (videoSink == null) {
lastRenderRealtimeUs = msToUs(getClock().elapsedRealtime());
maybeNotifyVideoSizeChanged(decodedVideoSize); maybeNotifyVideoSizeChanged(decodedVideoSize);
maybeNotifyRenderedFirstFrame(); maybeNotifyRenderedFirstFrame();
} }
@ -1769,15 +1664,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} }
} }
private void setJoiningDeadlineMs() { private void maybeUpdateOnFrameRenderedListener() {
joiningDeadlineMs =
allowedJoiningTimeMs > 0
? (getClock().elapsedRealtime() + allowedJoiningTimeMs)
: C.TIME_UNSET;
}
private void lowerFirstFrameState(@C.FirstFrameState int firstFrameState) {
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
@ -1792,13 +1679,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} }
private void maybeNotifyRenderedFirstFrame() { private void maybeNotifyRenderedFirstFrame() {
if (displaySurface != null && firstFrameState != C.FIRST_FRAME_RENDERED) { if (videoFrameReleaseControl.onFrameReleasedIsFirstFrame() && displaySurface != null) {
firstFrameState = C.FIRST_FRAME_RENDERED; notifyRenderedFirstFrame();
eventDispatcher.renderedFirstFrame(displaySurface);
haveReportedFirstFrameRenderedForCurrentSurface = true;
} }
} }
@RequiresNonNull("displaySurface")
private void notifyRenderedFirstFrame() {
eventDispatcher.renderedFirstFrame(displaySurface);
haveReportedFirstFrameRenderedForCurrentSurface = true;
}
private void maybeRenotifyRenderedFirstFrame() { private void maybeRenotifyRenderedFirstFrame() {
if (displaySurface != null && haveReportedFirstFrameRenderedForCurrentSurface) { if (displaySurface != null && haveReportedFirstFrameRenderedForCurrentSurface) {
eventDispatcher.renderedFirstFrame(displaySurface); eventDispatcher.renderedFirstFrame(displaySurface);
@ -1838,16 +1729,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer implements Video
} }
} }
private static boolean isBufferLate(long earlyUs) {
// Class a buffer as late if it should have been presented more than 30 ms ago.
return earlyUs < -30000;
}
private static boolean isBufferVeryLate(long earlyUs) {
// Class a buffer as very late if it should have been presented more than 500 ms ago.
return earlyUs < -500000;
}
@RequiresApi(29) @RequiresApi(29)
private static void setHdr10PlusInfoV29(MediaCodecAdapter codec, byte[] hdr10PlusInfo) { private static void setHdr10PlusInfoV29(MediaCodecAdapter codec, byte[] hdr10PlusInfo) {
Bundle codecParameters = new Bundle(); Bundle codecParameters = new Bundle();

View File

@ -0,0 +1,466 @@
/*
* 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.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. */
/* package */ 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_DROP_TO_KEYFRAME}, {@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_DROP_TO_KEYFRAME,
FRAME_RELEASE_SKIP,
FRAME_RELEASE_SKIP_TO_KEYFRAME,
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 frames up to the next key-frame should be dropped. */
public static final int FRAME_RELEASE_DROP_TO_KEYFRAME = 3;
/** Signals that a frame should be skipped. */
public static final int FRAME_RELEASE_SKIP = 4;
/** Signals that frames up to the next key-frame should be skipped. */
public static final int FRAME_RELEASE_SKIP_TO_KEYFRAME = 5;
/** Signals that a frame should not be released and the renderer should try again later. */
public static final int FRAME_RELEASE_TRY_AGAIN_LATER = 6;
/** 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 to drop all frames starting from this frame to the keyframe at or after the
* current playback position, if possible.
*
* @param earlyUs The time until the current 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 frame is the last frame in the current stream.
*/
boolean shouldDropFramesToKeyframe(long earlyUs, long elapsedRealtimeUs, boolean isLastFrame);
/** The default timing evaluator. */
FrameTimingEvaluator DEFAULT =
new FrameTimingEvaluator() {
@Override
public boolean shouldForceReleaseFrame(long earlyUs, long elapsedSinceLastReleaseUs) {
// Force render late buffers every 100ms to avoid frozen video effect.
return earlyUs < MIN_EARLY_US_LATE_THRESHOLD && elapsedSinceLastReleaseUs > 100_000;
}
@Override
public boolean shouldDropFrame(
long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
return earlyUs < MIN_EARLY_US_LATE_THRESHOLD && !isLastFrame;
}
@Override
public boolean shouldDropFramesToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
return earlyUs < MIN_EARLY_US_VERY_LATE_THRESHOLD && !isLastFrame;
}
};
}
/** The earliest time threshold, in microseconds, after which a frame is considered late. */
private static final long MIN_EARLY_US_LATE_THRESHOLD = -30_000;
/** The earliest time threshold, in microseconds, after which a frame is considered very late. */
private static final long MIN_EARLY_US_VERY_LATE_THRESHOLD = -500_000;
/** 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 VideoFrameReleaseHelper frameReleaseHelper;
private final long allowedJoiningTimeMs;
private FrameTimingEvaluator frameTimingEvaluator;
private boolean started;
private @C.FirstFrameState int firstFrameState;
private long initialPositionUs;
private long lastReleaseRealtimeUs;
private long lastPresentationTimeUs;
private long joiningDeadlineMs;
private float playbackSpeed;
private Clock clock;
/**
* Creates an instance.
*
* @param applicationContext The application context.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which the renderer can
* attempt to seamlessly join an ongoing playback.
*/
public VideoFrameReleaseControl(Context applicationContext, long allowedJoiningTimeMs) {
frameReleaseHelper = new VideoFrameReleaseHelper(applicationContext);
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.frameTimingEvaluator = FrameTimingEvaluator.DEFAULT;
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;
}
/** Sets the {@link FrameTimingEvaluator}. */
public void setFrameTimingEvaluator(FrameTimingEvaluator frameTimingEvaluator) {
this.frameTimingEvaluator = frameTimingEvaluator;
}
/** 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 have 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.
*
* @see Renderer#isReady()
* @param rendererReady Whether the renderer is ready.
* @return Whether the release control is ready.
*/
public boolean isReady(boolean rendererReady) {
if (rendererReady && 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. */
public void join() {
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) {
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
|| frameReleaseInfo.earlyUs > MAX_EARLY_US_THRESHOLD) {
return FRAME_RELEASE_TRY_AGAIN_LATER;
}
// Calculate release time and 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.
boolean treatDropAsSkip = joiningDeadlineMs != C.TIME_UNSET;
if (frameTimingEvaluator.shouldDropFramesToKeyframe(
frameReleaseInfo.earlyUs, elapsedRealtimeUs, isLastFrame)) {
return treatDropAsSkip ? FRAME_RELEASE_SKIP_TO_KEYFRAME : FRAME_RELEASE_DROP_TO_KEYFRAME;
} else if (frameTimingEvaluator.shouldDropFrame(
frameReleaseInfo.earlyUs, elapsedRealtimeUs, isLastFrame)) {
// While joining, dropped buffers are considered skipped.
return treatDropAsSkip ? FRAME_RELEASE_SKIP : FRAME_RELEASE_DROP;
}
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;
}
/**
* Change the {@link C.VideoChangeFrameRateStrategy}, used when calling {@link
* Surface#setFrameRate}.
*/
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) {
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) {
// No force releasing during joining.
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();
}
}
}

View File

@ -55,6 +55,9 @@ import java.util.concurrent.Executor;
/** Called when the sink renderers the first frame. */ /** Called when the sink renderers the first frame. */
void onFirstFrameRendered(VideoSink videoSink); void onFirstFrameRendered(VideoSink videoSink);
/** Called when the sink dropped a frame. */
void onFrameDropped(VideoSink videoSink);
/** Called when the output video size changed. */ /** Called when the output video size changed. */
void onVideoSizeChanged(VideoSink videoSink, VideoSize videoSize); void onVideoSizeChanged(VideoSink videoSink, VideoSize videoSize);
@ -62,49 +65,6 @@ import java.util.concurrent.Executor;
void onError(VideoSink videoSink, VideoSinkException videoSinkException); void onError(VideoSink videoSink, VideoSinkException videoSinkException);
} }
/** Controls the rendering of video frames. */
interface RenderControl {
/** Signals a frame must be rendered immediately. */
long RENDER_TIME_IMMEDIATELY = -1;
/** Signals a frame must be dropped. */
long RENDER_TIME_DROP = -2;
/** Signals that a frame should not be rendered yet. */
long RENDER_TIME_TRY_AGAIN_LATER = -3;
/**
* Returns the render timestamp, in nanoseconds, associated with this video frames or one of the
* {@code RENDER_TIME_} constants if the frame must be rendered immediately, dropped or not
* rendered yet.
*
* @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 playbackSpeed The current playback speed.
* @return The render timestamp, in nanoseconds, associated with this frame, or one of the
* {@code RENDER_TIME_} constants if the frame must be rendered immediately, dropped or not
* rendered yet.
*/
long getFrameRenderTimeNs(
long presentationTimeUs, long positionUs, long elapsedRealtimeUs, float playbackSpeed);
/**
* Informs the rendering control that a video frame will be rendered. Call this method before
* rendering a frame.
*
* @param presentationTimeUs The frame's presentation time, in microseconds.
*/
void onNextFrame(long presentationTimeUs);
/** Informs the rendering control that a video frame was rendered. */
void onFrameRendered();
/** Informs the rendering control that a video frame was dropped. */
void onFrameDropped();
}
/** /**
* Specifies how the input frames are made available to the video sink. One of {@link * Specifies how the input frames are made available to the video sink. One of {@link
* #INPUT_TYPE_SURFACE} or {@link #INPUT_TYPE_BITMAP}. * #INPUT_TYPE_SURFACE} or {@link #INPUT_TYPE_BITMAP}.

View File

@ -132,11 +132,13 @@ public final class CompositingVideoSinkProviderTest {
} }
private static CompositingVideoSinkProvider createCompositingVideoSinkProvider() { private static CompositingVideoSinkProvider createCompositingVideoSinkProvider() {
VideoSink.RenderControl renderControl = new TestRenderControl(); VideoFrameReleaseControl releaseControl =
new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(), /* allowedJoiningTimeMs= */ 0);
return new CompositingVideoSinkProvider( return new CompositingVideoSinkProvider(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
new TestPreviewingVideoGraphFactory(), new TestPreviewingVideoGraphFactory(),
renderControl); releaseControl);
} }
private static class TestPreviewingVideoGraphFactory implements PreviewingVideoGraph.Factory { private static class TestPreviewingVideoGraphFactory implements PreviewingVideoGraph.Factory {
@ -161,22 +163,4 @@ public final class CompositingVideoSinkProviderTest {
return previewingVideoGraph; return previewingVideoGraph;
} }
} }
private static class TestRenderControl implements VideoSink.RenderControl {
@Override
public long getFrameRenderTimeNs(
long presentationTimeUs, long positionUs, long elapsedRealtimeUs, float playbackSpeed) {
return presentationTimeUs;
}
@Override
public void onNextFrame(long presentationTimeUs) {}
@Override
public void onFrameRendered() {}
@Override
public void onFrameDropped() {}
}
} }

View File

@ -0,0 +1,505 @@
/*
* 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 com.google.common.truth.Truth.assertThat;
import androidx.media3.test.utils.FakeClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link VideoFrameReleaseControl}. */
@RunWith(AndroidJUnit4.class)
public class VideoFrameReleaseControlTest {
@Test
public void isReady_onNewInstance_returnsFalse() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ true)).isFalse();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ false)).isFalse();
}
@Test
public void isReady_afterReleasingFrame_returnsTrue() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ true)).isTrue();
}
@Test
public void isReady_withinJoiningDeadline_returnsTrue() {
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
createVideoFrameReleaseControl(/* allowedJoiningTimeMs= */ 100);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.join();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ false)).isTrue();
}
@Test
public void isReady_joiningDeadlineExceeded_returnsFalse() {
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
createVideoFrameReleaseControl(/* allowedJoiningTimeMs= */ 100);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.join();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ false)).isTrue();
clock.advanceTime(/* timeDiffMs= */ 101);
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ false)).isFalse();
}
@Test
public void onFrameReleasedIsFirstFrame_resetsAfterOnEnabled() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
}
@Test
public void onFrameReleasedIsFirstFrame_resetsAfterOnProcessedStreamChange() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
videoFrameReleaseControl.onProcessedStreamChange();
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
}
@Test
public void onFrameReleasedIsFirstFrame_resetsAfterSetOutputSurface() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
videoFrameReleaseControl.setOutputSurface(/* outputSurface= */ null);
assertThat(videoFrameReleaseControl.onFrameReleasedIsFirstFrame()).isTrue();
}
@Test
public void isReady_afterReset_returnsFalse() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ true)).isTrue();
videoFrameReleaseControl.reset();
assertThat(videoFrameReleaseControl.isReady(/* rendererReady= */ true)).isFalse();
}
@Test
public void getFrameReleaseAction_firstFrameAllowedBeforeStart_returnsReleaseImmediately() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
}
@Test
public void getFrameReleaseAction_firstFrameNotAllowedBeforeStart_returnsTryAgainLater() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ false);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
}
@Test
public void
getFrameReleaseAction_firstFrameNotAllowedBeforeStartAndStarted_returnsReleaseImmediately() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ false);
videoFrameReleaseControl.onStarted();
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
}
@Test
public void getFrameReleaseAction_secondFrameAndNotStarted_returnsTryAgainLater() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
// Second frame
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 10_000,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
}
@Test
public void getFrameReleaseAction_secondFrameAndStarted_returnsScheduled() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
// Second frame
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 10_000,
/* positionUs= */ 1,
/* elapsedRealtimeUs= */ 1,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED);
}
@Test
public void getFrameReleaseAction_secondFrameEarly_returnsTryAgainLater() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
clock.advanceTime(/* timeDiffMs= */ 10);
// Second frame is 51 ms too soon.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 61_000,
/* positionUs= */ 10_000,
/* elapsedRealtimeUs= */ 10_000,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
assertThat(frameReleaseInfo.getEarlyUs()).isEqualTo(51_000);
}
@Test
public void getFrameReleaseAction_frameLate_returnsDrop() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
clock.advanceTime(/* timeDiffMs= */ 40);
// Second frame is 31 ms late.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 9_000,
/* positionUs= */ 40_000,
/* elapsedRealtimeUs= */ 40_000,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_DROP);
}
@Test
public void getFrameReleaseAction_frameLateWhileJoining_returnsSkip() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
createVideoFrameReleaseControl(/* allowedJoiningTimeMs= */ 1234);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
clock.advanceTime(/* timeDiffMs= */ 40);
// Start joining.
videoFrameReleaseControl.join();
// Second frame is 31 ms late.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 9_000,
/* positionUs= */ 40_000,
/* elapsedRealtimeUs= */ 40_000,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SKIP);
}
@Test
public void getFrameReleaseAction_frameVeryLate_returnsDropToKeyframe() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(), /* allowedJoiningTimeMs= */ 0);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.setFrameTimingEvaluator(
new TestFrameTimingEvaluator(/* shouldForceRelease= */ false));
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
clock.advanceTime(/* timeDiffMs= */ 1_000);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 1_000,
/* positionUs= */ 1_000_000,
/* elapsedRealtimeUs= */ 1_000_000,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_DROP_TO_KEYFRAME);
}
@Test
public void getFrameReleaseAction_frameVeryLateAndJoining_returnsSkipToFrame() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
createVideoFrameReleaseControl(/* allowedJoiningTimeMs= */ 1234);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
clock.advanceTime(/* timeDiffMs= */ 1_000);
videoFrameReleaseControl.join();
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 1_000,
/* positionUs= */ 1_000_000,
/* elapsedRealtimeUs= */ 1_000_000,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SKIP_TO_KEYFRAME);
}
@Test
public void
getFrameReleaseAction_frameVeryLateAndFrameTimeEvaluatorForcesFrameRelease_returnsReleaseImmediately() {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(), /* allowedJoiningTimeMs= */ 0);
videoFrameReleaseControl.setFrameTimingEvaluator(
/* frameTimingEvaluator= */ new TestFrameTimingEvaluator(/* shouldForceRelease= */ true));
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
// First frame released.
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
videoFrameReleaseControl.onFrameReleasedIsFirstFrame();
clock.advanceTime(/* timeDiffMs= */ 1_000);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 1_000,
/* positionUs= */ 1_000_000,
/* elapsedRealtimeUs= */ 1_000_000,
/* outputStreamStartPositionUs= */ 0,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
}
private static VideoFrameReleaseControl createVideoFrameReleaseControl() {
return createVideoFrameReleaseControl(/* allowedJoiningTimeMs= */ 0);
}
private static VideoFrameReleaseControl createVideoFrameReleaseControl(
long allowedJoiningTimeMs) {
return new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(), allowedJoiningTimeMs);
}
private static class TestFrameTimingEvaluator
implements VideoFrameReleaseControl.FrameTimingEvaluator {
private final boolean shouldForceRelease;
public TestFrameTimingEvaluator(boolean shouldForceRelease) {
this.shouldForceRelease = shouldForceRelease;
}
@Override
public boolean shouldForceReleaseFrame(long earlyUs, long elapsedSinceLastReleaseUs) {
return shouldForceRelease;
}
@Override
public boolean shouldDropFrame(long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
return VideoFrameReleaseControl.FrameTimingEvaluator.DEFAULT.shouldDropFrame(
earlyUs, elapsedRealtimeUs, isLastFrame);
}
@Override
public boolean shouldDropFramesToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
return VideoFrameReleaseControl.FrameTimingEvaluator.DEFAULT.shouldDropFramesToKeyframe(
earlyUs, elapsedRealtimeUs, isLastFrame);
}
}
}