diff --git a/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java b/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java index 821ff03ef2..ff6002df73 100644 --- a/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java @@ -132,8 +132,7 @@ public interface VideoFrameProcessor { /** * Called when an exception occurs during asynchronous video frame processing. * - *
If an error occurred, consuming and producing further frames will not work as expected and - * the {@link VideoFrameProcessor} should be released. + *
Using {@code VideoFrameProcessor} after an error happens is undefined behavior. */ void onError(VideoFrameProcessingException exception); diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index b45650e8db..a712a2a0b0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -690,6 +690,9 @@ public final class GlUtil { /** * Destroys the {@link EGLContext} identified by the provided {@link EGLDisplay} and {@link * EGLContext}. + * + *
This is a no-op if called on already-destroyed {@link EGLDisplay} and {@link EGLContext}
+ * instances.
*/
@RequiresApi(17)
public static void destroyEglContext(
diff --git a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java
index 05a1bd94cc..5e576a7c0b 100644
--- a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java
+++ b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java
@@ -99,7 +99,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
/** A factory for {@link DefaultVideoFrameProcessor} instances. */
public static final class Factory implements VideoFrameProcessor.Factory {
- private static final String THREAD_NAME = "Effect:GlThread";
+ private static final String THREAD_NAME = "Effect:DefaultVideoFrameProcessor:GlThread";
/** A builder for {@link DefaultVideoFrameProcessor.Factory} instances. */
public static final class Builder {
@@ -285,7 +285,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
executorService == null ? Util.newSingleThreadExecutor(THREAD_NAME) : executorService;
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor =
new VideoFrameProcessingTaskExecutor(
- instanceExecutorService, shouldShutdownExecutorService, listener);
+ instanceExecutorService, shouldShutdownExecutorService, listener::onError);
Future Using {@code VideoCompositor} after an error happens is undefined behavior.
+ */
+ void onError(VideoFrameProcessingException exception);
+ }
+
+ private static final String THREAD_NAME = "Effect:VideoCompositor:GlThread";
+ private static final String TAG = "VideoCompositor";
private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_compositor_es2.glsl";
@@ -55,17 +72,30 @@ public final class VideoCompositor {
private final Context context;
private final DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener;
private final GlObjectsProvider glObjectsProvider;
+ private final VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor;
+
// List of queues of unprocessed frames for each input source.
+ @GuardedBy("this")
private final List If a non-null {@code executorService} is set, the {@link ExecutorService} must be
+ * {@linkplain ExecutorService#shutdown shut down} by the caller.
+ */
public VideoCompositor(
Context context,
GlObjectsProvider glObjectsProvider,
+ @Nullable ExecutorService executorService,
+ ErrorListener errorListener,
DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener,
@IntRange(from = 1) int textureOutputCapacity) {
this.context = context;
@@ -75,6 +105,16 @@ public final class VideoCompositor {
inputFrameInfos = new ArrayList<>();
outputTexturePool =
new TexturePool(/* useHighPrecisionColorComponents= */ false, textureOutputCapacity);
+
+ boolean ownsExecutor = executorService == null;
+ ExecutorService instanceExecutorService =
+ ownsExecutor ? Util.newSingleThreadExecutor(THREAD_NAME) : checkNotNull(executorService);
+ videoFrameProcessingTaskExecutor =
+ new VideoFrameProcessingTaskExecutor(
+ instanceExecutorService,
+ /* shouldShutdownExecutorService= */ ownsExecutor,
+ errorListener::onError);
+ videoFrameProcessingTaskExecutor.submit(this::setupGlObjects);
}
/**
@@ -86,7 +126,6 @@ public final class VideoCompositor {
return inputFrameInfos.size() - 1;
}
- // Below methods must be called on the GL thread.
/**
* Queues an input texture to be composited, for example from an upstream {@link
* DefaultVideoFrameProcessor.TextureOutputListener}.
@@ -94,7 +133,7 @@ public final class VideoCompositor {
* Each input source must have a unique {@code inputId} returned from {@link
* #registerInputSource}.
*/
- public void queueInputTexture(
+ public synchronized void queueInputTexture(
int inputId,
GlTextureInfo inputTexture,
long presentationTimeUs,
@@ -104,14 +143,35 @@ public final class VideoCompositor {
new InputFrameInfo(inputTexture, presentationTimeUs, releaseTextureCallback);
checkNotNull(inputFrameInfos.get(inputId)).add(inputFrameInfo);
- if (isReadyToComposite()) {
- compositeToOutputTexture();
+ videoFrameProcessingTaskExecutor.submit(
+ () -> {
+ if (isReadyToComposite()) {
+ compositeToOutputTexture();
+ }
+ });
+ }
+
+ public void release() {
+ try {
+ videoFrameProcessingTaskExecutor.release(/* releaseTask= */ this::releaseGlObjects);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
}
}
- private boolean isReadyToComposite() {
+ // Below methods must be called on the GL thread.
+ private void setupGlObjects() throws GlUtil.GlException {
+ EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
+ EGLContext eglContext =
+ glObjectsProvider.createEglContext(
+ eglDisplay, /* openGlVersion= */ 2, GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888);
+ glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext, eglDisplay);
+ }
+
+ private synchronized boolean isReadyToComposite() {
// TODO: b/262694346 - Use timestamps to determine when to composite instead of number of
- // frames.
+ // frames.
for (int inputId = 0; inputId < inputFrameInfos.size(); inputId++) {
if (checkNotNull(inputFrameInfos.get(inputId)).isEmpty()) {
return false;
@@ -120,13 +180,14 @@ public final class VideoCompositor {
return true;
}
- private void compositeToOutputTexture() throws VideoFrameProcessingException {
+ private synchronized void compositeToOutputTexture() throws VideoFrameProcessingException {
List Public methods can be called from any thread.
*
- * The wrapper handles calling {@link
- * VideoFrameProcessor.Listener#onError(VideoFrameProcessingException)} for errors that occur during
- * these tasks. The listener is invoked from the {@link ExecutorService}. Errors are assumed to be
- * non-recoverable, so the {@code VideoFrameProcessingTaskExecutor} should be released if an error
- * occurs.
+ * Calls {@link ErrorListener#onError} for errors that occur during these tasks. The listener is
+ * invoked from the {@link ExecutorService}.
*
* {@linkplain #submitWithHighPriority(Task) High priority tasks} are always executed before
* {@linkplain #submit(Task) default priority tasks}. Tasks with equal priority are executed in FIFO
@@ -52,16 +48,27 @@ import java.util.concurrent.RejectedExecutionException;
* Interface for tasks that may throw a {@link GlUtil.GlException} or {@link
* VideoFrameProcessingException}.
*/
- public interface Task {
+ interface Task {
/** Runs the task. */
void run() throws VideoFrameProcessingException, GlUtil.GlException;
}
+ /** Listener for errors. */
+ interface ErrorListener {
+ /**
+ * Called when an exception occurs while executing submitted tasks.
+ *
+ * Using the {@link VideoFrameProcessingTaskExecutor} after an error happens is undefined
+ * behavior.
+ */
+ void onError(VideoFrameProcessingException exception);
+ }
+
private static final long RELEASE_WAIT_TIME_MS = 500;
private final boolean shouldShutdownExecutorService;
private final ExecutorService singleThreadExecutorService;
- private final VideoFrameProcessor.Listener listener;
+ private final ErrorListener errorListener;
private final Object lock;
@GuardedBy("lock")
@@ -74,10 +81,10 @@ import java.util.concurrent.RejectedExecutionException;
public VideoFrameProcessingTaskExecutor(
ExecutorService singleThreadExecutorService,
boolean shouldShutdownExecutorService,
- VideoFrameProcessor.Listener listener) {
+ ErrorListener errorListener) {
this.singleThreadExecutorService = singleThreadExecutorService;
this.shouldShutdownExecutorService = shouldShutdownExecutorService;
- this.listener = listener;
+ this.errorListener = errorListener;
lock = new Object();
highPriorityTasks = new ArrayDeque<>();
}
@@ -186,7 +193,7 @@ import java.util.concurrent.RejectedExecutionException;
if (shouldShutdownExecutorService) {
singleThreadExecutorService.shutdown();
if (!singleThreadExecutorService.awaitTermination(RELEASE_WAIT_TIME_MS, MILLISECONDS)) {
- listener.onError(
+ errorListener.onError(
new VideoFrameProcessingException(
"Release timed out. OpenGL resources may not be cleaned up properly."));
}
@@ -231,6 +238,6 @@ import java.util.concurrent.RejectedExecutionException;
}
shouldCancelTasks = true;
}
- listener.onError(VideoFrameProcessingException.from(exception));
+ errorListener.onError(VideoFrameProcessingException.from(exception));
}
}
diff --git a/libraries/effect/src/test/java/androidx/media3/effect/ChainingGlShaderProgramListenerTest.java b/libraries/effect/src/test/java/androidx/media3/effect/ChainingGlShaderProgramListenerTest.java
index a7bd7688a7..31f81a709e 100644
--- a/libraries/effect/src/test/java/androidx/media3/effect/ChainingGlShaderProgramListenerTest.java
+++ b/libraries/effect/src/test/java/androidx/media3/effect/ChainingGlShaderProgramListenerTest.java
@@ -21,7 +21,6 @@ import static org.mockito.Mockito.verify;
import androidx.media3.common.C;
import androidx.media3.common.GlObjectsProvider;
import androidx.media3.common.GlTextureInfo;
-import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.util.Util;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
@@ -33,13 +32,13 @@ import org.junit.runner.RunWith;
public final class ChainingGlShaderProgramListenerTest {
private static final long EXECUTOR_WAIT_TIME_MS = 100;
- private final VideoFrameProcessor.Listener mockFrameProcessorListener =
- mock(VideoFrameProcessor.Listener.class);
+ private final VideoFrameProcessingTaskExecutor.ErrorListener mockErrorListener =
+ mock(VideoFrameProcessingTaskExecutor.ErrorListener.class);
private final VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor =
new VideoFrameProcessingTaskExecutor(
Util.newSingleThreadExecutor("Test"),
/* shouldShutdownExecutorService= */ true,
- mockFrameProcessorListener);
+ mockErrorListener);
private final GlObjectsProvider mockGlObjectsProvider = mock(GlObjectsProvider.class);
private final GlShaderProgram mockProducingGlShaderProgram = mock(GlShaderProgram.class);
private final GlShaderProgram mockConsumingGlShaderProgram = mock(GlShaderProgram.class);
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java
index b1d10d4191..79b232a9f1 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java
@@ -242,7 +242,7 @@ public final class VideoFrameProcessorTestRunner {
* Time to wait for the decoded frame to populate the {@link VideoFrameProcessor} instance's input
* surface and the {@link VideoFrameProcessor} to finish processing the frame, in milliseconds.
*/
- public static final int VIDEO_FRAME_PROCESSING_WAIT_MS = 5000;
+ public static final int VIDEO_FRAME_PROCESSING_WAIT_MS = 5_000;
private final String testId;
private final @MonotonicNonNull String videoAssetPath;
diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java
index 5a70007c2b..0508b38731 100644
--- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java
+++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java
@@ -557,11 +557,11 @@ public final class AndroidTestUtil {
*/
public static EGLContext createOpenGlObjects() throws GlUtil.GlException {
EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
- int[] configAttributes = GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888;
GlObjectsProvider glObjectsProvider =
new DefaultGlObjectsProvider(/* sharedEglContext= */ null);
EGLContext eglContext =
- glObjectsProvider.createEglContext(eglDisplay, /* openGlVersion= */ 2, configAttributes);
+ glObjectsProvider.createEglContext(
+ eglDisplay, /* openGlVersion= */ 2, GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888);
glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext, eglDisplay);
return eglContext;
}
diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TextureBitmapReader.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TextureBitmapReader.java
index 6eed5d3617..59b7c55972 100644
--- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TextureBitmapReader.java
+++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TextureBitmapReader.java
@@ -87,7 +87,7 @@ public final class TextureBitmapReader implements VideoFrameProcessorTestRunner.
GlTextureInfo outputTexture,
long presentationTimeUs,
DefaultVideoFrameProcessor.ReleaseOutputTextureCallback releaseOutputTextureCallback)
- throws VideoFrameProcessingException, GlUtil.GlException {
+ throws VideoFrameProcessingException {
readBitmap(outputTexture, presentationTimeUs);
releaseOutputTextureCallback.release(presentationTimeUs);
}
diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/VideoCompositorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/VideoCompositorPixelTest.java
index 71b0473f38..e749b1eeb2 100644
--- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/VideoCompositorPixelTest.java
+++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/VideoCompositorPixelTest.java
@@ -43,6 +43,10 @@ import androidx.media3.test.utils.BitmapPixelTestUtil;
import androidx.media3.test.utils.VideoFrameProcessorTestRunner;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -55,8 +59,6 @@ import org.junit.runners.Parameterized;
/** Pixel test for {@link VideoCompositor} compositing 2 input frames into 1 output frame. */
@RunWith(Parameterized.class)
public final class VideoCompositorPixelTest {
- private @MonotonicNonNull VideoFrameProcessorTestRunner inputVfpTestRunner1;
- private @MonotonicNonNull VideoFrameProcessorTestRunner inputVfpTestRunner2;
private static final String ORIGINAL_PNG_ASSET_PATH = "media/bitmap/input_images/media3test.png";
private static final String GRAYSCALE_PNG_ASSET_PATH =
@@ -66,10 +68,6 @@ public final class VideoCompositorPixelTest {
private static final String GRAYSCALE_AND_ROTATE180_COMPOSITE_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/electrical_colors/grayscaleAndRotate180Composite.png";
- private static final Effect ROTATE_180 =
- new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build();
- private static final Effect GRAYSCALE = RgbFilter.createGrayscaleFilter();
-
@Parameterized.Parameters(name = "useSharedExecutor={0}")
public static ImmutableList Composites input bitmaps from two input sources.
+ */
+ private static final class VideoCompositorTestRunner {
+ private static final int COMPOSITOR_TIMEOUT_MS = 5_000;
+ private static final Effect ROTATE_180_EFFECT =
+ new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build();
+ private static final Effect GRAYSCALE_EFFECT = RgbFilter.createGrayscaleFilter();
+
+ public final TextureBitmapReader inputBitmapReader1;
+ public final TextureBitmapReader inputBitmapReader2;
+ private final VideoFrameProcessorTestRunner inputVideoFrameProcessorTestRunner1;
+ private final VideoFrameProcessorTestRunner inputVideoFrameProcessorTestRunner2;
+ private final VideoCompositor videoCompositor;
+ private final @Nullable ExecutorService sharedExecutorService;
+ private final AtomicReference