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
This commit is contained in:
hschlueter 2022-06-22 12:03:41 +01:00 committed by Ian Baker
parent a7649b639c
commit 555ab97e34
6 changed files with 580 additions and 368 deletions

View File

@ -17,6 +17,7 @@ package androidx.media3.demo.transformer;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
@ -420,9 +421,28 @@ public final class TransformerActivity extends AppCompatActivity {
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider { 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 @Nullable
@Override @Override
public SurfaceView getDebugPreviewSurfaceView(int width, int height) { 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. // Update the UI on the main thread and wait for the output surface to be available.
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1); CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this); SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
@ -459,6 +479,7 @@ public final class TransformerActivity extends AppCompatActivity {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return null; return null;
} }
this.surfaceView = surfaceView;
return surfaceView; return surfaceView;
} }
} }

View File

@ -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.
*
* <p>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}.
*
* <p>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<GlMatrixTransformation> 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<GlMatrixTransformation> 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}
*
* <p>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<GlMatrixTransformation> matrixTransformationListBuilder =
new ImmutableList.Builder<GlMatrixTransformation>().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;
}
}
}

View File

@ -17,35 +17,24 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static com.google.common.collect.Iterables.getLast;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.opengl.EGLContext; import android.opengl.EGLContext;
import android.opengl.EGLDisplay; import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.util.Size;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* {@code FrameProcessorChain} applies changes to individual video frames. * {@code FrameProcessorChain} applies changes to individual video frames.
@ -53,10 +42,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously * <p>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 * 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. * {@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 * {@link #getPendingInputFrameCount()} can be used to check whether there are frames that have not
* fully processed yet. Output is written to the provided {@linkplain #create(Context, Listener, * been fully processed yet. Output is written to the provided {@linkplain #create(Context,
* float, int, int, long, List, SurfaceInfo.Provider, Transformer.DebugViewProvider, boolean) output * Listener, float, int, int, long, List, SurfaceInfo.Provider, Transformer.DebugViewProvider,
* surface}. * boolean) output surface}.
*/ */
// TODO(b/227625423): Factor out FrameProcessor interface and rename this class to GlFrameProcessor. // TODO(b/227625423): Factor out FrameProcessor interface and rename this class to GlFrameProcessor.
/* package */ final class FrameProcessorChain { /* package */ final class FrameProcessorChain {
@ -93,17 +82,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Surface}. * Surface}.
* @param debugViewProvider A {@link Transformer.DebugViewProvider}. * @param debugViewProvider A {@link Transformer.DebugViewProvider}.
* @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. * @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 * @throws FrameProcessingException If reading shader files fails, or an OpenGL error occurs while
* creating and configuring the OpenGL components. * 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( public static FrameProcessorChain create(
Context context, Context context,
Listener listener, FrameProcessorChain.Listener listener,
float pixelWidthHeightRatio, float pixelWidthHeightRatio,
int inputWidth, int inputWidth,
int inputHeight, int inputHeight,
@ -118,25 +103,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
Future<Optional<FrameProcessorChain>> frameProcessorChainFuture = Future<FrameProcessorChain> frameProcessorChainFuture =
singleThreadExecutorService.submit( singleThreadExecutorService.submit(
() -> () ->
Optional.fromNullable( createOpenGlObjectsAndFrameProcessorChain(
createOpenGlObjectsAndFrameProcessorChain( context,
context, listener,
listener, pixelWidthHeightRatio,
pixelWidthHeightRatio, inputWidth,
inputWidth, inputHeight,
inputHeight, streamOffsetUs,
streamOffsetUs, effects,
effects, outputSurfaceProvider,
outputSurfaceProvider, debugViewProvider,
debugViewProvider, enableExperimentalHdrEditing,
enableExperimentalHdrEditing, singleThreadExecutorService));
singleThreadExecutorService)));
try { try {
return frameProcessorChainFuture.get().orNull(); return frameProcessorChainFuture.get();
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new FrameProcessingException(e); throw new FrameProcessingException(e);
} catch (InterruptedException 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 * Creates the OpenGL context, surfaces, textures, and framebuffers, initializes {@link
* SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} corresponding to the {@link * GlTextureProcessor} instances corresponding to the {@link GlEffect} instances, and returns a
* GlEffect GlEffects}, and returns a new {@code FrameProcessorChain}. * new {@code FrameProcessorChain}.
* *
* <p>This method must be executed using the {@code singleThreadExecutorService}, as all later * <p>This method must be executed using the {@code singleThreadExecutorService}, as later OpenGL
* OpenGL commands will be called on that thread. * commands will be called on that thread.
*/ */
@WorkerThread @WorkerThread
@Nullable
private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain( private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain(
Context context, Context context,
Listener listener, FrameProcessorChain.Listener listener,
float pixelWidthHeightRatio, float pixelWidthHeightRatio,
int inputWidth, int inputWidth,
int inputHeight, int inputHeight,
@ -188,132 +171,151 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder = ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder =
new ImmutableList.Builder<>(); new ImmutableList.Builder<>();
// Scale to expand the frame to apply the pixelWidthHeightRatio. if (pixelWidthHeightRatio != 1f) {
if (pixelWidthHeightRatio > 1f) {
matrixTransformationListBuilder.add( matrixTransformationListBuilder.add(
new ScaleToFitTransformation.Builder() createPixelWidthHeightRatioTransformation(pixelWidthHeightRatio));
.setScale(/* scaleX= */ pixelWidthHeightRatio, /* scaleY= */ 1f)
.build());
} else if (pixelWidthHeightRatio < 1f) {
matrixTransformationListBuilder.add(
new ScaleToFitTransformation.Builder()
.setScale(/* scaleX= */ 1f, /* scaleY= */ 1f / pixelWidthHeightRatio)
.build());
} }
ImmutableList<GlTextureProcessor> textureProcessors =
getGlTextureProcessorsForGlEffects(
context,
effects,
eglDisplay,
eglContext,
matrixTransformationListBuilder,
outputSurfaceProvider,
streamOffsetUs,
listener,
debugViewProvider,
enableExperimentalHdrEditing);
ExternalTextureProcessor externalTextureProcessor = ExternalTextureProcessor externalTextureProcessor =
new ExternalTextureProcessor(context, enableExperimentalHdrEditing); new ExternalTextureProcessor(context, enableExperimentalHdrEditing);
int inputExternalTexId = GlUtil.createExternalTexture(); FrameProcessingTaskExecutor frameProcessingTaskExecutor =
Size outputSize = externalTextureProcessor.configure(inputWidth, inputHeight); new FrameProcessingTaskExecutor(singleThreadExecutorService, listener);
ImmutableList.Builder<TextureInfo> intermediateTextures = new ImmutableList.Builder<>(); chainTextureProcessorsWithListeners(
ImmutableList.Builder<SingleFrameGlTextureProcessor> textureProcessors = externalTextureProcessor, textureProcessors, frameProcessingTaskExecutor, listener);
new ImmutableList.Builder<SingleFrameGlTextureProcessor>().add(externalTextureProcessor);
// Combine consecutive GlMatrixTransformations into a single SingleFrameGlTextureProcessor and return new FrameProcessorChain(
// convert all other GlEffects to SingleFrameGlTextureProcessors. 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}.
*
* <p>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.
*
* <p>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<GlTextureProcessor> getGlTextureProcessorsForGlEffects(
Context context,
List<GlEffect> effects,
EGLDisplay eglDisplay,
EGLContext eglContext,
ImmutableList.Builder<GlMatrixTransformation> matrixTransformationListBuilder,
SurfaceInfo.Provider outputSurfaceProvider,
long streamOffsetUs,
FrameProcessorChain.Listener listener,
Transformer.DebugViewProvider debugViewProvider,
boolean enableExperimentalHdrEditing)
throws FrameProcessingException {
ImmutableList.Builder<GlTextureProcessor> textureProcessorListBuilder =
new ImmutableList.Builder<>();
for (int i = 0; i < effects.size(); i++) { for (int i = 0; i < effects.size(); i++) {
GlEffect effect = effects.get(i); GlEffect effect = effects.get(i);
if (effect instanceof GlMatrixTransformation) { if (effect instanceof GlMatrixTransformation) {
matrixTransformationListBuilder.add((GlMatrixTransformation) effect); matrixTransformationListBuilder.add((GlMatrixTransformation) effect);
continue; continue;
} }
ImmutableList<GlMatrixTransformation> matrixTransformations = ImmutableList<GlMatrixTransformation> matrixTransformations =
matrixTransformationListBuilder.build(); matrixTransformationListBuilder.build();
if (!matrixTransformations.isEmpty()) { if (!matrixTransformations.isEmpty()) {
MatrixTransformationProcessor matrixTransformationProcessor = textureProcessorListBuilder.add(
new MatrixTransformationProcessor(context, matrixTransformations); new MatrixTransformationProcessor(context, matrixTransformations));
intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight()));
outputSize =
matrixTransformationProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
textureProcessors.add(matrixTransformationProcessor);
matrixTransformationListBuilder = new ImmutableList.Builder<>(); matrixTransformationListBuilder = new ImmutableList.Builder<>();
} }
intermediateTextures.add(createTexture(outputSize.getWidth(), outputSize.getHeight())); textureProcessorListBuilder.add(effect.toGlTextureProcessor(context));
SingleFrameGlTextureProcessor textureProcessor = effect.toGlTextureProcessor(context);
outputSize = textureProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
textureProcessors.add(textureProcessor);
} }
textureProcessorListBuilder.add(
// TODO(b/227625423): Request the output surface during processing when the output size becomes new FinalMatrixTransformationProcessorWrapper(
// available asynchronously via the final GlTextureProcessor instead of requesting it here. context,
// This will also avoid needing to return null here when no surface is provided. eglDisplay,
Size requestedOutputSize = eglContext,
MatrixUtils.configureAndGetOutputSize( matrixTransformationListBuilder.build(),
outputSize.getWidth(), outputSize.getHeight(), matrixTransformationListBuilder.build()); outputSurfaceProvider,
@Nullable streamOffsetUs,
SurfaceInfo outputSurfaceInfo = listener,
outputSurfaceProvider.getSurfaceInfo( debugViewProvider,
requestedOutputSize.getWidth(), requestedOutputSize.getHeight()); enableExperimentalHdrEditing));
if (outputSurfaceInfo == null) { return textureProcessorListBuilder.build();
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<GlMatrixTransformation> 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);
} }
private static TextureInfo createTexture(int outputWidth, int outputHeight) /**
throws GlUtil.GlException { * Chains the given {@link GlTextureProcessor} instances using {@link
int texId = GlUtil.createTexture(outputWidth, outputHeight); * ChainingGlTextureProcessorListener} instances.
int fboId = GlUtil.createFboForTexture(texId); *
return new TextureInfo(texId, fboId, outputWidth, outputHeight); * <p>The {@link ExternalTextureProcessor} is the first processor in the chain.
*/
private static void chainTextureProcessorsWithListeners(
ExternalTextureProcessor externalTextureProcessor,
ImmutableList<GlTextureProcessor> 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 TAG = "FrameProcessorChain";
private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; private static final String THREAD_NAME = "Transformer:FrameProcessorChain";
private static final long RELEASE_WAIT_TIME_MS = 100; private static final long RELEASE_WAIT_TIME_MS = 100;
private final boolean enableExperimentalHdrEditing;
private final EGLDisplay eglDisplay; private final EGLDisplay eglDisplay;
private final EGLContext eglContext; private final EGLContext eglContext;
private final FrameProcessingTaskExecutor frameProcessingTaskExecutor; private final FrameProcessingTaskExecutor frameProcessingTaskExecutor;
@ -322,100 +324,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* timestamps, in microseconds. * timestamps, in microseconds.
*/ */
private final long streamOffsetUs; private final long streamOffsetUs;
/** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */ /**
private final AtomicInteger pendingFrameCount; * Number of frames {@linkplain #registerInputFrame() registered} but not processed off the {@link
/** Wraps the {@link #inputSurfaceTexture}. */ * #inputSurfaceTexture} yet.
private final Surface inputSurface; */
private final AtomicInteger pendingInputFrameCount;
/** Associated with an OpenGL external texture. */ /** Associated with an OpenGL external texture. */
private final SurfaceTexture inputSurfaceTexture; private final SurfaceTexture inputSurfaceTexture;
/** Identifier of the OpenGL texture associated with the input {@link SurfaceTexture}. */ /** Wraps the {@link #inputSurfaceTexture}. */
private final int inputExternalTexId; private final Surface inputSurface;
/** Transformation matrix associated with the {@link #inputSurfaceTexture}. */
private final float[] textureTransformMatrix;
/** private final float[] inputSurfaceTextureTransformMatrix;
* Contains an {@link ExternalTextureProcessor} at the 0th index and optionally other {@link private final TextureInfo inputExternalTexture;
* SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} at indices >= 1. private final ExternalTextureProcessor inputExternalTextureProcessor;
*/ private final ImmutableList<GlTextureProcessor> textureProcessors;
private final ImmutableList<SingleFrameGlTextureProcessor> 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<TextureInfo> 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 boolean inputStreamEnded; private boolean inputStreamEnded;
// TODO(b/227625423): accept GlTextureProcessors instead of SingleFrameGlTextureProcessors once
// this interface exists.
private FrameProcessorChain( private FrameProcessorChain(
EGLDisplay eglDisplay, EGLDisplay eglDisplay,
EGLContext eglContext, EGLContext eglContext,
ExecutorService singleThreadExecutorService, FrameProcessingTaskExecutor frameProcessingTaskExecutor,
int inputExternalTexId,
long streamOffsetUs, long streamOffsetUs,
ImmutableList<TextureInfo> intermediateTextures, TextureInfo inputExternalTexture,
ImmutableList<SingleFrameGlTextureProcessor> textureProcessors, ExternalTextureProcessor inputExternalTextureProcessor,
int outputWidth, ImmutableList<GlTextureProcessor> textureProcessors) {
int outputHeight,
EGLSurface outputEglSurface,
Listener listener,
@Nullable SurfaceView debugSurfaceView,
boolean enableExperimentalHdrEditing) {
checkState(!textureProcessors.isEmpty()); checkState(!textureProcessors.isEmpty());
this.eglDisplay = eglDisplay; this.eglDisplay = eglDisplay;
this.eglContext = eglContext; this.eglContext = eglContext;
this.inputExternalTexId = inputExternalTexId; this.frameProcessingTaskExecutor = frameProcessingTaskExecutor;
this.streamOffsetUs = streamOffsetUs; this.streamOffsetUs = streamOffsetUs;
this.intermediateTextures = intermediateTextures; this.inputExternalTexture = inputExternalTexture;
this.inputExternalTextureProcessor = inputExternalTextureProcessor;
this.textureProcessors = textureProcessors; this.textureProcessors = textureProcessors;
this.outputWidth = outputWidth;
this.outputHeight = outputHeight;
this.outputEglSurface = outputEglSurface;
this.listener = listener;
this.stopProcessing = new AtomicBoolean();
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
frameProcessingTaskExecutor = pendingInputFrameCount = new AtomicInteger();
new FrameProcessingTaskExecutor(singleThreadExecutorService, listener); inputSurfaceTexture = new SurfaceTexture(inputExternalTexture.texId);
pendingFrameCount = new AtomicInteger();
inputSurfaceTexture = new SurfaceTexture(inputExternalTexId);
inputSurface = new Surface(inputSurfaceTexture); inputSurface = new Surface(inputSurfaceTexture);
textureTransformMatrix = new float[16]; inputSurfaceTextureTransformMatrix = new float[16];
if (debugSurfaceView != null) {
debugSurfaceViewWrapper = new SurfaceViewWrapper(debugSurfaceView);
}
} }
/** Returns the input {@link Surface}. */ /** Returns the input {@link Surface}. */
public Surface getInputSurface() { public Surface getInputSurface() {
// TODO(b/227625423): Allow input surface to be recreated for input size change. // TODO(b/227625423): Allow input surface to be recreated for input size change.
inputSurfaceTexture.setOnFrameAvailableListener( inputSurfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> frameProcessingTaskExecutor.submit(this::processFrame)); surfaceTexture -> frameProcessingTaskExecutor.submit(this::processInputFrame));
return inputSurface; return inputSurface;
} }
@ -428,15 +382,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
public void registerInputFrame() { public void registerInputFrame() {
checkState(!inputStreamEnded); checkState(!inputStreamEnded);
pendingFrameCount.incrementAndGet(); pendingInputFrameCount.incrementAndGet();
} }
/** /**
* Returns the number of input frames that have been {@linkplain #registerInputFrame() registered} * 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() { public int getPendingInputFrameCount() {
return pendingFrameCount.get(); return pendingInputFrameCount.get();
} }
/** /**
@ -447,7 +401,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void signalEndOfInputStream() { public void signalEndOfInputStream() {
checkState(!inputStreamEnded); checkState(!inputStreamEnded);
inputStreamEnded = true; inputStreamEnded = true;
frameProcessingTaskExecutor.submit(this::signalEndOfOutputStream); frameProcessingTaskExecutor.submit(this::processEndOfInputStream);
} }
/** /**
@ -461,7 +415,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>This method blocks until all OpenGL resources are released or releasing times out. * <p>This method blocks until all OpenGL resources are released or releasing times out.
*/ */
public void release() { public void release() {
stopProcessing.set(true);
try { try {
frameProcessingTaskExecutor.release( frameProcessingTaskExecutor.release(
/* releaseTask= */ this::releaseTextureProcessorsAndDestroyGlContext, /* 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}.
* *
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}. * <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
*/ */
@WorkerThread @WorkerThread
private void processFrame() throws FrameProcessingException { private void processInputFrame() {
checkState(Thread.currentThread().getName().equals(THREAD_NAME)); checkState(Thread.currentThread().getName().equals(THREAD_NAME));
if (!inputExternalTextureProcessor.acceptsInputFrame()) {
frameProcessingTaskExecutor.submit(this::processInputFrame); // Try again later.
return;
}
inputSurfaceTexture.updateTexImage(); inputSurfaceTexture.updateTexImage();
long inputFrameTimeNs = inputSurfaceTexture.getTimestamp(); long inputFrameTimeNs = inputSurfaceTexture.getTimestamp();
// Correct for the stream offset so processors see original media presentation timestamps. // Correct for the stream offset so processors see original media presentation timestamps.
long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs; long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs;
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix);
((ExternalTextureProcessor) textureProcessors.get(0)) inputExternalTextureProcessor.setTextureTransformMatrix(inputSurfaceTextureTransformMatrix);
.setTextureTransformMatrix(textureTransformMatrix); checkState(
int inputTexId = inputExternalTexId; inputExternalTextureProcessor.maybeQueueInputFrame(
inputExternalTexture, presentationTimeUs));
try { checkState(pendingInputFrameCount.getAndDecrement() > 0);
for (int i = 0; i < textureProcessors.size() - 1; i++) { // After the inputExternalTextureProcessor has produced an output frame, it is processed
if (stopProcessing.get()) { // asynchronously by the texture processors chained after it.
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);
} }
/** 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.
*
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
*/
@WorkerThread @WorkerThread
private void signalEndOfOutputStream() { private void processEndOfInputStream() {
if (getPendingFrameCount() == 0) { if (getPendingInputFrameCount() == 0) {
listener.onFrameProcessingEnded(); // Propagates the end of stream signal through the chained texture processors.
inputExternalTextureProcessor.signalEndOfInputStream();
} else { } else {
frameProcessingTaskExecutor.submit(this::signalEndOfOutputStream); frameProcessingTaskExecutor.submit(this::processEndOfInputStream);
} }
} }
/** /**
* Releases the {@link SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} and destroys * Releases the {@link GlTextureProcessor} instances and destroys the OpenGL context.
* the OpenGL context.
* *
* <p>This method must be called on the {@linkplain #THREAD_NAME background thread}. * <p>This method must be called on the {@linkplain #THREAD_NAME background thread}.
*/ */
@WorkerThread @WorkerThread
private void releaseTextureProcessorsAndDestroyGlContext() private void releaseTextureProcessorsAndDestroyGlContext()
throws GlUtil.GlException, FrameProcessingException { throws GlUtil.GlException, FrameProcessingException {
inputExternalTextureProcessor.release();
for (int i = 0; i < textureProcessors.size(); i++) { for (int i = 0; i < textureProcessors.size(); i++) {
textureProcessors.get(i).release(); textureProcessors.get(i).release();
} }
GlUtil.destroyEglContext(eglDisplay, eglContext); 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;
}
}
} }

View File

@ -75,6 +75,16 @@ public abstract class SingleFrameGlTextureProcessor implements GlTextureProcesso
this.listener = listener; this.listener = listener;
} }
/**
* Returns whether the {@code SingleFrameGlTextureProcessor} can accept an input frame.
*
* <p>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 @Override
public final boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) { public final boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
if (outputTextureInUse) { if (outputTextureInUse) {

View File

@ -17,7 +17,6 @@
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context; import android.content.Context;
import android.media.MediaCodec; import android.media.MediaCodec;
@ -32,7 +31,6 @@ import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.Pure;
@ -91,8 +89,6 @@ import org.checkerframework.dataflow.qual.Pure;
effectsListBuilder.add(Presentation.createForHeight(transformationRequest.outputHeight)); effectsListBuilder.add(Presentation.createForHeight(transformationRequest.outputHeight));
} }
AtomicReference<TransformationException> encoderInitializationException =
new AtomicReference<>();
encoderWrapper = encoderWrapper =
new EncoderWrapper( new EncoderWrapper(
encoderFactory, encoderFactory,
@ -100,9 +96,8 @@ import org.checkerframework.dataflow.qual.Pure;
allowedOutputMimeTypes, allowedOutputMimeTypes,
transformationRequest, transformationRequest,
fallbackListener, fallbackListener,
encoderInitializationException); asyncErrorListener);
@Nullable FrameProcessorChain frameProcessorChain;
try { try {
frameProcessorChain = frameProcessorChain =
FrameProcessorChain.create( FrameProcessorChain.create(
@ -137,12 +132,6 @@ import org.checkerframework.dataflow.qual.Pure;
e, TransformationException.ERROR_CODE_GL_INIT_FAILED); 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 = decoder =
decoderFactory.createForVideoDecoding( decoderFactory.createForVideoDecoding(
inputFormat, inputFormat,
@ -266,7 +255,7 @@ import org.checkerframework.dataflow.qual.Pure;
} }
if (maxPendingFrameCount != Codec.UNLIMITED_PENDING_FRAME_COUNT if (maxPendingFrameCount != Codec.UNLIMITED_PENDING_FRAME_COUNT
&& frameProcessorChain.getPendingFrameCount() == maxPendingFrameCount) { && frameProcessorChain.getPendingInputFrameCount() == maxPendingFrameCount) {
return false; return false;
} }
@ -303,7 +292,7 @@ import org.checkerframework.dataflow.qual.Pure;
private final List<String> allowedOutputMimeTypes; private final List<String> allowedOutputMimeTypes;
private final TransformationRequest transformationRequest; private final TransformationRequest transformationRequest;
private final FallbackListener fallbackListener; private final FallbackListener fallbackListener;
private final AtomicReference<TransformationException> encoderInitializationException; private final Transformer.AsyncErrorListener asyncErrorListener;
private @MonotonicNonNull SurfaceInfo encoderSurfaceInfo; private @MonotonicNonNull SurfaceInfo encoderSurfaceInfo;
@ -317,14 +306,14 @@ import org.checkerframework.dataflow.qual.Pure;
List<String> allowedOutputMimeTypes, List<String> allowedOutputMimeTypes,
TransformationRequest transformationRequest, TransformationRequest transformationRequest,
FallbackListener fallbackListener, FallbackListener fallbackListener,
AtomicReference<TransformationException> encoderInitializationException) { Transformer.AsyncErrorListener asyncErrorListener) {
this.encoderFactory = encoderFactory; this.encoderFactory = encoderFactory;
this.inputFormat = inputFormat; this.inputFormat = inputFormat;
this.allowedOutputMimeTypes = allowedOutputMimeTypes; this.allowedOutputMimeTypes = allowedOutputMimeTypes;
this.transformationRequest = transformationRequest; this.transformationRequest = transformationRequest;
this.fallbackListener = fallbackListener; this.fallbackListener = fallbackListener;
this.encoderInitializationException = encoderInitializationException; this.asyncErrorListener = asyncErrorListener;
} }
@Override @Override
@ -365,7 +354,7 @@ import org.checkerframework.dataflow.qual.Pure;
encoder = encoder =
encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes); encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes);
} catch (TransformationException e) { } catch (TransformationException e) {
encoderInitializationException.set(e); asyncErrorListener.onTransformationException(e);
return null; return null;
} }
Format encoderSupportedFormat = encoder.getConfigurationFormat(); Format encoderSupportedFormat = encoder.getConfigurationFormat();

View File

@ -29,7 +29,6 @@ import androidx.media3.common.util.ListenerSet;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -52,7 +51,7 @@ public final class VideoEncoderWrapperTest {
/* allowedOutputMimeTypes= */ ImmutableList.of(), /* allowedOutputMimeTypes= */ ImmutableList.of(),
emptyTransformationRequest, emptyTransformationRequest,
fallbackListener, fallbackListener,
new AtomicReference<>()); mock(Transformer.AsyncErrorListener.class));
@Before @Before
public void registerTrack() { public void registerTrack() {