Move decode-only and no surface logic inside VideoFrameReleaseControl

This brings the parts related to video frame release decision making
in a single place and simplifies the calling side in
MediaCodecVideoRenderer.

PiperOrigin-RevId: 737941729
(cherry picked from commit 0e169ab1bea3a4cd9ff2772d77618c66b5262f3c)
This commit is contained in:
tonihei 2025-03-18 03:44:46 -07:00
parent 0a284f4927
commit e2017e35db
7 changed files with 261 additions and 50 deletions

View File

@ -129,7 +129,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public boolean isReady(boolean rendererOtherwiseReady) {
return videoFrameReleaseControl.isReady(rendererOtherwiseReady);
return outputSurface == null || videoFrameReleaseControl.isReady(rendererOtherwiseReady);
}
@Override

View File

@ -1697,9 +1697,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
});
}
// The frame release action should be retrieved for all frames (even the ones that will be
// skipped), 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.
@VideoFrameReleaseControl.FrameReleaseAction
int frameReleaseAction =
videoFrameReleaseControl.getFrameReleaseAction(
@ -1707,31 +1704,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
positionUs,
elapsedRealtimeUs,
getOutputStreamStartPositionUs(),
isDecodeOnlyBuffer,
isLastBuffer,
videoFrameReleaseInfo);
if (frameReleaseAction == VideoFrameReleaseControl.FRAME_RELEASE_IGNORE) {
// The buffer is no longer valid and needs to be ignored.
return false;
}
// Skip decode-only buffers, e.g. after seeking, immediately.
if (isDecodeOnlyBuffer && !isLastBuffer) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
// We are not rendering on a surface, the renderer will wait until a surface is set.
if (displaySurface == null) {
// Skip frames in sync with playback, so we'll be at the right frame if a surface is set.
if (getState() == STATE_STARTED && videoFrameReleaseInfo.getEarlyUs() < 30_000) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true;
}
return false;
}
switch (frameReleaseAction) {
case VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY:
long releaseTimeNs = getClock().nanoTime();
@ -1748,6 +1723,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
updateVideoFrameProcessingOffsetCounters(videoFrameReleaseInfo.getEarlyUs());
return true;
case VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER:
case VideoFrameReleaseControl.FRAME_RELEASE_IGNORE:
return false;
case VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED:
releaseFrame(checkStateNotNull(codec), bufferIndex, presentationTimeUs, format);

View File

@ -43,7 +43,7 @@ public final class VideoFrameReleaseControl {
/**
* The frame release action returned by {@link #getFrameReleaseAction(long, long, long, long,
* boolean, FrameReleaseInfo)}.
* boolean, boolean, FrameReleaseInfo)}.
*
* <p>One of {@link #FRAME_RELEASE_IMMEDIATELY}, {@link #FRAME_RELEASE_SCHEDULED}, {@link
* #FRAME_RELEASE_DROP}, {@link #FRAME_RELEASE_IGNORE}, {@link ##FRAME_RELEASE_SKIP} or {@link
@ -180,14 +180,15 @@ public final class VideoFrameReleaseControl {
private boolean joiningRenderNextFrameImmediately;
private float playbackSpeed;
private Clock clock;
private boolean hasOutputSurface;
/**
* Creates an instance.
*
* @param applicationContext The application context.
* @param frameTimingEvaluator The {@link FrameTimingEvaluator} that will assist in {@linkplain
* #getFrameReleaseAction(long, long, long, long, boolean, FrameReleaseInfo) frame release
* actions}.
* #getFrameReleaseAction(long, long, long, long, boolean, boolean, FrameReleaseInfo) frame
* release actions}.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which the renderer can
* attempt to seamlessly join an ongoing playback.
*/
@ -240,6 +241,7 @@ public final class VideoFrameReleaseControl {
/** Called when the display surface changed. */
public void setOutputSurface(@Nullable Surface outputSurface) {
hasOutputSurface = outputSurface != null;
frameReleaseHelper.onSurfaceChanged(outputSurface);
lowerFirstFrameState(C.FIRST_FRAME_NOT_RENDERED);
}
@ -327,6 +329,8 @@ public final class VideoFrameReleaseControl {
* @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 isDecodeOnlyFrame Whether the frame is decode-only because its presentation time is
* before the intended start time.
* @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
@ -339,6 +343,7 @@ public final class VideoFrameReleaseControl {
long positionUs,
long elapsedRealtimeUs,
long outputStreamStartPositionUs,
boolean isDecodeOnlyFrame,
boolean isLastFrame,
FrameReleaseInfo frameReleaseInfo)
throws ExoPlaybackException {
@ -355,6 +360,23 @@ public final class VideoFrameReleaseControl {
frameReleaseInfo.earlyUs =
calculateEarlyTimeUs(positionUs, elapsedRealtimeUs, presentationTimeUs);
if (isDecodeOnlyFrame && !isLastFrame) {
return FRAME_RELEASE_SKIP;
}
if (!hasOutputSurface) {
// Skip frames in sync with playback, so we'll be at the right frame if a surface is set.
if (frameTimingEvaluator.shouldIgnoreFrame(
frameReleaseInfo.earlyUs,
positionUs,
elapsedRealtimeUs,
isLastFrame,
/* treatDroppedBuffersAsSkipped= */ true)) {
return FRAME_RELEASE_IGNORE;
}
return started && frameReleaseInfo.earlyUs < 30_000
? FRAME_RELEASE_SKIP
: FRAME_RELEASE_TRY_AGAIN_LATER;
}
if (shouldForceRelease(positionUs, frameReleaseInfo.earlyUs, outputStreamStartPositionUs)) {
return FRAME_RELEASE_IMMEDIATELY;
}

View File

@ -147,6 +147,7 @@ import androidx.media3.exoplayer.ExoPlaybackException;
positionUs,
elapsedRealtimeUs,
outputStreamStartPositionUs,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
videoFrameReleaseInfo);
switch (frameReleaseAction) {

View File

@ -17,16 +17,33 @@ package androidx.media3.exoplayer.video;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.test.utils.FakeClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link VideoFrameReleaseControl}. */
@RunWith(AndroidJUnit4.class)
public class VideoFrameReleaseControlTest {
private Surface surface;
@Before
public void setUp() {
surface = new Surface(new SurfaceTexture(/* texName= */ 0));
}
@After
public void tearDown() {
surface.release();
}
@Test
public void isReady_onNewInstance_returnsFalse() {
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
@ -152,6 +169,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -171,6 +189,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
@ -195,6 +214,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -217,6 +237,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -229,6 +250,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
@ -252,6 +274,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -264,6 +287,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 1,
/* elapsedRealtimeUs= */ 1,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SCHEDULED);
@ -288,6 +312,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -301,6 +326,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 10_000,
/* elapsedRealtimeUs= */ 10_000,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
@ -319,6 +345,7 @@ public class VideoFrameReleaseControlTest {
/* shouldDropFrame= */ true,
/* shouldIgnoreFrame= */ false),
/* allowedJoiningTimeMs= */ 0);
videoFrameReleaseControl.setOutputSurface(surface);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
@ -331,6 +358,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -344,6 +372,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 10_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_DROP);
@ -364,6 +393,7 @@ public class VideoFrameReleaseControlTest {
/* shouldDropFrame= */ true,
/* shouldIgnoreFrame= */ false),
/* allowedJoiningTimeMs= */ 1234);
videoFrameReleaseControl.setOutputSurface(surface);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
@ -378,6 +408,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 10_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
@ -388,6 +419,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 11_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SKIP);
@ -408,6 +440,7 @@ public class VideoFrameReleaseControlTest {
/* shouldDropFrame= */ true,
/* shouldIgnoreFrame= */ false),
/* allowedJoiningTimeMs= */ 1234);
videoFrameReleaseControl.setOutputSurface(surface);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
@ -422,6 +455,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 10_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -433,6 +467,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 11_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_DROP);
@ -451,6 +486,7 @@ public class VideoFrameReleaseControlTest {
/* shouldDropFrame= */ false,
/* shouldIgnoreFrame= */ true),
/* allowedJoiningTimeMs= */ 0);
videoFrameReleaseControl.setOutputSurface(surface);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
@ -463,6 +499,7 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
@ -475,21 +512,181 @@ public class VideoFrameReleaseControlTest {
/* positionUs= */ 1_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IGNORE);
}
private static VideoFrameReleaseControl createVideoFrameReleaseControl() {
@Test
public void getFrameReleaseAction_decodeOnlyFrame_returnsSkip() throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ true,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SKIP);
}
@Test
public void getFrameReleaseAction_decodeOnlyAndLastFrame_returnsReleaseImmediately()
throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ true,
/* isLastFrame= */ true,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IMMEDIATELY);
}
@Test
public void getFrameReleaseAction_decodeOnlyFrameWithoutSurface_returnsSkip() throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setOutputSurface(/* outputSurface= */ null);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 0,
/* positionUs= */ 0,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ true,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SKIP);
}
@Test
public void getFrameReleaseAction_withoutSurfaceOnTime_returnsTryAgainLater() throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setOutputSurface(/* outputSurface= */ null);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 100_000,
/* positionUs= */ 50_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
}
@Test
public void getFrameReleaseAction_withoutSurfaceShouldIgnore_returnsIgnore() throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl =
new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(),
new TestFrameTimingEvaluator(
/* shouldForceRelease= */ false,
/* shouldDropFrame= */ false,
/* shouldIgnoreFrame= */ true),
/* allowedJoiningTimeMs= */ 0);
videoFrameReleaseControl.setOutputSurface(/* outputSurface= */ null);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 100_000,
/* positionUs= */ 50_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_IGNORE);
}
@Test
public void getFrameReleaseAction_withoutSurfaceFrameLateNotStarted_returnsTryAgainLater()
throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setOutputSurface(/* outputSurface= */ null);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 100_000,
/* positionUs= */ 90_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_TRY_AGAIN_LATER);
}
@Test
public void getFrameReleaseAction_withoutSurfaceFrameLateAndStarted_returnsSkip()
throws Exception {
VideoFrameReleaseControl.FrameReleaseInfo frameReleaseInfo =
new VideoFrameReleaseControl.FrameReleaseInfo();
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ false);
VideoFrameReleaseControl videoFrameReleaseControl = createVideoFrameReleaseControl();
videoFrameReleaseControl.setOutputSurface(/* outputSurface= */ null);
videoFrameReleaseControl.setClock(clock);
videoFrameReleaseControl.onEnabled(/* releaseFirstFrameBeforeStarted= */ true);
videoFrameReleaseControl.onStarted();
assertThat(
videoFrameReleaseControl.getFrameReleaseAction(
/* presentationTimeUs= */ 100_000,
/* positionUs= */ 90_000,
/* elapsedRealtimeUs= */ 0,
/* outputStreamStartPositionUs= */ 0,
/* isDecodeOnlyFrame= */ false,
/* isLastFrame= */ false,
frameReleaseInfo))
.isEqualTo(VideoFrameReleaseControl.FRAME_RELEASE_SKIP);
}
private VideoFrameReleaseControl createVideoFrameReleaseControl() {
return createVideoFrameReleaseControl(/* allowedJoiningTimeMs= */ 0);
}
private static VideoFrameReleaseControl createVideoFrameReleaseControl(
long allowedJoiningTimeMs) {
return new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(),
new TestFrameTimingEvaluator(),
allowedJoiningTimeMs);
private VideoFrameReleaseControl createVideoFrameReleaseControl(long allowedJoiningTimeMs) {
VideoFrameReleaseControl videoFrameReleaseControl =
new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(),
new TestFrameTimingEvaluator(),
allowedJoiningTimeMs);
videoFrameReleaseControl.setOutputSurface(surface);
return videoFrameReleaseControl;
}
private static class TestFrameTimingEvaluator

View File

@ -20,10 +20,14 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import androidx.media3.common.VideoSize;
import androidx.media3.test.utils.FakeClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
@ -36,6 +40,18 @@ public class VideoFrameRenderControlTest {
private static final int VIDEO_WIDTH = 640;
private static final int VIDEO_HEIGHT = 480;
private Surface surface;
@Before
public void setUp() {
surface = new Surface(new SurfaceTexture(/* texName= */ 0));
}
@After
public void tearDown() {
surface.release();
}
@Test
public void releaseFirstFrame() throws Exception {
VideoFrameRenderControl.FrameRenderer frameRenderer =
@ -275,7 +291,7 @@ public class VideoFrameRenderControlTest {
assertThat(videoFrameRenderControl.isEnded()).isFalse();
}
private static VideoFrameReleaseControl createVideoFrameReleaseControl() {
private VideoFrameReleaseControl createVideoFrameReleaseControl() {
return createVideoFrameReleaseControl(
new TestFrameTimingEvaluator(
/* shouldForceReleaseFrames= */ false,
@ -283,12 +299,15 @@ public class VideoFrameRenderControlTest {
/* shouldIgnoreFrames= */ false));
}
private static VideoFrameReleaseControl createVideoFrameReleaseControl(
private VideoFrameReleaseControl createVideoFrameReleaseControl(
VideoFrameReleaseControl.FrameTimingEvaluator frameTimingEvaluator) {
return new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(),
frameTimingEvaluator,
/* allowedJoiningTimeMs= */ 0);
VideoFrameReleaseControl videoFrameReleaseControl =
new VideoFrameReleaseControl(
ApplicationProvider.getApplicationContext(),
frameTimingEvaluator,
/* allowedJoiningTimeMs= */ 0);
videoFrameReleaseControl.setOutputSurface(surface);
return videoFrameReleaseControl;
}
private static class TestFrameTimingEvaluator

View File

@ -716,13 +716,9 @@ public final class ExperimentalFrameExtractor {
public boolean isReady() {
// When using FrameReadingGlShaderProgram, frames will not be rendered to the output surface,
// and VideoFrameRenderControl.onFrameAvailableForRendering will not be called. The base class
// never becomes ready.
if (frameRenderedSinceLastPositionReset) {
// Treat this renderer as ready if a frame has been rendered into the effects pipeline.
// The renderer needs to become ready for ExoPlayer to enter STATE_READY.
return true;
}
return super.isReady();
// never becomes ready. Treat this renderer as ready if a frame has been rendered into the
// effects pipeline. The renderer needs to become ready for ExoPlayer to enter STATE_READY.
return frameRenderedSinceLastPositionReset;
}
@Override