diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java index 20dc9912a2..ccbb5928b0 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java @@ -44,7 +44,7 @@ import org.junit.Test; import org.junit.runner.RunWith; /** - * Pixel test for frame processing via {@link FrameEditor#processData()}. + * Pixel test for frame processing via {@link FrameEditor}. * *
Expected images are taken from an emulator, so tests on different emulators or physical * devices may fail. To test on other devices, please increase the {@link @@ -61,8 +61,11 @@ public final class FrameEditorDataProcessingTest { private static final String INPUT_MP4_ASSET_STRING = "media/mp4/sample.mp4"; /** Timeout for dequeueing buffers from the codec, in microseconds. */ private static final int DEQUEUE_TIMEOUT_US = 5_000_000; - /** Time to wait for the frame editor's input to be populated by the decoder, in milliseconds. */ - private static final int SURFACE_WAIT_MS = 1000; + /** + * Time to wait for the decoded frame to populate the frame editor's input surface and the frame + * editor to finish processing the frame, in milliseconds. + */ + private static final int FRAME_PROCESSING_WAIT_MS = 1000; /** The ratio of width over height, for each pixel in a frame. */ private static final float PIXEL_WIDTH_HEIGHT_RATIO = 1; @@ -84,10 +87,7 @@ public final class FrameEditorDataProcessingTest { setUpAndPrepareFirstFrame(identityMatrix); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editorOutputImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editorOutputImage); - editorOutputImage.close(); + Bitmap actualBitmap = processFirstFrameAndEnd(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = @@ -107,10 +107,7 @@ public final class FrameEditorDataProcessingTest { Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editorOutputImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editorOutputImage); - editorOutputImage.close(); + Bitmap actualBitmap = processFirstFrameAndEnd(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = @@ -130,10 +127,7 @@ public final class FrameEditorDataProcessingTest { Bitmap expectedBitmap = BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editorOutputImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editorOutputImage); - editorOutputImage.close(); + Bitmap actualBitmap = processFirstFrameAndEnd(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = @@ -155,10 +149,7 @@ public final class FrameEditorDataProcessingTest { setUpAndPrepareFirstFrame(rotate90Matrix); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editorOutputImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editorOutputImage); - editorOutputImage.close(); + Bitmap actualBitmap = processFirstFrameAndEnd(); // TODO(b/207848601): switch to using proper tooling for testing against golden data. float averagePixelAbsoluteDifference = @@ -205,7 +196,7 @@ public final class FrameEditorDataProcessingTest { String mimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME)); mediaCodec = MediaCodec.createDecoderByType(mimeType); mediaCodec.configure( - mediaFormat, frameEditor.getInputSurface(), /* crypto= */ null, /* flags= */ 0); + mediaFormat, frameEditor.createInputSurface(), /* crypto= */ null, /* flags= */ 0); mediaCodec.start(); int inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); @@ -237,10 +228,6 @@ public final class FrameEditorDataProcessingTest { } while (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); - - // Sleep to give time for the surface texture to be populated. - Thread.sleep(SURFACE_WAIT_MS); - assertThat(frameEditor.canProcessData()).isTrue(); } finally { mediaExtractor.release(); if (mediaCodec != null) { @@ -248,4 +235,16 @@ public final class FrameEditorDataProcessingTest { } } } + + private Bitmap processFirstFrameAndEnd() throws InterruptedException, TransformationException { + checkNotNull(frameEditor).signalEndOfInputStream(); + Thread.sleep(FRAME_PROCESSING_WAIT_MS); + assertThat(frameEditor.isEnded()).isTrue(); + frameEditor.getAndRethrowBackgroundExceptions(); + + Image editorOutputImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); + Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(editorOutputImage); + editorOutputImage.close(); + return actualBitmap; + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java index 67008fe3a6..7b00b1351b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java @@ -31,16 +31,26 @@ import android.view.SurfaceView; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.Util; import java.io.IOException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * {@code FrameEditor} applies changes to individual video frames. * - *
Input becomes available on its {@link #getInputSurface() input surface} asynchronously so - * {@link #canProcessData()} needs to be checked before calling {@link #processData()}. Output is - * written to its {@link #create(Context, int, int, float, GlFrameProcessor, Surface, boolean, - * Transformer.DebugViewProvider) output surface}. + *
Input becomes available on its {@link #createInputSurface() input surface} asynchronously and
+ * is processed on a background thread as it becomes available. All input frames should be {@link
+ * #registerInputFrame() registered} before they are rendered to the input surface. {@link
+ * #hasPendingFrames()} can be used to check whether there are frames that have not been fully
+ * processed yet. Output is written to its {@link #create(Context, int, int, float,
+ * GlFrameProcessor, Surface, boolean, Transformer.DebugViewProvider) output surface}.
*/
/* package */ final class FrameEditor {
@@ -86,50 +96,9 @@ import java.util.concurrent.atomic.AtomicInteger;
TransformationException.ERROR_CODE_GL_INIT_FAILED);
}
- ExternalCopyFrameProcessor externalCopyFrameProcessor =
- new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing);
@Nullable
SurfaceView debugSurfaceView =
debugViewProvider.getDebugPreviewSurfaceView(outputWidth, outputHeight);
-
- EGLDisplay eglDisplay;
- EGLContext eglContext;
- EGLSurface eglSurface;
- int inputExternalTexId;
- int intermediateTexId;
- int frameBuffer;
- @Nullable EGLSurface debugPreviewEglSurface = null;
- try {
- eglDisplay = GlUtil.createEglDisplay();
-
- if (enableExperimentalHdrEditing) {
- eglContext = GlUtil.createEglContextEs3Rgba1010102(eglDisplay);
- // TODO(b/209404935): Don't assume BT.2020 PQ input/output.
- eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurface);
- if (debugSurfaceView != null) {
- debugPreviewEglSurface =
- GlUtil.getEglSurfaceBt2020Pq(eglDisplay, checkNotNull(debugSurfaceView.getHolder()));
- }
- } else {
- eglContext = GlUtil.createEglContext(eglDisplay);
- eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface);
- if (debugSurfaceView != null) {
- debugPreviewEglSurface =
- GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder()));
- }
- }
-
- GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
- inputExternalTexId = GlUtil.createExternalTexture();
- intermediateTexId = GlUtil.createTexture(outputWidth, outputHeight);
- frameBuffer = GlUtil.createFboForTexture(intermediateTexId);
- externalCopyFrameProcessor.initialize();
- transformationFrameProcessor.initialize();
- } catch (GlUtil.GlException | IOException e) {
- throw TransformationException.createForFrameEditor(
- e, TransformationException.ERROR_CODE_GL_INIT_FAILED);
- }
-
int debugPreviewWidth;
int debugPreviewHeight;
if (debugSurfaceView != null) {
@@ -140,15 +109,95 @@ import java.util.concurrent.atomic.AtomicInteger;
debugPreviewHeight = C.LENGTH_UNSET;
}
+ ExternalCopyFrameProcessor externalCopyFrameProcessor =
+ new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing);
+
+ ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME);
+ Future As the {@code FrameEditor} will call OpenGL commands on the {@code
+ * singleThreadExecutorService}'s thread, the OpenGL context and objects also need to be created
+ * on that thread. So this method should only be called on the {@code
+ * singleThreadExecutorService}'s thread.
+ */
+ private static FrameEditor createOpenGlObjectsAndFrameEditor(
+ ExecutorService singleThreadExecutorService,
+ ExternalCopyFrameProcessor externalCopyFrameProcessor,
+ GlFrameProcessor transformationFrameProcessor,
+ Surface outputSurface,
+ int outputWidth,
+ int outputHeight,
+ boolean enableExperimentalHdrEditing,
+ @Nullable SurfaceView debugSurfaceView,
+ int debugPreviewWidth,
+ int debugPreviewHeight)
+ throws IOException {
+ EGLDisplay eglDisplay = GlUtil.createEglDisplay();
+
+ final EGLContext eglContext;
+ final EGLSurface eglSurface;
+ @Nullable EGLSurface debugPreviewEglSurface = null;
+ if (enableExperimentalHdrEditing) {
+ eglContext = GlUtil.createEglContextEs3Rgba1010102(eglDisplay);
+ // TODO(b/209404935): Don't assume BT.2020 PQ input/output.
+ eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurface);
+ if (debugSurfaceView != null) {
+ debugPreviewEglSurface =
+ GlUtil.getEglSurfaceBt2020Pq(eglDisplay, checkNotNull(debugSurfaceView.getHolder()));
+ }
+ } else {
+ eglContext = GlUtil.createEglContext(eglDisplay);
+ eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface);
+ if (debugSurfaceView != null) {
+ debugPreviewEglSurface =
+ GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder()));
+ }
+ }
+
+ GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
+ int inputExternalTexId = GlUtil.createExternalTexture();
+ int intermediateTexId = GlUtil.createTexture(outputWidth, outputHeight);
+ int frameBuffer = GlUtil.createFboForTexture(intermediateTexId);
+ externalCopyFrameProcessor.initialize();
+ transformationFrameProcessor.initialize();
+
return new FrameEditor(
+ singleThreadExecutorService,
eglDisplay,
eglContext,
eglSurface,
+ externalCopyFrameProcessor,
+ transformationFrameProcessor,
inputExternalTexId,
intermediateTexId,
frameBuffer,
- externalCopyFrameProcessor,
- transformationFrameProcessor,
outputWidth,
outputHeight,
debugPreviewEglSurface,
@@ -156,17 +205,28 @@ import java.util.concurrent.atomic.AtomicInteger;
debugPreviewHeight);
}
+ private static final String THREAD_NAME = "Transformer:FrameEditor";
+
+ /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */
+ private final ExecutorService singleThreadExecutorService;
+ /** Futures corresponding to the executor service's pending tasks. */
+ private final ConcurrentLinkedQueue This method must not be called again after creating an input surface.
+ *
+ * @return The configured input {@link Surface}.
+ * @throws IllegalStateException If an input {@link Surface} has already been created.
+ */
+ public Surface createInputSurface() {
+ checkState(inputSurface == null, "The input surface has already been created.");
inputSurfaceTexture = new SurfaceTexture(inputExternalTexId);
inputSurfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> {
- checkState(pendingInputFrameCount.getAndDecrement() > 0);
- availableInputFrameCount.incrementAndGet();
+ if (releaseRequested) {
+ // Frames can still become available after a transformation is cancelled but they can be
+ // ignored.
+ return;
+ }
+ try {
+ futures.add(singleThreadExecutorService.submit(this::processFrame));
+ } catch (RejectedExecutionException e) {
+ if (!releaseRequested) {
+ throw e;
+ }
+ }
});
inputSurface = new Surface(inputSurfaceTexture);
- }
-
- /** Returns the input {@link Surface}. */
- public Surface getInputSurface() {
return inputSurface;
}
@@ -240,75 +321,104 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public void registerInputFrame() {
checkState(!inputStreamEnded);
- pendingInputFrameCount.incrementAndGet();
+ pendingFrameCount.incrementAndGet();
}
/**
- * Returns whether there is available input data that can be processed by calling {@link
- * #processData()}.
+ * Checks whether any exceptions occurred during asynchronous frame processing and rethrows the
+ * first exception encountered.
*/
- public boolean canProcessData() {
- return availableInputFrameCount.get() > 0;
- }
-
- /**
- * Processes an input frame.
- *
- * @throws TransformationException If an OpenGL error occurs while processing the data.
- * @throws IllegalStateException If there is no input data to process. Use {@link
- * #canProcessData()} to check whether input data is available.
- */
- public void processData() throws TransformationException {
- checkState(canProcessData());
- try {
- inputSurfaceTexture.updateTexImage();
- inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
- long presentationTimeNs = inputSurfaceTexture.getTimestamp();
-
- GlUtil.focusFramebuffer(
- eglDisplay, eglContext, eglSurface, frameBuffer, outputWidth, outputHeight);
- externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix);
- externalCopyFrameProcessor.updateProgramAndDraw(inputExternalTexId, presentationTimeNs);
-
- GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
- transformationFrameProcessor.updateProgramAndDraw(intermediateTexId, presentationTimeNs);
- EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs);
- EGL14.eglSwapBuffers(eglDisplay, eglSurface);
-
- if (debugPreviewEglSurface != null) {
- GlUtil.focusEglSurface(
- eglDisplay, eglContext, debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight);
- GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0);
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- // The four-vertex triangle strip forms a quad.
- GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
- EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface);
+ public void getAndRethrowBackgroundExceptions() throws TransformationException {
+ @Nullable Future> oldestGlProcessingFuture = futures.peek();
+ while (oldestGlProcessingFuture != null && oldestGlProcessingFuture.isDone()) {
+ futures.poll();
+ try {
+ oldestGlProcessingFuture.get();
+ } catch (ExecutionException e) {
+ throw TransformationException.createForFrameEditor(
+ e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw TransformationException.createForFrameEditor(
+ e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED);
}
- } catch (GlUtil.GlException e) {
- throw TransformationException.createForFrameEditor(
- e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED);
+ oldestGlProcessingFuture = futures.peek();
}
- availableInputFrameCount.decrementAndGet();
}
- /** Releases all resources. */
- public void release() {
- externalCopyFrameProcessor.release();
- transformationFrameProcessor.release();
- GlUtil.destroyEglContext(eglDisplay, eglContext);
- inputSurfaceTexture.release();
- inputSurface.release();
+ /**
+ * Returns whether there are input frames that have been {@link #registerInputFrame() registered}
+ * but not completely processed yet.
+ */
+ public boolean hasPendingFrames() {
+ return pendingFrameCount.get() > 0;
}
- /** Returns whether all data has been processed. */
+ /** Returns whether all frames have been processed. */
public boolean isEnded() {
- return inputStreamEnded
- && pendingInputFrameCount.get() == 0
- && availableInputFrameCount.get() == 0;
+ return inputStreamEnded && !hasPendingFrames();
}
- /** Informs the {@code FrameEditor} that no further input data should be accepted. */
+ /** Informs the {@code FrameEditor} that no further input frames should be accepted. */
public void signalEndOfInputStream() {
inputStreamEnded = true;
}
+
+ /**
+ * Releases all resources.
+ *
+ * If the frame editor is released before it has {@link #isEnded() ended}, it will attempt to
+ * cancel processing any input frames that have already become available. Input frames that become
+ * available after release are ignored.
+ */
+ public void release() {
+ releaseRequested = true;
+ while (!futures.isEmpty()) {
+ checkNotNull(futures.poll()).cancel(/* mayInterruptIfRunning= */ true);
+ }
+ futures.add(
+ singleThreadExecutorService.submit(
+ () -> {
+ externalCopyFrameProcessor.release();
+ transformationFrameProcessor.release();
+ GlUtil.destroyEglContext(eglDisplay, eglContext);
+ }));
+ if (inputSurfaceTexture != null) {
+ inputSurfaceTexture.release();
+ }
+ if (inputSurface != null) {
+ inputSurface.release();
+ }
+ singleThreadExecutorService.shutdown();
+ }
+
+ /** Processes an input frame. */
+ @RequiresNonNull("inputSurfaceTexture")
+ private void processFrame() {
+ inputSurfaceTexture.updateTexImage();
+ inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
+ long presentationTimeNs = inputSurfaceTexture.getTimestamp();
+
+ GlUtil.focusFramebuffer(
+ eglDisplay, eglContext, eglSurface, frameBuffer, outputWidth, outputHeight);
+ externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix);
+ externalCopyFrameProcessor.updateProgramAndDraw(inputExternalTexId, presentationTimeNs);
+
+ GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
+ transformationFrameProcessor.updateProgramAndDraw(intermediateTexId, presentationTimeNs);
+ EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs);
+ EGL14.eglSwapBuffers(eglDisplay, eglSurface);
+
+ if (debugPreviewEglSurface != null) {
+ GlUtil.focusEglSurface(
+ eglDisplay, eglContext, debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight);
+ GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ // The four-vertex triangle strip forms a quad.
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
+ EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface);
+ }
+
+ checkState(pendingFrameCount.getAndDecrement() > 0);
+ }
}
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 96fc4e12ea..070223d934 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
@@ -48,7 +48,7 @@ import org.checkerframework.dataflow.qual.Pure;
private final Codec encoder;
private final DecoderInputBuffer encoderOutputBuffer;
- private boolean waitingForFrameEditorInput;
+ private boolean signaledEndOfStreamToEncoder;
public VideoTranscodingSamplePipeline(
Context context,
@@ -177,7 +177,7 @@ import org.checkerframework.dataflow.qual.Pure;
decoder =
decoderFactory.createForVideoDecoding(
inputFormat,
- frameEditor == null ? encoder.getInputSurface() : frameEditor.getInputSurface());
+ frameEditor == null ? encoder.getInputSurface() : frameEditor.createInputSurface());
}
@Override
@@ -193,18 +193,39 @@ import org.checkerframework.dataflow.qual.Pure;
@Override
public boolean processData() throws TransformationException {
- if (hasProcessedAllInputData()) {
+ if (frameEditor != null) {
+ frameEditor.getAndRethrowBackgroundExceptions();
+ if (frameEditor.isEnded()) {
+ if (!signaledEndOfStreamToEncoder) {
+ encoder.signalEndOfInputStream();
+ signaledEndOfStreamToEncoder = true;
+ }
+ return false;
+ }
+ }
+ if (decoder.isEnded()) {
return false;
}
+ boolean canProcessMoreDataImmediately = false;
if (SDK_INT >= 29
&& !(("samsung".equals(Util.MANUFACTURER) || "OnePlus".equals(Util.MANUFACTURER))
&& SDK_INT < 31)) {
// TODO(b/213455700): Fix Samsung and OnePlus devices filling the decoder in processDataV29().
- return processDataV29();
+ processDataV29();
} else {
- return processDataDefault();
+ canProcessMoreDataImmediately = processDataDefault();
}
+ if (decoder.isEnded()) {
+ if (frameEditor != null) {
+ frameEditor.signalEndOfInputStream();
+ } else {
+ encoder.signalEndOfInputStream();
+ signaledEndOfStreamToEncoder = true;
+ return false;
+ }
+ }
+ return canProcessMoreDataImmediately;
}
/**
@@ -221,54 +242,22 @@ import org.checkerframework.dataflow.qual.Pure;
* Transformer}, using this method requires API level 29 or higher.
*/
@RequiresApi(29)
- private boolean processDataV29() throws TransformationException {
- if (frameEditor != null) {
- // Processes as many frames as possible. FrameEditor's output surface will block when it's
- // full, so there will be no frame drop and the surface will not grow out of bound.
- while (frameEditor.canProcessData()) {
- frameEditor.processData();
- }
- }
-
- while (decoder.getOutputBufferInfo() != null) {
- if (frameEditor != null) {
- frameEditor.registerInputFrame();
- }
- decoder.releaseOutputBuffer(/* render= */ true);
- }
- if (decoder.isEnded()) {
- signalEndOfInputStream();
- }
-
- return frameEditor != null && frameEditor.canProcessData();
+ private void processDataV29() throws TransformationException {
+ while (maybeProcessDecoderOutput()) {}
}
- /** Processes input data. */
+ /**
+ * Processes at most one input frame and returns whether a frame was processed.
+ *
+ * Only renders decoder output to the {@link FrameEditor}'s input surface if the {@link
+ * FrameEditor} has finished processing the previous frame.
+ */
private boolean processDataDefault() throws TransformationException {
- if (frameEditor != null) {
- if (frameEditor.canProcessData()) {
- waitingForFrameEditorInput = false;
- frameEditor.processData();
- return true;
- }
- if (waitingForFrameEditorInput) {
- return false;
- }
- }
-
- boolean decoderHasOutputBuffer = decoder.getOutputBufferInfo() != null;
- if (decoderHasOutputBuffer) {
- if (frameEditor != null) {
- frameEditor.registerInputFrame();
- waitingForFrameEditorInput = true;
- }
- decoder.releaseOutputBuffer(/* render= */ true);
- }
- if (decoder.isEnded()) {
- signalEndOfInputStream();
+ // TODO(b/214975934): Check whether this can be converted to a while-loop like processDataV29.
+ if (frameEditor != null && frameEditor.hasPendingFrames()) {
return false;
}
- return decoderHasOutputBuffer && !waitingForFrameEditorInput;
+ return maybeProcessDecoderOutput();
}
@Override
@@ -332,16 +321,21 @@ import org.checkerframework.dataflow.qual.Pure;
.build();
}
- private boolean hasProcessedAllInputData() {
- return decoder.isEnded() && (frameEditor == null || frameEditor.isEnded());
- }
+ /**
+ * Feeds at most one decoder output frame to the next step of the pipeline.
+ *
+ * @return Whether a frame was processed.
+ * @throws TransformationException If a problem occurs while processing the frame.
+ */
+ private boolean maybeProcessDecoderOutput() throws TransformationException {
+ if (decoder.getOutputBufferInfo() == null) {
+ return false;
+ }
- private void signalEndOfInputStream() throws TransformationException {
if (frameEditor != null) {
- frameEditor.signalEndOfInputStream();
- }
- if (frameEditor == null || frameEditor.isEnded()) {
- encoder.signalEndOfInputStream();
+ frameEditor.registerInputFrame();
}
+ decoder.releaseOutputBuffer(/* render= */ true);
+ return true;
}
}