Move FrameProcessorChain OpenGL setup to factory method.

The encoder surface is no longer needed for the OpenGL setup and frame
processor initialization, as a placeholder surface is used instead. So
all of the setup can now be done in the factory method.

PiperOrigin-RevId: 438844450
This commit is contained in:
hschlueter 2022-04-01 17:41:01 +01:00 committed by Ian Baker
parent f8f8b75500
commit 2a66c7b8f5
6 changed files with 129 additions and 144 deletions

View File

@ -260,7 +260,7 @@ public final class FrameProcessorChainPixelTest {
outputSize.getHeight(), outputSize.getHeight(),
PixelFormat.RGBA_8888, PixelFormat.RGBA_8888,
/* maxImages= */ 1); /* maxImages= */ 1);
frameProcessorChain.configure( frameProcessorChain.setOutputSurface(
outputImageReader.getSurface(), outputImageReader.getSurface(),
outputSize.getWidth(), outputSize.getWidth(),
outputSize.getHeight(), outputSize.getHeight(),

View File

@ -28,10 +28,9 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** /**
* Robolectric tests for {@link FrameProcessorChain}. * Tests for creating and configuring a {@link FrameProcessorChain}.
* *
* <p>See {@code FrameProcessorChainPixelTest} in the androidTest directory for instrumentation * <p>See {@link FrameProcessorChainPixelTest} for data processing tests.
* tests.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class FrameProcessorChainTest { public final class FrameProcessorChainTest {

View File

@ -54,7 +54,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* 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 #getPendingFrameCount()} can be used to check whether there are frames that have not been
* fully processed yet. Output is written to its {@linkplain #configure(Surface, int, int, * fully processed yet. Output is written to its {@linkplain #setOutputSurface(Surface, int, int,
* SurfaceView) output surface}. * SurfaceView) output surface}.
*/ */
/* package */ final class FrameProcessorChain { /* package */ final class FrameProcessorChain {
@ -73,7 +73,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame. * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame.
* @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. * @return A new instance.
* @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1. * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1, reading shader
* files fails, or an OpenGL error occurs while creating and configuring the OpenGL
* components.
*/ */
public static FrameProcessorChain create( public static FrameProcessorChain create(
Context context, Context context,
@ -93,42 +95,104 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
TransformationException.ERROR_CODE_GL_INIT_FAILED); TransformationException.ERROR_CODE_GL_INIT_FAILED);
} }
ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
ExternalCopyFrameProcessor externalCopyFrameProcessor = ExternalCopyFrameProcessor externalCopyFrameProcessor =
new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing);
externalCopyFrameProcessor.setInputSize(inputWidth, inputHeight);
Size inputSize = externalCopyFrameProcessor.getOutputSize(); try {
for (int i = 0; i < frameProcessors.size(); i++) { return singleThreadExecutorService
frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); .submit(
inputSize = frameProcessors.get(i).getOutputSize(); () ->
createOpenGlObjectsAndFrameProcessorChain(
inputWidth,
inputHeight,
frameProcessors,
enableExperimentalHdrEditing,
singleThreadExecutorService,
externalCopyFrameProcessor))
.get();
} catch (ExecutionException e) {
throw TransformationException.createForFrameProcessorChain(
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw TransformationException.createForFrameProcessorChain(
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
}
} }
/**
* Creates the OpenGL textures, framebuffers, initializes the {@link GlFrameProcessor
* GlFrameProcessors} and returns a new {@code FrameProcessorChain}.
*
* <p>This method must by executed using the {@code singleThreadExecutorService}.
*/
private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain(
int inputWidth,
int inputHeight,
List<GlFrameProcessor> frameProcessors,
boolean enableExperimentalHdrEditing,
ExecutorService singleThreadExecutorService,
ExternalCopyFrameProcessor externalCopyFrameProcessor)
throws IOException {
checkState(Thread.currentThread().getName().equals(THREAD_NAME));
EGLDisplay eglDisplay = GlUtil.createEglDisplay();
EGLContext eglContext =
enableExperimentalHdrEditing
? GlUtil.createEglContextEs3Rgba1010102(eglDisplay)
: GlUtil.createEglContext(eglDisplay);
if (GlUtil.isSurfacelessContextExtensionSupported()) {
GlUtil.focusEglSurface(
eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1);
} else if (enableExperimentalHdrEditing) {
// TODO(b/209404935): Don't assume BT.2020 PQ input/output.
GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay);
} else {
GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay);
}
int inputExternalTexId = GlUtil.createExternalTexture();
externalCopyFrameProcessor.setInputSize(inputWidth, inputHeight);
externalCopyFrameProcessor.initialize(inputExternalTexId);
int[] framebuffers = new int[frameProcessors.size()];
Size inputSize = externalCopyFrameProcessor.getOutputSize();
for (int i = 0; i < frameProcessors.size(); i++) {
int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight());
framebuffers[i] = GlUtil.createFboForTexture(inputTexId);
frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight());
frameProcessors.get(i).initialize(inputTexId);
inputSize = frameProcessors.get(i).getOutputSize();
}
return new FrameProcessorChain( return new FrameProcessorChain(
externalCopyFrameProcessor, frameProcessors, enableExperimentalHdrEditing); eglDisplay,
eglContext,
singleThreadExecutorService,
inputExternalTexId,
externalCopyFrameProcessor,
framebuffers,
ImmutableList.copyOf(frameProcessors),
enableExperimentalHdrEditing);
} }
private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; private static final String THREAD_NAME = "Transformer:FrameProcessorChain";
private final boolean enableExperimentalHdrEditing; private final boolean enableExperimentalHdrEditing;
private @MonotonicNonNull EGLDisplay eglDisplay; private final EGLDisplay eglDisplay;
private @MonotonicNonNull EGLContext eglContext; private final EGLContext eglContext;
/** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */ /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */
private final ExecutorService singleThreadExecutorService; private final ExecutorService singleThreadExecutorService;
/** Futures corresponding to the executor service's pending tasks. */ /** Futures corresponding to the executor service's pending tasks. */
private final ConcurrentLinkedQueue<Future<?>> futures; private final ConcurrentLinkedQueue<Future<?>> futures;
/** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */ /** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */
private final AtomicInteger pendingFrameCount; private final AtomicInteger pendingFrameCount;
/** Prevents further frame processing tasks from being scheduled after {@link #release()}. */
private volatile boolean releaseRequested;
private boolean inputStreamEnded;
/** Wraps the {@link #inputSurfaceTexture}. */ /** Wraps the {@link #inputSurfaceTexture}. */
private @MonotonicNonNull Surface inputSurface; private final Surface inputSurface;
/** Associated with an OpenGL external texture. */ /** Associated with an OpenGL external texture. */
private @MonotonicNonNull SurfaceTexture inputSurfaceTexture; private final SurfaceTexture inputSurfaceTexture;
/**
* Identifier of the external texture the {@link ExternalCopyFrameProcessor} reads its input from.
*/
private int inputExternalTexId;
/** Transformation matrix associated with the {@link #inputSurfaceTexture}. */ /** Transformation matrix associated with the {@link #inputSurfaceTexture}. */
private final float[] textureTransformMatrix; private final float[] textureTransformMatrix;
@ -158,19 +222,33 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
*/ */
private @MonotonicNonNull EGLSurface debugPreviewEglSurface; private @MonotonicNonNull EGLSurface debugPreviewEglSurface;
private boolean inputStreamEnded;
/** Prevents further frame processing tasks from being scheduled after {@link #release()}. */
private volatile boolean releaseRequested;
private FrameProcessorChain( private FrameProcessorChain(
EGLDisplay eglDisplay,
EGLContext eglContext,
ExecutorService singleThreadExecutorService,
int inputExternalTexId,
ExternalCopyFrameProcessor externalCopyFrameProcessor, ExternalCopyFrameProcessor externalCopyFrameProcessor,
List<GlFrameProcessor> frameProcessors, int[] framebuffers,
ImmutableList<GlFrameProcessor> frameProcessors,
boolean enableExperimentalHdrEditing) { boolean enableExperimentalHdrEditing) {
this.eglDisplay = eglDisplay;
this.eglContext = eglContext;
this.singleThreadExecutorService = singleThreadExecutorService;
this.externalCopyFrameProcessor = externalCopyFrameProcessor; this.externalCopyFrameProcessor = externalCopyFrameProcessor;
this.frameProcessors = ImmutableList.copyOf(frameProcessors); this.framebuffers = framebuffers;
this.frameProcessors = frameProcessors;
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
futures = new ConcurrentLinkedQueue<>(); futures = new ConcurrentLinkedQueue<>();
pendingFrameCount = new AtomicInteger(); pendingFrameCount = new AtomicInteger();
inputSurfaceTexture = new SurfaceTexture(inputExternalTexId);
inputSurface = new Surface(inputSurfaceTexture);
textureTransformMatrix = new float[16]; textureTransformMatrix = new float[16];
framebuffers = new int[frameProcessors.size()];
outputSize = outputSize =
frameProcessors.isEmpty() frameProcessors.isEmpty()
? externalCopyFrameProcessor.getOutputSize() ? externalCopyFrameProcessor.getOutputSize()
@ -185,26 +263,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
/** /**
* Configures the {@code FrameProcessorChain} to process frames to the specified output targets. * Sets the output {@link Surface}.
* *
* <p>This method may only be called once and may override the {@linkplain * <p>This method may override the output size of the final {@link GlFrameProcessor}.
* GlFrameProcessor#setInputSize(int, int) output size} of the final {@link GlFrameProcessor}.
* *
* @param outputSurface The output {@link Surface}. * @param outputSurface The output {@link Surface}.
* @param outputWidth The output width, in pixels. * @param outputWidth The output width, in pixels.
* @param outputHeight The output height, in pixels. * @param outputHeight The output height, in pixels.
* @param debugSurfaceView Optional debug {@link SurfaceView} to show output. * @param debugSurfaceView Optional debug {@link SurfaceView} to show output.
* @throws IllegalStateException If the {@code FrameProcessorChain} has already been configured.
* @throws TransformationException If reading shader files fails, or an OpenGL error occurs while
* creating and configuring the OpenGL components.
*/ */
public void configure( public void setOutputSurface(
Surface outputSurface, Surface outputSurface,
int outputWidth, int outputWidth,
int outputHeight, int outputHeight,
@Nullable SurfaceView debugSurfaceView) @Nullable SurfaceView debugSurfaceView) {
throws TransformationException {
checkState(inputSurface == null, "The FrameProcessorChain has already been configured.");
// TODO(b/218488308): Don't override output size for encoder fallback. Instead allow the final // TODO(b/218488308): Don't override output size for encoder fallback. Instead allow the final
// GlFrameProcessor to be re-configured or append another GlFrameProcessor. // GlFrameProcessor to be re-configured or append another GlFrameProcessor.
outputSize = new Size(outputWidth, outputHeight); outputSize = new Size(outputWidth, outputHeight);
@ -214,21 +286,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
debugPreviewHeight = debugSurfaceView.getHeight(); debugPreviewHeight = debugSurfaceView.getHeight();
} }
try { futures.add(
// Wait for task to finish to be able to use inputExternalTexId to create the SurfaceTexture. singleThreadExecutorService.submit(
singleThreadExecutorService () -> createOpenGlSurfaces(outputSurface, debugSurfaceView)));
.submit(this::createOpenGlObjectsAndInitializeFrameProcessors)
.get();
} catch (ExecutionException e) {
throw TransformationException.createForFrameProcessorChain(
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw TransformationException.createForFrameProcessorChain(
e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
}
inputSurfaceTexture = new SurfaceTexture(inputExternalTexId);
inputSurfaceTexture.setOnFrameAvailableListener( inputSurfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> { surfaceTexture -> {
if (releaseRequested) { if (releaseRequested) {
@ -244,21 +305,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
}); });
inputSurface = new Surface(inputSurfaceTexture);
futures.add(
singleThreadExecutorService.submit(
() -> createOpenGlSurfaces(outputSurface, debugSurfaceView)));
} }
/** /** Returns the input {@link Surface}. */
* Returns the input {@link Surface}.
*
* <p>The {@code FrameProcessorChain} must be {@linkplain #configure(Surface, int, int,
* SurfaceView) configured}.
*/
public Surface getInputSurface() { public Surface getInputSurface() {
checkStateNotNull(inputSurface, "The FrameProcessorChain must be configured.");
return inputSurface; return inputSurface;
} }
@ -347,9 +397,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* Creates the OpenGL surfaces. * Creates the OpenGL surfaces.
* *
* <p>This method should only be called after {@link * <p>This method should only be called on the {@linkplain #THREAD_NAME background thread}.
* #createOpenGlObjectsAndInitializeFrameProcessors()} and must be called on the background
* thread.
*/ */
private void createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView) { private void createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView) {
checkState(Thread.currentThread().getName().equals(THREAD_NAME)); checkState(Thread.currentThread().getName().equals(THREAD_NAME));
@ -371,57 +419,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
/**
* Creates the OpenGL textures and framebuffers, and initializes the {@link GlFrameProcessor
* GlFrameProcessors}.
*
* <p>This method should only be called on the background thread.
*/
private Void createOpenGlObjectsAndInitializeFrameProcessors() throws IOException {
checkState(Thread.currentThread().getName().equals(THREAD_NAME));
eglDisplay = GlUtil.createEglDisplay();
eglContext =
enableExperimentalHdrEditing
? GlUtil.createEglContextEs3Rgba1010102(eglDisplay)
: GlUtil.createEglContext(eglDisplay);
if (GlUtil.isSurfacelessContextExtensionSupported()) {
GlUtil.focusEglSurface(
eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1);
} else if (enableExperimentalHdrEditing) {
// TODO(b/209404935): Don't assume BT.2020 PQ input/output.
GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay);
} else {
GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay);
}
inputExternalTexId = GlUtil.createExternalTexture();
externalCopyFrameProcessor.initialize(inputExternalTexId);
Size intermediateSize = externalCopyFrameProcessor.getOutputSize();
for (int i = 0; i < frameProcessors.size(); i++) {
int inputTexId =
GlUtil.createTexture(intermediateSize.getWidth(), intermediateSize.getHeight());
framebuffers[i] = GlUtil.createFboForTexture(inputTexId);
frameProcessors.get(i).initialize(inputTexId);
intermediateSize = frameProcessors.get(i).getOutputSize();
}
// Return something because only Callables not Runnables can throw checked exceptions.
return null;
}
/** /**
* Processes an input frame. * Processes an input frame.
* *
* <p>This method should only be called on the background thread. * <p>This method should only be called on the {@linkplain #THREAD_NAME background thread}.
*/ */
@RequiresNonNull("inputSurfaceTexture") @RequiresNonNull("inputSurfaceTexture")
private void processFrame() { private void processFrame() {
checkState(Thread.currentThread().getName().equals(THREAD_NAME)); checkState(Thread.currentThread().getName().equals(THREAD_NAME));
checkStateNotNull(eglSurface); checkStateNotNull(eglSurface, "No output surface set.");
checkStateNotNull(eglContext);
checkStateNotNull(eglDisplay);
if (frameProcessors.isEmpty()) { if (frameProcessors.isEmpty()) {
GlUtil.focusEglSurface( GlUtil.focusEglSurface(

View File

@ -116,7 +116,7 @@ import org.checkerframework.dataflow.qual.Pure;
requestedEncoderFormat, requestedEncoderFormat,
encoderSupportedFormat)); encoderSupportedFormat));
frameProcessorChain.configure( frameProcessorChain.setOutputSurface(
/* outputSurface= */ encoder.getInputSurface(), /* outputSurface= */ encoder.getInputSurface(),
/* outputWidth= */ encoderSupportedFormat.width, /* outputWidth= */ encoderSupportedFormat.width,
/* outputHeight= */ encoderSupportedFormat.height, /* outputHeight= */ encoderSupportedFormat.height,

View File

@ -114,6 +114,8 @@ public class DefaultEncoderFactoryTest {
@Test @Test
public void createForVideoEncoding_withNoSupportedEncoder_throws() { public void createForVideoEncoding_withNoSupportedEncoder_throws() {
Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30);
TransformationException exception =
assertThrows( assertThrows(
TransformationException.class, TransformationException.class,
() -> () ->
@ -121,6 +123,10 @@ public class DefaultEncoderFactoryTest {
.createForVideoEncoding( .createForVideoEncoding(
requestedVideoFormat, requestedVideoFormat,
/* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H265))); /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H265)));
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception.errorCode)
.isEqualTo(TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED);
} }
@Test @Test

View File

@ -30,7 +30,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import android.media.MediaCodecInfo;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Handler; import android.os.Handler;
@ -405,26 +404,6 @@ public final class TransformerEndToEndTest {
.isEqualTo(TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); .isEqualTo(TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED);
} }
@Test
public void startTransformation_withVideoEncoderFormatUnsupported_completesWithError()
throws Exception {
Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false)
.setTransformationRequest(
new TransformationRequest.Builder()
.setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type
.build())
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY);
transformer.startTransformation(mediaItem, outputPath);
TransformationException exception = TransformerTestRunner.runUntilError(transformer);
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception.errorCode)
.isEqualTo(TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED);
}
@Test @Test
public void startTransformation_withIoError_completesWithError() throws Exception { public void startTransformation_withIoError_completesWithError() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build();
@ -801,11 +780,6 @@ public final class TransformerEndToEndTest {
throwingCodecConfig, throwingCodecConfig,
/* colorFormats= */ ImmutableList.of(), /* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ true); /* isDecoder= */ true);
addCodec(
MimeTypes.VIDEO_H263,
throwingCodecConfig,
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible),
/* isDecoder= */ false);
addCodec( addCodec(
MimeTypes.AUDIO_AMR_NB, MimeTypes.AUDIO_AMR_NB,
throwingCodecConfig, throwingCodecConfig,