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 40e63b320d..f3dc89c94d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java @@ -31,12 +31,18 @@ import android.view.Surface; import android.view.SurfaceView; import androidx.annotation.Nullable; import androidx.media3.common.C; -import androidx.media3.common.util.GlProgram; import androidx.media3.common.util.GlUtil; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; -/** FrameEditor applies changes to individual video frames. */ +/** + * {@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, Matrix, Surface, boolean, + * Transformer.DebugViewProvider) output surface}. + */ /* package */ final class FrameEditor { static { @@ -79,6 +85,8 @@ import java.util.concurrent.atomic.AtomicInteger; TransformationException.ERROR_CODE_GL_INIT_FAILED); } + GlFrameProcessor frameProcessor = + new GlFrameProcessor(context, transformationMatrix, enableExperimentalHdrEditing); @Nullable SurfaceView debugSurfaceView = debugViewProvider.getDebugPreviewSurfaceView(outputWidth, outputHeight); @@ -87,7 +95,6 @@ import java.util.concurrent.atomic.AtomicInteger; EGLContext eglContext; EGLSurface eglSurface; int textureId; - GlProgram glProgram; @Nullable EGLSurface debugPreviewEglSurface = null; try { eglDisplay = GlUtil.createEglDisplay(); @@ -111,9 +118,7 @@ import java.util.concurrent.atomic.AtomicInteger; GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); textureId = GlUtil.createExternalTexture(); - glProgram = - configureGlProgram( - context, transformationMatrix, textureId, enableExperimentalHdrEditing); + frameProcessor.initialize(); } catch (IOException | GlUtil.GlException e) { throw TransformationException.createForFrameEditor( e, TransformationException.ERROR_CODE_GL_INIT_FAILED); @@ -134,7 +139,7 @@ import java.util.concurrent.atomic.AtomicInteger; eglContext, eglSurface, textureId, - glProgram, + frameProcessor, outputWidth, outputHeight, debugPreviewEglSurface, @@ -142,97 +147,7 @@ import java.util.concurrent.atomic.AtomicInteger; debugPreviewHeight); } - private static GlProgram configureGlProgram( - Context context, - Matrix transformationMatrix, - int textureId, - boolean enableExperimentalHdrEditing) - throws IOException { - // TODO(b/205002913): check the loaded program is consistent with the attributes - // and uniforms expected in the code. - String vertexShaderFilePath = - enableExperimentalHdrEditing - ? VERTEX_SHADER_TRANSFORMATION_ES3_PATH - : VERTEX_SHADER_TRANSFORMATION_PATH; - String fragmentShaderFilePath = - enableExperimentalHdrEditing - ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH - : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; - GlProgram glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); - - // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. - glProgram.setBufferAttribute( - "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); - glProgram.setBufferAttribute( - "aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); - glProgram.setSamplerTexIdUniform("uTexSampler", textureId, /* unit= */ 0); - - if (enableExperimentalHdrEditing) { - // In HDR editing mode the decoder output is sampled in YUV. - glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); - } - - float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix); - glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrixArray); - return glProgram; - } - - /** - * Returns a 4x4, column-major Matrix float array, from an input {@link Matrix}. This is useful - * for converting to the 4x4 column-major format commonly used in OpenGL. - */ - private static float[] getGlMatrixArray(Matrix matrix) { - float[] matrix3x3Array = new float[9]; - matrix.getValues(matrix3x3Array); - float[] matrix4x4Array = getMatrix4x4Array(matrix3x3Array); - - // Transpose from row-major to column-major representations. - float[] transposedMatrix4x4Array = new float[16]; - android.opengl.Matrix.transposeM( - transposedMatrix4x4Array, /* mTransOffset= */ 0, matrix4x4Array, /* mOffset= */ 0); - - return transposedMatrix4x4Array; - } - - /** - * Returns a 4x4 matrix array containing the 3x3 matrix array's contents. - * - *
The 3x3 matrix array is expected to be in 2 dimensions, and the 4x4 matrix array is expected
- * to be in 3 dimensions. The output will have the third row/column's values be an identity
- * matrix's values, so that vertex transformations using this matrix will not affect the z axis.
- *
- * Input format: [a, b, c, d, e, f, g, h, i]
- * Output format: [a, b, 0, c, d, e, 0, f, 0, 0, 1, 0, g, h, 0, i]
- */
- private static float[] getMatrix4x4Array(float[] matrix3x3Array) {
- float[] matrix4x4Array = new float[16];
- matrix4x4Array[10] = 1;
- for (int inputRow = 0; inputRow < 3; inputRow++) {
- for (int inputColumn = 0; inputColumn < 3; inputColumn++) {
- int outputRow = (inputRow == 2) ? 3 : inputRow;
- int outputColumn = (inputColumn == 2) ? 3 : inputColumn;
- matrix4x4Array[outputRow * 4 + outputColumn] = matrix3x3Array[inputRow * 3 + inputColumn];
- }
- }
- return matrix4x4Array;
- }
-
- private static final String VERTEX_SHADER_TRANSFORMATION_PATH =
- "shaders/vertex_shader_transformation.glsl";
- private static final String FRAGMENT_SHADER_COPY_EXTERNAL_PATH =
- "shaders/fragment_shader_copy_external.glsl";
- private static final String VERTEX_SHADER_TRANSFORMATION_ES3_PATH =
- "shaders/vertex_shader_transformation_es3.glsl";
- private static final String FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH =
- "shaders/fragment_shader_copy_external_yuv_es3.glsl";
- // Color transform coefficients from
- // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/colorconversion/ColorConverter.cpp;l=668-670;drc=487adf977a50cac3929eba15fad0d0f461c7ff0f.
- private static final float[] MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM = {
- 1.168f, 1.168f, 1.168f,
- 0.0f, -0.188f, 2.148f,
- 1.683f, -0.652f, 0.0f,
- };
-
+ private final GlFrameProcessor frameProcessor;
private final float[] textureTransformMatrix;
private final EGLDisplay eglDisplay;
private final EGLContext eglContext;
@@ -242,7 +157,6 @@ import java.util.concurrent.atomic.AtomicInteger;
private final AtomicInteger availableInputFrameCount;
private final SurfaceTexture inputSurfaceTexture;
private final Surface inputSurface;
- private final GlProgram glProgram;
private final int outputWidth;
private final int outputHeight;
@Nullable private final EGLSurface debugPreviewEglSurface;
@@ -256,7 +170,7 @@ import java.util.concurrent.atomic.AtomicInteger;
EGLContext eglContext,
EGLSurface eglSurface,
int textureId,
- GlProgram glProgram,
+ GlFrameProcessor frameProcessor,
int outputWidth,
int outputHeight,
@Nullable EGLSurface debugPreviewEglSurface,
@@ -266,14 +180,14 @@ import java.util.concurrent.atomic.AtomicInteger;
this.eglContext = eglContext;
this.eglSurface = eglSurface;
this.textureId = textureId;
- this.glProgram = glProgram;
- this.pendingInputFrameCount = new AtomicInteger();
- this.availableInputFrameCount = new AtomicInteger();
+ this.frameProcessor = frameProcessor;
this.outputWidth = outputWidth;
this.outputHeight = outputHeight;
this.debugPreviewEglSurface = debugPreviewEglSurface;
this.debugPreviewWidth = debugPreviewWidth;
this.debugPreviewHeight = debugPreviewHeight;
+ pendingInputFrameCount = new AtomicInteger();
+ availableInputFrameCount = new AtomicInteger();
textureTransformMatrix = new float[16];
inputSurfaceTexture = new SurfaceTexture(textureId);
inputSurfaceTexture.setOnFrameAvailableListener(
@@ -321,16 +235,20 @@ import java.util.concurrent.atomic.AtomicInteger;
try {
inputSurfaceTexture.updateTexImage();
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
- glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix);
- glProgram.bindAttributesAndUniforms();
-
- focusAndDrawQuad(eglSurface, outputWidth, outputHeight);
+ GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
+ frameProcessor.setTextureTransformMatrix(textureTransformMatrix);
+ frameProcessor.updateProgramAndDraw(textureId);
long presentationTimeNs = inputSurfaceTexture.getTimestamp();
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs);
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
if (debugPreviewEglSurface != null) {
- focusAndDrawQuad(debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight);
+ 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);
}
} catch (GlUtil.GlException e) {
@@ -342,22 +260,13 @@ import java.util.concurrent.atomic.AtomicInteger;
/** Releases all resources. */
public void release() {
- glProgram.delete();
+ frameProcessor.release();
GlUtil.deleteTexture(textureId);
GlUtil.destroyEglContext(eglDisplay, eglContext);
inputSurfaceTexture.release();
inputSurface.release();
}
- /** Focuses the specified surface with the specified width and height, then draws a quad. */
- private void focusAndDrawQuad(EGLSurface eglSurface, int width, int height) {
- GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height);
- 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);
- }
-
/** Returns whether all data has been processed. */
public boolean isEnded() {
return inputStreamEnded
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java
new file mode 100644
index 0000000000..55bd315111
--- /dev/null
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java
@@ -0,0 +1,181 @@
+/*
+ * 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.checkStateNotNull;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.opengl.GLES20;
+import androidx.media3.common.util.GlProgram;
+import androidx.media3.common.util.GlUtil;
+import java.io.IOException;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** Manages a GLSL shader program for applying a transformation matrix to a frame. */
+/* package */ class GlFrameProcessor {
+
+ static {
+ GlUtil.glAssertionsEnabled = true;
+ }
+
+ /**
+ * Returns a 4x4, column-major Matrix float array, from an input {@link Matrix}. This is useful
+ * for converting to the 4x4 column-major format commonly used in OpenGL.
+ */
+ private static float[] getGlMatrixArray(Matrix matrix) {
+ float[] matrix3x3Array = new float[9];
+ matrix.getValues(matrix3x3Array);
+ float[] matrix4x4Array = getMatrix4x4Array(matrix3x3Array);
+
+ // Transpose from row-major to column-major representations.
+ float[] transposedMatrix4x4Array = new float[16];
+ android.opengl.Matrix.transposeM(
+ transposedMatrix4x4Array, /* mTransOffset= */ 0, matrix4x4Array, /* mOffset= */ 0);
+
+ return transposedMatrix4x4Array;
+ }
+
+ /**
+ * Returns a 4x4 matrix array containing the 3x3 matrix array's contents.
+ *
+ *
The 3x3 matrix array is expected to be in 2 dimensions, and the 4x4 matrix array is expected
+ * to be in 3 dimensions. The output will have the third row/column's values be an identity
+ * matrix's values, so that vertex transformations using this matrix will not affect the z axis.
+ *
+ * Input format: [a, b, c, d, e, f, g, h, i]
+ * Output format: [a, b, 0, c, d, e, 0, f, 0, 0, 1, 0, g, h, 0, i]
+ */
+ private static float[] getMatrix4x4Array(float[] matrix3x3Array) {
+ float[] matrix4x4Array = new float[16];
+ matrix4x4Array[10] = 1;
+ for (int inputRow = 0; inputRow < 3; inputRow++) {
+ for (int inputColumn = 0; inputColumn < 3; inputColumn++) {
+ int outputRow = (inputRow == 2) ? 3 : inputRow;
+ int outputColumn = (inputColumn == 2) ? 3 : inputColumn;
+ matrix4x4Array[outputRow * 4 + outputColumn] = matrix3x3Array[inputRow * 3 + inputColumn];
+ }
+ }
+ return matrix4x4Array;
+ }
+
+ private static final String VERTEX_SHADER_TRANSFORMATION_PATH =
+ "shaders/vertex_shader_transformation.glsl";
+ private static final String FRAGMENT_SHADER_COPY_EXTERNAL_PATH =
+ "shaders/fragment_shader_copy_external.glsl";
+ private static final String VERTEX_SHADER_TRANSFORMATION_ES3_PATH =
+ "shaders/vertex_shader_transformation_es3.glsl";
+ private static final String FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH =
+ "shaders/fragment_shader_copy_external_yuv_es3.glsl";
+ // Color transform coefficients from
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/colorconversion/ColorConverter.cpp;l=668-670;drc=487adf977a50cac3929eba15fad0d0f461c7ff0f.
+ private static final float[] MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM = {
+ 1.168f, 1.168f, 1.168f,
+ 0.0f, -0.188f, 2.148f,
+ 1.683f, -0.652f, 0.0f,
+ };
+
+ private final Context context;
+ private final Matrix transformationMatrix;
+ private final boolean enableExperimentalHdrEditing;
+
+ private @MonotonicNonNull GlProgram glProgram;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context A {@link Context}.
+ * @param transformationMatrix The transformation matrix to apply to each frame.
+ * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal.
+ */
+ public GlFrameProcessor(
+ Context context, Matrix transformationMatrix, boolean enableExperimentalHdrEditing) {
+ this.context = context;
+ this.transformationMatrix = transformationMatrix;
+ this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
+ }
+
+ /**
+ * Does any initialization necessary such as loading and compiling a GLSL shader programs.
+ *
+ *
This method may only be called after creating the OpenGL context and focusing a render + * target. + */ + public void initialize() throws IOException { + // TODO(b/205002913): check the loaded program is consistent with the attributes + // and uniforms expected in the code. + String vertexShaderFilePath = + enableExperimentalHdrEditing + ? VERTEX_SHADER_TRANSFORMATION_ES3_PATH + : VERTEX_SHADER_TRANSFORMATION_PATH; + String fragmentShaderFilePath = + enableExperimentalHdrEditing + ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH + : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; + + glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. + glProgram.setBufferAttribute( + "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + glProgram.setBufferAttribute( + "aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + if (enableExperimentalHdrEditing) { + // In HDR editing mode the decoder output is sampled in YUV. + glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); + } + float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix); + glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrixArray); + } + + /** + * Sets the texture transform matrix for converting an external surface texture's coordinates to + * sampling locations. + * + * @param textureTransformMatrix The external surface texture's {@link + * android.graphics.SurfaceTexture#getTransformMatrix(float[]) transform matrix}. + */ + public void setTextureTransformMatrix(float[] textureTransformMatrix) { + checkStateNotNull(glProgram); + glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); + } + + /** + * Updates the shader program's vertex attributes and uniforms, binds them, and draws. + * + *
The frame processor must be {@link #initialize() initialized}. The caller is responsible for + * focussing the correct render target before calling this method. + * + * @param inputTexId The identifier of an OpenGL texture that the fragment shader can sample from. + */ + // TODO(b/214975934): Also pass presentationTimeNs. + public void updateProgramAndDraw(int inputTexId) { + checkStateNotNull(glProgram); + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* unit= */ 0); + glProgram.use(); + glProgram.bindAttributesAndUniforms(); + 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); + } + + /** Releases all resources. */ + public void release() { + if (glProgram != null) { + glProgram.delete(); + } + } +}