Fix and refactor logic to remove all frames in ExternalTextureManager

The existing logic was not working sometimes because:

1. The repeated scheduling in releaseAllFramesFromMediaCodec was
starving the thread on which the SurfaceTexture frameAvailableListener
was called.

2. The case where a pending frame arrives on the surface after flush
finishes executing was not handled.

The consequence of both problems is that availableFrameCount ended up
being > pendingFrames.size().

PiperOrigin-RevId: 668916256
This commit is contained in:
kimvde 2024-08-29 06:35:14 -07:00 committed by Copybara-Service
parent e30161656e
commit 37561c829f

View File

@ -29,7 +29,6 @@ import static java.lang.Math.abs;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.opengl.GLES31;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -39,7 +38,6 @@ import androidx.media3.common.GlTextureInfo;
import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@ -81,9 +79,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// LINT.IfChange(SURFACE_TEXTURE_TIMEOUT_MS) // LINT.IfChange(SURFACE_TEXTURE_TIMEOUT_MS)
private static final long SURFACE_TEXTURE_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 500; private static final long SURFACE_TEXTURE_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 500;
// Wait delay between checking whether a registered frame arrives on the SurfaceTexture.
private static final long SURFACE_TEXTURE_WAIT_DELAY_MS = 10;
private final GlObjectsProvider glObjectsProvider; private final GlObjectsProvider glObjectsProvider;
private @MonotonicNonNull ExternalShaderProgram externalShaderProgram; private @MonotonicNonNull ExternalShaderProgram externalShaderProgram;
private final int externalTexId; private final int externalTexId;
@ -106,9 +101,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable private Future<?> forceSignalEndOfStreamFuture; @Nullable private Future<?> forceSignalEndOfStreamFuture;
private boolean shouldRejectIncomingFrames; private boolean shouldRejectIncomingFrames;
// The first time trying to remove all frames from MediaCodec, used to escape repeated waiting for @Nullable private CountDownLatch releaseAllFramesLatch;
// a frame to arrive on the SurfaceTexture.
private long firstTryToRemoveAllFramesTimeMs;
@Nullable private volatile RuntimeException releaseAllFramesException; @Nullable private volatile RuntimeException releaseAllFramesException;
@ -157,9 +150,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_SURFACE_TEXTURE_INPUT, C.TIME_UNSET); DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_SURFACE_TEXTURE_INPUT, C.TIME_UNSET);
if (shouldRejectIncomingFrames) { if (shouldRejectIncomingFrames) {
surfaceTexture.updateTexImage(); surfaceTexture.updateTexImage();
pendingFrames.poll();
if (releaseAllFramesLatch != null && pendingFrames.isEmpty()) {
releaseAllFramesLatch.countDown();
}
Log.w( Log.w(
TAG, TAG,
"Dropping frame received on SurfaceTexture after forcing EOS: " "Dropping frame received on SurfaceTexture: "
+ surfaceTexture.getTimestamp() / 1000); + surfaceTexture.getTimestamp() / 1000);
return; return;
} }
@ -171,22 +168,36 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
maybeQueueFrameToExternalShaderProgram(); maybeQueueFrameToExternalShaderProgram();
})); }));
surface = new Surface(surfaceTexture); surface = new Surface(surfaceTexture);
firstTryToRemoveAllFramesTimeMs = C.TIME_UNSET;
} }
@Override @Override
public void releaseAllRegisteredFrames() { public void releaseAllRegisteredFrames() {
// Blocks the calling thread until all the registered frames are received and released. // Blocks the calling thread until all the registered frames are received and released.
CountDownLatch countDownLatch = new CountDownLatch(1); CountDownLatch releaseAllFramesLatch = new CountDownLatch(1);
videoFrameProcessingTaskExecutor.submit(() -> releaseAllFramesFromMediaCodec(countDownLatch)); this.releaseAllFramesLatch = releaseAllFramesLatch;
videoFrameProcessingTaskExecutor.submit(
() -> {
try {
removeAllSurfaceTextureFrames();
} catch (RuntimeException e) {
releaseAllFramesException = e;
Log.e(TAG, "Failed to remove texture frames", e);
if (this.releaseAllFramesLatch != null) {
this.releaseAllFramesLatch.countDown();
}
}
});
try { try {
countDownLatch.await(); if (!releaseAllFramesLatch.await(SURFACE_TEXTURE_TIMEOUT_MS, MILLISECONDS)) {
Log.w(TAG, "Timeout reached while waiting for latch to be unblocked.");
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Not re-thrown to not crash frame processing. Frame process can likely continue even when // Not re-thrown to not crash frame processing. Frame process can likely continue even when
// not all rendered frames arrive. // not all rendered frames arrive.
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
Log.w(TAG, "Interrupted when waiting for MediaCodec frames to arrive."); Log.w(TAG, "Interrupted when waiting for MediaCodec frames to arrive.");
} }
this.releaseAllFramesLatch = null;
if (releaseAllFramesException != null) { if (releaseAllFramesException != null) {
throw releaseAllFramesException; throw releaseAllFramesException;
} }
@ -334,7 +345,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void forceSignalEndOfStream() { private void forceSignalEndOfStream() {
// Reset because there could be further input streams after the current one ends.
Log.w( Log.w(
TAG, TAG,
Util.formatInvariant( Util.formatInvariant(
@ -343,7 +353,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Reset because there could be further input streams after the current one ends. // Reset because there could be further input streams after the current one ends.
currentInputStreamEnded = false; currentInputStreamEnded = false;
currentFrame = null; currentFrame = null;
shouldRejectIncomingFrames = true;
// Frames could be made available while waiting for OpenGL to finish processing. That is, // Frames could be made available while waiting for OpenGL to finish processing. That is,
// time out is triggered while waiting for the downstream shader programs to process a frame, // time out is triggered while waiting for the downstream shader programs to process a frame,
@ -354,43 +363,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
signalEndOfCurrentInputStream(); signalEndOfCurrentInputStream();
} }
private void releaseAllFramesFromMediaCodec(CountDownLatch latch) {
try {
removeAllSurfaceTextureFrames();
} catch (RuntimeException e) {
releaseAllFramesException = e;
latch.countDown();
}
if (pendingFrames.isEmpty()
// Assumes a frame that is registered would not take longer than SURFACE_TEXTURE_TIMEOUT_MS
// to arrive, otherwise unblock the waiting thread.
|| (firstTryToRemoveAllFramesTimeMs != C.TIME_UNSET
&& SystemClock.DEFAULT.currentTimeMillis() - firstTryToRemoveAllFramesTimeMs
>= SURFACE_TEXTURE_TIMEOUT_MS)) {
firstTryToRemoveAllFramesTimeMs = C.TIME_UNSET;
latch.countDown();
return;
}
if (firstTryToRemoveAllFramesTimeMs == C.TIME_UNSET) {
firstTryToRemoveAllFramesTimeMs = SystemClock.DEFAULT.currentTimeMillis();
}
Future<?> unusedFuture =
scheduledExecutorService.schedule(
() ->
videoFrameProcessingTaskExecutor.submit(
() -> releaseAllFramesFromMediaCodec(latch)),
SURFACE_TEXTURE_WAIT_DELAY_MS,
MILLISECONDS);
}
private void removeAllSurfaceTextureFrames() { private void removeAllSurfaceTextureFrames() {
shouldRejectIncomingFrames = true;
while (availableFrameCount > 0) { while (availableFrameCount > 0) {
availableFrameCount--; availableFrameCount--;
surfaceTexture.updateTexImage(); surfaceTexture.updateTexImage();
pendingFrames.remove(); pendingFrames.remove();
} }
if (releaseAllFramesLatch != null && pendingFrames.isEmpty()) {
releaseAllFramesLatch.countDown();
}
} }
private void maybeQueueFrameToExternalShaderProgram() { private void maybeQueueFrameToExternalShaderProgram() {