From 555ab97e34d6586d38a9979bc0595a4818b7ecb8 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 22 Jun 2022 12:03:41 +0100 Subject: [PATCH] Support chaining async GlTextureProcessors in FrameProcessorChain. After this change, FrameProcessorChain chains any GlTextureProcessors instead of only SingleFrameGlTextureProcessors. The GlTextureProcessors are chained in a bidirectional manner using ChainingGlTextureProcessorListener to feed input and output related events forward and release events backwards. PiperOrigin-RevId: 456478414 --- .../demo/transformer/TransformerActivity.java | 21 + ...lMatrixTransformationProcessorWrapper.java | 342 +++++++++++ .../transformer/FrameProcessorChain.java | 549 +++++++----------- .../SingleFrameGlTextureProcessor.java | 10 + .../VideoTranscodingSamplePipeline.java | 23 +- .../transformer/VideoEncoderWrapperTest.java | 3 +- 6 files changed, 580 insertions(+), 368 deletions(-) create mode 100644 libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java 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() {