diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java
index 57370ff761..2c0b38fbf9 100644
--- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java
@@ -17,6 +17,7 @@ package androidx.media3.demo.transformer;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static androidx.media3.common.util.Assertions.checkNotNull;
+import static androidx.media3.common.util.Assertions.checkState;
import android.app.Activity;
import android.content.Context;
@@ -420,9 +421,28 @@ public final class TransformerActivity extends AppCompatActivity {
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
+ private @MonotonicNonNull SurfaceView surfaceView;
+ private int width;
+ private int height;
+
+ public DemoDebugViewProvider() {
+ width = C.LENGTH_UNSET;
+ height = C.LENGTH_UNSET;
+ }
+
@Nullable
@Override
public SurfaceView getDebugPreviewSurfaceView(int width, int height) {
+ checkState(
+ surfaceView == null || (this.width == width && this.height == height),
+ "Transformer should not change the output size mid-transformation.");
+ if (surfaceView != null) {
+ return surfaceView;
+ }
+
+ this.width = width;
+ this.height = height;
+
// Update the UI on the main thread and wait for the output surface to be available.
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
@@ -459,6 +479,7 @@ public final class TransformerActivity extends AppCompatActivity {
Thread.currentThread().interrupt();
return null;
}
+ this.surfaceView = surfaceView;
return surfaceView;
}
}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java
new file mode 100644
index 0000000000..e582af3309
--- /dev/null
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2022 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.transformer;
+
+import static androidx.media3.common.util.Assertions.checkState;
+
+import android.content.Context;
+import android.opengl.EGL14;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLExt;
+import android.opengl.EGLSurface;
+import android.util.Size;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.media3.common.C;
+import androidx.media3.common.util.GlUtil;
+import androidx.media3.common.util.Log;
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * Wrapper around a {@link GlTextureProcessor} that writes to the provided output surface and
+ * optional debug surface view.
+ *
+ *
The wrapped {@link GlTextureProcessor} applies the {@link GlMatrixTransformation} instances
+ * passed to the constructor, followed by any transformations needed to convert the frames to the
+ * dimensions specified by the provided {@link SurfaceInfo}.
+ *
+ *
This wrapper is used for the final {@link GlTextureProcessor} instance in the chain of {@link
+ * GlTextureProcessor} instances used by {@link FrameProcessorChain}.
+ */
+/* package */ final class FinalMatrixTransformationProcessorWrapper implements GlTextureProcessor {
+
+ private static final String TAG = "FinalProcessorWrapper";
+
+ private final Context context;
+ private final ImmutableList matrixTransformations;
+ private final EGLDisplay eglDisplay;
+ private final EGLContext eglContext;
+ private final SurfaceInfo.Provider outputSurfaceProvider;
+ private final long streamOffsetUs;
+ private final Transformer.DebugViewProvider debugViewProvider;
+ private final FrameProcessorChain.Listener frameProcessorChainListener;
+ private final boolean enableExperimentalHdrEditing;
+
+ private int inputWidth;
+ private int inputHeight;
+ @Nullable private MatrixTransformationProcessor matrixTransformationProcessor;
+ @Nullable private SurfaceInfo outputSurfaceInfo;
+ @Nullable private EGLSurface outputEglSurface;
+ @Nullable private SurfaceViewWrapper debugSurfaceViewWrapper;
+ private @MonotonicNonNull Listener listener;
+
+ public FinalMatrixTransformationProcessorWrapper(
+ Context context,
+ EGLDisplay eglDisplay,
+ EGLContext eglContext,
+ ImmutableList matrixTransformations,
+ SurfaceInfo.Provider outputSurfaceProvider,
+ long streamOffsetUs,
+ FrameProcessorChain.Listener listener,
+ Transformer.DebugViewProvider debugViewProvider,
+ boolean enableExperimentalHdrEditing) {
+ this.context = context;
+ this.matrixTransformations = matrixTransformations;
+ this.eglDisplay = eglDisplay;
+ this.eglContext = eglContext;
+ this.outputSurfaceProvider = outputSurfaceProvider;
+ this.streamOffsetUs = streamOffsetUs;
+ this.debugViewProvider = debugViewProvider;
+ this.frameProcessorChainListener = listener;
+ this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The {@code FinalMatrixTransformationProcessorWrapper} will only call {@link
+ * Listener#onInputFrameProcessed(TextureInfo)}. Other events are handled via the {@link
+ * FrameProcessorChain.Listener} passed to the constructor.
+ */
+ @Override
+ public void setListener(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
+ try {
+ if (!ensureConfigured(inputTexture.width, inputTexture.height)) {
+ return false;
+ }
+
+ EGLSurface outputEglSurface = this.outputEglSurface;
+ SurfaceInfo outputSurfaceInfo = this.outputSurfaceInfo;
+ MatrixTransformationProcessor matrixTransformationProcessor =
+ this.matrixTransformationProcessor;
+
+ GlUtil.focusEglSurface(
+ eglDisplay,
+ eglContext,
+ outputEglSurface,
+ outputSurfaceInfo.width,
+ outputSurfaceInfo.height);
+ GlUtil.clearOutputFrame();
+ matrixTransformationProcessor.drawFrame(inputTexture.texId, presentationTimeUs);
+ EGLExt.eglPresentationTimeANDROID(
+ eglDisplay,
+ outputEglSurface,
+ /* presentationTimeNs= */ (presentationTimeUs + streamOffsetUs) * 1000);
+ EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
+ } catch (FrameProcessingException | GlUtil.GlException e) {
+ frameProcessorChainListener.onFrameProcessingError(
+ FrameProcessingException.from(e, presentationTimeUs));
+ }
+
+ if (debugSurfaceViewWrapper != null && matrixTransformationProcessor != null) {
+ MatrixTransformationProcessor matrixTransformationProcessor =
+ this.matrixTransformationProcessor;
+ try {
+ debugSurfaceViewWrapper.maybeRenderToSurfaceView(
+ () -> {
+ GlUtil.clearOutputFrame();
+ matrixTransformationProcessor.drawFrame(inputTexture.texId, presentationTimeUs);
+ });
+ } catch (FrameProcessingException | GlUtil.GlException e) {
+ Log.d(TAG, "Error rendering to debug preview", e);
+ }
+ }
+ if (listener != null) {
+ listener.onInputFrameProcessed(inputTexture);
+ }
+ return true;
+ }
+
+ @EnsuresNonNullIf(
+ expression = {"outputSurfaceInfo", "outputEglSurface", "matrixTransformationProcessor"},
+ result = true)
+ private boolean ensureConfigured(int inputWidth, int inputHeight)
+ throws FrameProcessingException, GlUtil.GlException {
+ if (inputWidth == this.inputWidth
+ && inputHeight == this.inputHeight
+ && outputSurfaceInfo != null
+ && outputEglSurface != null
+ && matrixTransformationProcessor != null) {
+ return true;
+ }
+
+ this.inputWidth = inputWidth;
+ this.inputHeight = inputHeight;
+ Size requestedOutputSize =
+ MatrixUtils.configureAndGetOutputSize(inputWidth, inputHeight, matrixTransformations);
+ @Nullable
+ SurfaceInfo outputSurfaceInfo =
+ outputSurfaceProvider.getSurfaceInfo(
+ requestedOutputSize.getWidth(), requestedOutputSize.getHeight());
+ if (outputSurfaceInfo == null) {
+ if (matrixTransformationProcessor != null) {
+ matrixTransformationProcessor.release();
+ matrixTransformationProcessor = null;
+ }
+ outputEglSurface = null;
+ return false;
+ }
+ if (outputSurfaceInfo == this.outputSurfaceInfo
+ && outputEglSurface != null
+ && matrixTransformationProcessor != null) {
+ return true;
+ }
+
+ EGLSurface outputEglSurface;
+ if (enableExperimentalHdrEditing) {
+ // TODO(b/227624622): Don't assume BT.2020 PQ input/output.
+ outputEglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurfaceInfo.surface);
+ } else {
+ outputEglSurface = GlUtil.getEglSurface(eglDisplay, outputSurfaceInfo.surface);
+ }
+
+ @Nullable
+ SurfaceView debugSurfaceView =
+ debugViewProvider.getDebugPreviewSurfaceView(
+ outputSurfaceInfo.width, outputSurfaceInfo.height);
+ if (debugSurfaceView != null) {
+ debugSurfaceViewWrapper =
+ new SurfaceViewWrapper(
+ eglDisplay, eglContext, enableExperimentalHdrEditing, debugSurfaceView);
+ }
+
+ matrixTransformationProcessor =
+ createMatrixTransformationProcessorForOutputSurface(requestedOutputSize, outputSurfaceInfo);
+
+ this.outputSurfaceInfo = outputSurfaceInfo;
+ this.outputEglSurface = outputEglSurface;
+ return true;
+ }
+
+ private MatrixTransformationProcessor createMatrixTransformationProcessorForOutputSurface(
+ Size requestedOutputSize, SurfaceInfo outputSurfaceInfo) throws FrameProcessingException {
+ ImmutableList.Builder matrixTransformationListBuilder =
+ new ImmutableList.Builder().addAll(matrixTransformations);
+ if (outputSurfaceInfo.orientationDegrees != 0) {
+ matrixTransformationListBuilder.add(
+ new ScaleToFitTransformation.Builder()
+ .setRotationDegrees(outputSurfaceInfo.orientationDegrees)
+ .build());
+ }
+ if (outputSurfaceInfo.width != requestedOutputSize.getWidth()
+ || outputSurfaceInfo.height != requestedOutputSize.getHeight()) {
+ matrixTransformationListBuilder.add(
+ Presentation.createForWidthAndHeight(
+ outputSurfaceInfo.width, outputSurfaceInfo.height, Presentation.LAYOUT_SCALE_TO_FIT));
+ }
+
+ MatrixTransformationProcessor matrixTransformationProcessor =
+ new MatrixTransformationProcessor(context, matrixTransformationListBuilder.build());
+ Size outputSize = matrixTransformationProcessor.configure(inputWidth, inputHeight);
+ checkState(outputSize.getWidth() == outputSurfaceInfo.width);
+ checkState(outputSize.getHeight() == outputSurfaceInfo.height);
+ return matrixTransformationProcessor;
+ }
+
+ @Override
+ public void releaseOutputFrame(TextureInfo outputTexture) {
+ throw new UnsupportedOperationException(
+ "The final texture processor writes to a surface so there is no texture to release");
+ }
+
+ @Override
+ public void signalEndOfInputStream() {
+ frameProcessorChainListener.onFrameProcessingEnded();
+ }
+
+ @Override
+ @WorkerThread
+ public void release() throws FrameProcessingException {
+ if (matrixTransformationProcessor != null) {
+ matrixTransformationProcessor.release();
+ }
+ }
+
+ /**
+ * Wrapper around a {@link SurfaceView} that keeps track of whether the output surface is valid,
+ * and makes rendering a no-op if not.
+ */
+ private static final class SurfaceViewWrapper implements SurfaceHolder.Callback {
+ private final EGLDisplay eglDisplay;
+ private final EGLContext eglContext;
+ private final boolean enableExperimentalHdrEditing;
+
+ @GuardedBy("this")
+ @Nullable
+ private Surface surface;
+
+ @GuardedBy("this")
+ @Nullable
+ private EGLSurface eglSurface;
+
+ private int width;
+ private int height;
+
+ public SurfaceViewWrapper(
+ EGLDisplay eglDisplay,
+ EGLContext eglContext,
+ boolean enableExperimentalHdrEditing,
+ SurfaceView surfaceView) {
+ this.eglDisplay = eglDisplay;
+ this.eglContext = eglContext;
+ this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
+ surfaceView.getHolder().addCallback(this);
+ surface = surfaceView.getHolder().getSurface();
+ width = surfaceView.getWidth();
+ height = surfaceView.getHeight();
+ }
+
+ /**
+ * Focuses the wrapped surface view's surface as an {@link EGLSurface}, renders using {@code
+ * renderingTask} and swaps buffers, if the view's holder has a valid surface. Does nothing
+ * otherwise.
+ */
+ @WorkerThread
+ public synchronized void maybeRenderToSurfaceView(FrameProcessingTask renderingTask)
+ throws GlUtil.GlException, FrameProcessingException {
+ if (surface == null) {
+ return;
+ }
+
+ if (eglSurface == null) {
+ if (enableExperimentalHdrEditing) {
+ eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, surface);
+ } else {
+ eglSurface = GlUtil.getEglSurface(eglDisplay, surface);
+ }
+ }
+ EGLSurface eglSurface = this.eglSurface;
+ GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height);
+ renderingTask.run();
+ EGL14.eglSwapBuffers(eglDisplay, eglSurface);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {}
+
+ @Override
+ public synchronized void surfaceChanged(
+ SurfaceHolder holder, int format, int width, int height) {
+ this.width = width;
+ this.height = height;
+ Surface newSurface = holder.getSurface();
+ if (surface == null || !surface.equals(newSurface)) {
+ surface = newSurface;
+ eglSurface = null;
+ }
+ }
+
+ @Override
+ public synchronized void surfaceDestroyed(SurfaceHolder holder) {
+ surface = null;
+ eglSurface = null;
+ width = C.LENGTH_UNSET;
+ height = C.LENGTH_UNSET;
+ }
+ }
+}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java
index 8476dadabb..3ebc57741d 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java
@@ -17,35 +17,24 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
-import static com.google.common.collect.Iterables.getLast;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
-import android.opengl.EGLExt;
-import android.opengl.EGLSurface;
-import android.util.Size;
import android.view.Surface;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.media3.common.C;
import androidx.media3.common.util.GlUtil;
-import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
-import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* {@code FrameProcessorChain} applies changes to individual video frames.
@@ -53,10 +42,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously
* and is processed on a background thread as it becomes available. All input frames should be
* {@linkplain #registerInputFrame() registered} before they are rendered to the input surface.
- * {@link #getPendingFrameCount()} can be used to check whether there are frames that have not been
- * fully processed yet. Output is written to the provided {@linkplain #create(Context, Listener,
- * float, int, int, long, List, SurfaceInfo.Provider, Transformer.DebugViewProvider, boolean) output
- * surface}.
+ * {@link #getPendingInputFrameCount()} can be used to check whether there are frames that have not
+ * been fully processed yet. Output is written to the provided {@linkplain #create(Context,
+ * Listener, float, int, int, long, List, SurfaceInfo.Provider, Transformer.DebugViewProvider,
+ * boolean) output surface}.
*/
// TODO(b/227625423): Factor out FrameProcessor interface and rename this class to GlFrameProcessor.
/* package */ final class FrameProcessorChain {
@@ -93,17 +82,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Surface}.
* @param debugViewProvider A {@link Transformer.DebugViewProvider}.
* @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal.
- * @return A new instance or {@code null}, if no output surface was provided.
+ * @return A new instance.
* @throws FrameProcessingException If reading shader files fails, or an OpenGL error occurs while
* creating and configuring the OpenGL components.
*/
- // TODO(b/227625423): Remove @Nullable here and allow the output surface to be @Nullable until
- // the output surface is requested when the output size becomes available asynchronously
- // via the final GlTextureProcessor.
- @Nullable
public static FrameProcessorChain create(
Context context,
- Listener listener,
+ FrameProcessorChain.Listener listener,
float pixelWidthHeightRatio,
int inputWidth,
int inputHeight,
@@ -118,25 +103,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
- Future> frameProcessorChainFuture =
+ Future frameProcessorChainFuture =
singleThreadExecutorService.submit(
() ->
- Optional.fromNullable(
- createOpenGlObjectsAndFrameProcessorChain(
- context,
- listener,
- pixelWidthHeightRatio,
- inputWidth,
- inputHeight,
- streamOffsetUs,
- effects,
- outputSurfaceProvider,
- debugViewProvider,
- enableExperimentalHdrEditing,
- singleThreadExecutorService)));
+ createOpenGlObjectsAndFrameProcessorChain(
+ context,
+ listener,
+ pixelWidthHeightRatio,
+ inputWidth,
+ inputHeight,
+ streamOffsetUs,
+ effects,
+ outputSurfaceProvider,
+ debugViewProvider,
+ enableExperimentalHdrEditing,
+ singleThreadExecutorService));
try {
- return frameProcessorChainFuture.get().orNull();
+ return frameProcessorChainFuture.get();
} catch (ExecutionException e) {
throw new FrameProcessingException(e);
} catch (InterruptedException e) {
@@ -146,18 +130,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
/**
- * Creates the OpenGL context, surfaces, textures, and framebuffers, initializes the {@link
- * SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} corresponding to the {@link
- * GlEffect GlEffects}, and returns a new {@code FrameProcessorChain}.
+ * Creates the OpenGL context, surfaces, textures, and framebuffers, initializes {@link
+ * GlTextureProcessor} instances corresponding to the {@link GlEffect} instances, and returns a
+ * new {@code FrameProcessorChain}.
*
- * This method must be executed using the {@code singleThreadExecutorService}, as all later
- * OpenGL commands will be called on that thread.
+ *
This method must be executed using the {@code singleThreadExecutorService}, as later OpenGL
+ * commands will be called on that thread.
*/
@WorkerThread
- @Nullable
private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain(
Context context,
- Listener listener,
+ FrameProcessorChain.Listener listener,
float pixelWidthHeightRatio,
int inputWidth,
int inputHeight,
@@ -188,132 +171,151 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ImmutableList.Builder matrixTransformationListBuilder =
new ImmutableList.Builder<>();
- // Scale to expand the frame to apply the pixelWidthHeightRatio.
- if (pixelWidthHeightRatio > 1f) {
+ if (pixelWidthHeightRatio != 1f) {
matrixTransformationListBuilder.add(
- new ScaleToFitTransformation.Builder()
- .setScale(/* scaleX= */ pixelWidthHeightRatio, /* scaleY= */ 1f)
- .build());
- } else if (pixelWidthHeightRatio < 1f) {
- matrixTransformationListBuilder.add(
- new ScaleToFitTransformation.Builder()
- .setScale(/* scaleX= */ 1f, /* scaleY= */ 1f / pixelWidthHeightRatio)
- .build());
+ createPixelWidthHeightRatioTransformation(pixelWidthHeightRatio));
}
+ ImmutableList textureProcessors =
+ getGlTextureProcessorsForGlEffects(
+ context,
+ effects,
+ eglDisplay,
+ eglContext,
+ matrixTransformationListBuilder,
+ outputSurfaceProvider,
+ streamOffsetUs,
+ listener,
+ debugViewProvider,
+ enableExperimentalHdrEditing);
+
ExternalTextureProcessor externalTextureProcessor =
new ExternalTextureProcessor(context, enableExperimentalHdrEditing);
- int inputExternalTexId = GlUtil.createExternalTexture();
- Size outputSize = externalTextureProcessor.configure(inputWidth, inputHeight);
- ImmutableList.Builder intermediateTextures = new ImmutableList.Builder<>();
- ImmutableList.Builder textureProcessors =
- new ImmutableList.Builder().add(externalTextureProcessor);
+ FrameProcessingTaskExecutor frameProcessingTaskExecutor =
+ new FrameProcessingTaskExecutor(singleThreadExecutorService, listener);
+ chainTextureProcessorsWithListeners(
+ externalTextureProcessor, textureProcessors, frameProcessingTaskExecutor, listener);
- // Combine consecutive GlMatrixTransformations into a single SingleFrameGlTextureProcessor and
- // convert all other GlEffects to SingleFrameGlTextureProcessors.
+ return new FrameProcessorChain(
+ eglDisplay,
+ eglContext,
+ frameProcessingTaskExecutor,
+ streamOffsetUs,
+ /* inputExternalTexture= */ new TextureInfo(
+ GlUtil.createExternalTexture(), /* fboId= */ C.INDEX_UNSET, inputWidth, inputHeight),
+ externalTextureProcessor,
+ textureProcessors);
+ }
+
+ /**
+ * Returns a new {@link GlMatrixTransformation} to expand or shrink the frame based on the {@code
+ * pixelWidthHeightRatio}.
+ *
+ * If {@code pixelWidthHeightRatio} is 1, this method returns an identity transformation that
+ * can be ignored.
+ */
+ private static GlMatrixTransformation createPixelWidthHeightRatioTransformation(
+ float pixelWidthHeightRatio) {
+ if (pixelWidthHeightRatio > 1f) {
+ return new ScaleToFitTransformation.Builder()
+ .setScale(/* scaleX= */ pixelWidthHeightRatio, /* scaleY= */ 1f)
+ .build();
+ } else {
+ return new ScaleToFitTransformation.Builder()
+ .setScale(/* scaleX= */ 1f, /* scaleY= */ 1f / pixelWidthHeightRatio)
+ .build();
+ }
+ }
+
+ /**
+ * Combines consecutive {@link GlMatrixTransformation} instances into a single {@link
+ * MatrixTransformationProcessor} and converts all other {@link GlEffect} instances to separate
+ * {@link GlTextureProcessor} instances.
+ *
+ *
The final {@link GlTextureProcessor} is wrapped in a {@link
+ * FinalMatrixTransformationProcessorWrapper} so that it can write directly to the {@linkplain
+ * SurfaceInfo.Provider provided output surface}.
+ */
+ private static ImmutableList getGlTextureProcessorsForGlEffects(
+ Context context,
+ List effects,
+ EGLDisplay eglDisplay,
+ EGLContext eglContext,
+ ImmutableList.Builder matrixTransformationListBuilder,
+ SurfaceInfo.Provider outputSurfaceProvider,
+ long streamOffsetUs,
+ FrameProcessorChain.Listener listener,
+ Transformer.DebugViewProvider debugViewProvider,
+ boolean enableExperimentalHdrEditing)
+ throws FrameProcessingException {
+ ImmutableList.Builder textureProcessorListBuilder =
+ new ImmutableList.Builder<>();
for (int i = 0; i < effects.size(); i++) {
GlEffect effect = effects.get(i);
if (effect instanceof GlMatrixTransformation) {
matrixTransformationListBuilder.add((GlMatrixTransformation) effect);
continue;
}
-
ImmutableList matrixTransformations =
matrixTransformationListBuilder.build();
if (!matrixTransformations.isEmpty()) {
- MatrixTransformationProcessor matrixTransformationProcessor =
- new MatrixTransformationProcessor(context, matrixTransformations);
- intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
- outputSize =
- matrixTransformationProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
- textureProcessors.add(matrixTransformationProcessor);
+ textureProcessorListBuilder.add(
+ new MatrixTransformationProcessor(context, matrixTransformations));
matrixTransformationListBuilder = new ImmutableList.Builder<>();
}
- intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
- SingleFrameGlTextureProcessor textureProcessor = effect.toGlTextureProcessor(context);
- outputSize = textureProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
- textureProcessors.add(textureProcessor);
+ textureProcessorListBuilder.add(effect.toGlTextureProcessor(context));
}
-
- // TODO(b/227625423): Request the output surface during processing when the output size becomes
- // available asynchronously via the final GlTextureProcessor instead of requesting it here.
- // This will also avoid needing to return null here when no surface is provided.
- Size requestedOutputSize =
- MatrixUtils.configureAndGetOutputSize(
- outputSize.getWidth(), outputSize.getHeight(), matrixTransformationListBuilder.build());
- @Nullable
- SurfaceInfo outputSurfaceInfo =
- outputSurfaceProvider.getSurfaceInfo(
- requestedOutputSize.getWidth(), requestedOutputSize.getHeight());
- if (outputSurfaceInfo == null) {
- Log.d(TAG, "No output surface provided.");
- return null;
- }
-
- if (outputSurfaceInfo.orientationDegrees != 0) {
- matrixTransformationListBuilder.add(
- new ScaleToFitTransformation.Builder()
- .setRotationDegrees(outputSurfaceInfo.orientationDegrees)
- .build());
- }
- if (outputSurfaceInfo.width != outputSize.getWidth()
- || outputSurfaceInfo.height != outputSize.getHeight()) {
- matrixTransformationListBuilder.add(
- Presentation.createForWidthAndHeight(
- outputSurfaceInfo.width, outputSurfaceInfo.height, Presentation.LAYOUT_SCALE_TO_FIT));
- }
-
- // Convert final list of matrix transformations (including additional transformations for the
- // output surface) to a SingleFrameGlTextureProcessors.
- ImmutableList matrixTransformations =
- matrixTransformationListBuilder.build();
- if (!matrixTransformations.isEmpty()) {
- intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
- MatrixTransformationProcessor matrixTransformationProcessor =
- new MatrixTransformationProcessor(context, matrixTransformations);
- outputSize =
- matrixTransformationProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
- checkState(outputSize.getWidth() == outputSurfaceInfo.width);
- checkState(outputSize.getHeight() == outputSurfaceInfo.height);
- textureProcessors.add(matrixTransformationProcessor);
- }
-
- EGLSurface outputEglSurface;
- if (enableExperimentalHdrEditing) {
- // TODO(b/227624622): Don't assume BT.2020 PQ input/output.
- outputEglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurfaceInfo.surface);
- } else {
- outputEglSurface = GlUtil.getEglSurface(eglDisplay, outputSurfaceInfo.surface);
- }
- return new FrameProcessorChain(
- eglDisplay,
- eglContext,
- singleThreadExecutorService,
- inputExternalTexId,
- streamOffsetUs,
- intermediateTextures.build(),
- textureProcessors.build(),
- outputSurfaceInfo.width,
- outputSurfaceInfo.height,
- outputEglSurface,
- listener,
- debugViewProvider.getDebugPreviewSurfaceView(
- outputSurfaceInfo.width, outputSurfaceInfo.height),
- enableExperimentalHdrEditing);
+ textureProcessorListBuilder.add(
+ new FinalMatrixTransformationProcessorWrapper(
+ context,
+ eglDisplay,
+ eglContext,
+ matrixTransformationListBuilder.build(),
+ outputSurfaceProvider,
+ streamOffsetUs,
+ listener,
+ debugViewProvider,
+ enableExperimentalHdrEditing));
+ return textureProcessorListBuilder.build();
}
- private static TextureInfo createTexture(int outputWidth, int outputHeight)
- throws GlUtil.GlException {
- int texId = GlUtil.createTexture(outputWidth, outputHeight);
- int fboId = GlUtil.createFboForTexture(texId);
- return new TextureInfo(texId, fboId, outputWidth, outputHeight);
+ /**
+ * Chains the given {@link GlTextureProcessor} instances using {@link
+ * ChainingGlTextureProcessorListener} instances.
+ *
+ * The {@link ExternalTextureProcessor} is the first processor in the chain.
+ */
+ private static void chainTextureProcessorsWithListeners(
+ ExternalTextureProcessor externalTextureProcessor,
+ ImmutableList textureProcessors,
+ FrameProcessingTaskExecutor frameProcessingTaskExecutor,
+ FrameProcessorChain.Listener listener) {
+ externalTextureProcessor.setListener(
+ new ChainingGlTextureProcessorListener(
+ /* previousGlTextureProcessor= */ null,
+ textureProcessors.get(0),
+ frameProcessingTaskExecutor,
+ listener));
+ GlTextureProcessor previousGlTextureProcessor = externalTextureProcessor;
+ for (int i = 0; i < textureProcessors.size(); i++) {
+ GlTextureProcessor glTextureProcessor = textureProcessors.get(i);
+ @Nullable
+ GlTextureProcessor nextGlTextureProcessor =
+ i + 1 < textureProcessors.size() ? textureProcessors.get(i + 1) : null;
+ glTextureProcessor.setListener(
+ new ChainingGlTextureProcessorListener(
+ previousGlTextureProcessor,
+ nextGlTextureProcessor,
+ frameProcessingTaskExecutor,
+ listener));
+ previousGlTextureProcessor = glTextureProcessor;
+ }
}
private static final String TAG = "FrameProcessorChain";
private static final String THREAD_NAME = "Transformer:FrameProcessorChain";
private static final long RELEASE_WAIT_TIME_MS = 100;
- private final boolean enableExperimentalHdrEditing;
private final EGLDisplay eglDisplay;
private final EGLContext eglContext;
private final FrameProcessingTaskExecutor frameProcessingTaskExecutor;
@@ -322,100 +324,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* timestamps, in microseconds.
*/
private final long streamOffsetUs;
- /** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */
- private final AtomicInteger pendingFrameCount;
- /** Wraps the {@link #inputSurfaceTexture}. */
- private final Surface inputSurface;
+ /**
+ * Number of frames {@linkplain #registerInputFrame() registered} but not processed off the {@link
+ * #inputSurfaceTexture} yet.
+ */
+ private final AtomicInteger pendingInputFrameCount;
/** Associated with an OpenGL external texture. */
private final SurfaceTexture inputSurfaceTexture;
- /** Identifier of the OpenGL texture associated with the input {@link SurfaceTexture}. */
- private final int inputExternalTexId;
- /** Transformation matrix associated with the {@link #inputSurfaceTexture}. */
- private final float[] textureTransformMatrix;
+ /** Wraps the {@link #inputSurfaceTexture}. */
+ private final Surface inputSurface;
- /**
- * Contains an {@link ExternalTextureProcessor} at the 0th index and optionally other {@link
- * SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} at indices >= 1.
- */
- private final ImmutableList textureProcessors;
-
- /**
- * {@link TextureInfo} instances describing the intermediate textures that receive output from the
- * previous {@link SingleFrameGlTextureProcessor}, and provide input for the following {@link
- * SingleFrameGlTextureProcessor}.
- */
- private final ImmutableList intermediateTextures;
-
- private final Listener listener;
-
- /**
- * Prevents further frame processing tasks from being scheduled or executed after {@link
- * #release()} is called or an exception occurred.
- */
- private final AtomicBoolean stopProcessing;
-
- private final int outputWidth;
- private final int outputHeight;
- /**
- * Wraps the output {@link Surface} that is populated with the output of the final {@link
- * SingleFrameGlTextureProcessor} for each frame.
- */
- private final EGLSurface outputEglSurface;
- /**
- * Wraps a debug {@link SurfaceView} that is populated with the output of the final {@link
- * SingleFrameGlTextureProcessor} for each frame.
- */
- private @MonotonicNonNull SurfaceViewWrapper debugSurfaceViewWrapper;
+ private final float[] inputSurfaceTextureTransformMatrix;
+ private final TextureInfo inputExternalTexture;
+ private final ExternalTextureProcessor inputExternalTextureProcessor;
+ private final ImmutableList textureProcessors;
private boolean inputStreamEnded;
- // TODO(b/227625423): accept GlTextureProcessors instead of SingleFrameGlTextureProcessors once
- // this interface exists.
private FrameProcessorChain(
EGLDisplay eglDisplay,
EGLContext eglContext,
- ExecutorService singleThreadExecutorService,
- int inputExternalTexId,
+ FrameProcessingTaskExecutor frameProcessingTaskExecutor,
long streamOffsetUs,
- ImmutableList intermediateTextures,
- ImmutableList textureProcessors,
- int outputWidth,
- int outputHeight,
- EGLSurface outputEglSurface,
- Listener listener,
- @Nullable SurfaceView debugSurfaceView,
- boolean enableExperimentalHdrEditing) {
+ TextureInfo inputExternalTexture,
+ ExternalTextureProcessor inputExternalTextureProcessor,
+ ImmutableList textureProcessors) {
checkState(!textureProcessors.isEmpty());
this.eglDisplay = eglDisplay;
this.eglContext = eglContext;
- this.inputExternalTexId = inputExternalTexId;
+ this.frameProcessingTaskExecutor = frameProcessingTaskExecutor;
this.streamOffsetUs = streamOffsetUs;
- this.intermediateTextures = intermediateTextures;
+ this.inputExternalTexture = inputExternalTexture;
+ this.inputExternalTextureProcessor = inputExternalTextureProcessor;
this.textureProcessors = textureProcessors;
- this.outputWidth = outputWidth;
- this.outputHeight = outputHeight;
- this.outputEglSurface = outputEglSurface;
- this.listener = listener;
- this.stopProcessing = new AtomicBoolean();
- this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
- frameProcessingTaskExecutor =
- new FrameProcessingTaskExecutor(singleThreadExecutorService, listener);
- pendingFrameCount = new AtomicInteger();
- inputSurfaceTexture = new SurfaceTexture(inputExternalTexId);
+ pendingInputFrameCount = new AtomicInteger();
+ inputSurfaceTexture = new SurfaceTexture(inputExternalTexture.texId);
inputSurface = new Surface(inputSurfaceTexture);
- textureTransformMatrix = new float[16];
- if (debugSurfaceView != null) {
- debugSurfaceViewWrapper = new SurfaceViewWrapper(debugSurfaceView);
- }
+ inputSurfaceTextureTransformMatrix = new float[16];
}
/** Returns the input {@link Surface}. */
public Surface getInputSurface() {
// TODO(b/227625423): Allow input surface to be recreated for input size change.
inputSurfaceTexture.setOnFrameAvailableListener(
- surfaceTexture -> frameProcessingTaskExecutor.submit(this::processFrame));
+ surfaceTexture -> frameProcessingTaskExecutor.submit(this::processInputFrame));
return inputSurface;
}
@@ -428,15 +382,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
public void registerInputFrame() {
checkState(!inputStreamEnded);
- pendingFrameCount.incrementAndGet();
+ pendingInputFrameCount.incrementAndGet();
}
/**
* Returns the number of input frames that have been {@linkplain #registerInputFrame() registered}
- * but not completely processed yet.
+ * but not processed off the {@linkplain #getInputSurface() input surface} yet.
*/
- public int getPendingFrameCount() {
- return pendingFrameCount.get();
+ public int getPendingInputFrameCount() {
+ return pendingInputFrameCount.get();
}
/**
@@ -447,7 +401,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void signalEndOfInputStream() {
checkState(!inputStreamEnded);
inputStreamEnded = true;
- frameProcessingTaskExecutor.submit(this::signalEndOfOutputStream);
+ frameProcessingTaskExecutor.submit(this::processEndOfInputStream);
}
/**
@@ -461,7 +415,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* This method blocks until all OpenGL resources are released or releasing times out.
*/
public void release() {
- stopProcessing.set(true);
try {
frameProcessingTaskExecutor.release(
/* releaseTask= */ this::releaseTextureProcessorsAndDestroyGlContext,
@@ -475,163 +428,61 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
/**
- * Processes an input frame.
+ * Processes an input frame from the {@linkplain #getInputSurface() external input surface
+ * texture}.
*
*
This method must be called on the {@linkplain #THREAD_NAME background thread}.
*/
@WorkerThread
- private void processFrame() throws FrameProcessingException {
+ private void processInputFrame() {
checkState(Thread.currentThread().getName().equals(THREAD_NAME));
+ if (!inputExternalTextureProcessor.acceptsInputFrame()) {
+ frameProcessingTaskExecutor.submit(this::processInputFrame); // Try again later.
+ return;
+ }
inputSurfaceTexture.updateTexImage();
long inputFrameTimeNs = inputSurfaceTexture.getTimestamp();
// Correct for the stream offset so processors see original media presentation timestamps.
long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs;
- inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
- ((ExternalTextureProcessor) textureProcessors.get(0))
- .setTextureTransformMatrix(textureTransformMatrix);
- int inputTexId = inputExternalTexId;
-
- try {
- for (int i = 0; i < textureProcessors.size() - 1; i++) {
- if (stopProcessing.get()) {
- return;
- }
-
- TextureInfo outputTexture = intermediateTextures.get(i);
- GlUtil.focusFramebuffer(
- eglDisplay,
- eglContext,
- outputEglSurface,
- outputTexture.fboId,
- outputTexture.width,
- outputTexture.height);
- GlUtil.clearOutputFrame();
- textureProcessors.get(i).drawFrame(inputTexId, presentationTimeUs);
- inputTexId = outputTexture.texId;
- }
- GlUtil.focusEglSurface(eglDisplay, eglContext, outputEglSurface, outputWidth, outputHeight);
- GlUtil.clearOutputFrame();
- getLast(textureProcessors).drawFrame(inputTexId, presentationTimeUs);
-
- EGLExt.eglPresentationTimeANDROID(eglDisplay, outputEglSurface, inputFrameTimeNs);
- EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
- } catch (GlUtil.GlException e) {
- throw new FrameProcessingException(e, presentationTimeUs);
- }
-
- try {
- if (debugSurfaceViewWrapper != null) {
- long finalPresentationTimeUs = presentationTimeUs;
- int finalInputTexId = inputTexId;
- debugSurfaceViewWrapper.maybeRenderToSurfaceView(
- () -> {
- GlUtil.clearOutputFrame();
- getLast(textureProcessors).drawFrame(finalInputTexId, finalPresentationTimeUs);
- });
- }
- } catch (FrameProcessingException | GlUtil.GlException e) {
- Log.d(TAG, "Error rendering to debug preview", e);
- }
-
- checkState(pendingFrameCount.getAndDecrement() > 0);
+ inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix);
+ inputExternalTextureProcessor.setTextureTransformMatrix(inputSurfaceTextureTransformMatrix);
+ checkState(
+ inputExternalTextureProcessor.maybeQueueInputFrame(
+ inputExternalTexture, presentationTimeUs));
+ checkState(pendingInputFrameCount.getAndDecrement() > 0);
+ // After the inputExternalTextureProcessor has produced an output frame, it is processed
+ // asynchronously by the texture processors chained after it.
}
- /** Calls {@link Listener#onFrameProcessingEnded()} once no more frames are pending. */
+ /**
+ * Propagates the end-of-stream signal through the texture processors once no more input frames
+ * are pending.
+ *
+ *
This method must be called on the {@linkplain #THREAD_NAME background thread}.
+ */
@WorkerThread
- private void signalEndOfOutputStream() {
- if (getPendingFrameCount() == 0) {
- listener.onFrameProcessingEnded();
+ private void processEndOfInputStream() {
+ if (getPendingInputFrameCount() == 0) {
+ // Propagates the end of stream signal through the chained texture processors.
+ inputExternalTextureProcessor.signalEndOfInputStream();
} else {
- frameProcessingTaskExecutor.submit(this::signalEndOfOutputStream);
+ frameProcessingTaskExecutor.submit(this::processEndOfInputStream);
}
}
/**
- * Releases the {@link SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} and destroys
- * the OpenGL context.
+ * Releases the {@link GlTextureProcessor} instances and destroys the OpenGL context.
*
*
This method must be called on the {@linkplain #THREAD_NAME background thread}.
*/
@WorkerThread
private void releaseTextureProcessorsAndDestroyGlContext()
throws GlUtil.GlException, FrameProcessingException {
+ inputExternalTextureProcessor.release();
for (int i = 0; i < textureProcessors.size(); i++) {
textureProcessors.get(i).release();
}
GlUtil.destroyEglContext(eglDisplay, eglContext);
}
-
- /**
- * Wrapper around a {@link SurfaceView} that keeps track of whether the output surface is valid,
- * and makes rendering a no-op if not.
- */
- private final class SurfaceViewWrapper implements SurfaceHolder.Callback {
-
- @GuardedBy("this")
- @Nullable
- private Surface surface;
-
- @GuardedBy("this")
- @Nullable
- private EGLSurface eglSurface;
-
- private int width;
- private int height;
-
- public SurfaceViewWrapper(SurfaceView surfaceView) {
- surfaceView.getHolder().addCallback(this);
- surface = surfaceView.getHolder().getSurface();
- width = surfaceView.getWidth();
- height = surfaceView.getHeight();
- }
-
- /**
- * Focuses the wrapped surface view's surface as an {@link EGLSurface}, renders using {@code
- * renderingTask} and swaps buffers, if the view's holder has a valid surface. Does nothing
- * otherwise.
- */
- @WorkerThread
- public synchronized void maybeRenderToSurfaceView(FrameProcessingTask renderingTask)
- throws GlUtil.GlException, FrameProcessingException {
- if (surface == null) {
- return;
- }
-
- if (eglSurface == null) {
- if (enableExperimentalHdrEditing) {
- eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, surface);
- } else {
- eglSurface = GlUtil.getEglSurface(eglDisplay, surface);
- }
- }
- EGLSurface eglSurface = this.eglSurface;
- GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height);
- renderingTask.run();
- EGL14.eglSwapBuffers(eglDisplay, eglSurface);
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder holder) {}
-
- @Override
- public synchronized void surfaceChanged(
- SurfaceHolder holder, int format, int width, int height) {
- this.width = width;
- this.height = height;
- Surface newSurface = holder.getSurface();
- if (surface == null || !surface.equals(newSurface)) {
- surface = newSurface;
- eglSurface = null;
- }
- }
-
- @Override
- public synchronized void surfaceDestroyed(SurfaceHolder holder) {
- surface = null;
- eglSurface = null;
- width = C.LENGTH_UNSET;
- height = C.LENGTH_UNSET;
- }
- }
}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java
index 84f05c815b..b767f973a5 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java
@@ -75,6 +75,16 @@ public abstract class SingleFrameGlTextureProcessor implements GlTextureProcesso
this.listener = listener;
}
+ /**
+ * Returns whether the {@code SingleFrameGlTextureProcessor} can accept an input frame.
+ *
+ *
If this method returns {@code true}, the next call to {@link #maybeQueueInputFrame(
+ * TextureInfo, long)} will also return {@code true}.
+ */
+ public boolean acceptsInputFrame() {
+ return !outputTextureInUse;
+ }
+
@Override
public final boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
if (outputTextureInUse) {
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
index 2a7a9aa37c..3bdb16f73e 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
@@ -17,7 +17,6 @@
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
-import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.media.MediaCodec;
@@ -32,7 +31,6 @@ import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.dataflow.qual.Pure;
@@ -91,8 +89,6 @@ import org.checkerframework.dataflow.qual.Pure;
effectsListBuilder.add(Presentation.createForHeight(transformationRequest.outputHeight));
}
- AtomicReference encoderInitializationException =
- new AtomicReference<>();
encoderWrapper =
new EncoderWrapper(
encoderFactory,
@@ -100,9 +96,8 @@ import org.checkerframework.dataflow.qual.Pure;
allowedOutputMimeTypes,
transformationRequest,
fallbackListener,
- encoderInitializationException);
+ asyncErrorListener);
- @Nullable FrameProcessorChain frameProcessorChain;
try {
frameProcessorChain =
FrameProcessorChain.create(
@@ -137,12 +132,6 @@ import org.checkerframework.dataflow.qual.Pure;
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
}
- if (frameProcessorChain == null) {
- // Failed to create FrameProcessorChain because the encoder could not provide a surface.
- throw checkStateNotNull(encoderInitializationException.get());
- }
- this.frameProcessorChain = frameProcessorChain;
-
decoder =
decoderFactory.createForVideoDecoding(
inputFormat,
@@ -266,7 +255,7 @@ import org.checkerframework.dataflow.qual.Pure;
}
if (maxPendingFrameCount != Codec.UNLIMITED_PENDING_FRAME_COUNT
- && frameProcessorChain.getPendingFrameCount() == maxPendingFrameCount) {
+ && frameProcessorChain.getPendingInputFrameCount() == maxPendingFrameCount) {
return false;
}
@@ -303,7 +292,7 @@ import org.checkerframework.dataflow.qual.Pure;
private final List allowedOutputMimeTypes;
private final TransformationRequest transformationRequest;
private final FallbackListener fallbackListener;
- private final AtomicReference encoderInitializationException;
+ private final Transformer.AsyncErrorListener asyncErrorListener;
private @MonotonicNonNull SurfaceInfo encoderSurfaceInfo;
@@ -317,14 +306,14 @@ import org.checkerframework.dataflow.qual.Pure;
List allowedOutputMimeTypes,
TransformationRequest transformationRequest,
FallbackListener fallbackListener,
- AtomicReference encoderInitializationException) {
+ Transformer.AsyncErrorListener asyncErrorListener) {
this.encoderFactory = encoderFactory;
this.inputFormat = inputFormat;
this.allowedOutputMimeTypes = allowedOutputMimeTypes;
this.transformationRequest = transformationRequest;
this.fallbackListener = fallbackListener;
- this.encoderInitializationException = encoderInitializationException;
+ this.asyncErrorListener = asyncErrorListener;
}
@Override
@@ -365,7 +354,7 @@ import org.checkerframework.dataflow.qual.Pure;
encoder =
encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes);
} catch (TransformationException e) {
- encoderInitializationException.set(e);
+ asyncErrorListener.onTransformationException(e);
return null;
}
Format encoderSupportedFormat = encoder.getConfigurationFormat();
diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java
index e7e7c5e4f6..ac86e370dd 100644
--- a/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java
+++ b/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java
@@ -29,7 +29,6 @@ import androidx.media3.common.util.ListenerSet;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,7 +51,7 @@ public final class VideoEncoderWrapperTest {
/* allowedOutputMimeTypes= */ ImmutableList.of(),
emptyTransformationRequest,
fallbackListener,
- new AtomicReference<>());
+ mock(Transformer.AsyncErrorListener.class));
@Before
public void registerTrack() {