Separate matrix effect specification and implementation.

This change splits AdvancedFrameProcessor into 4 files:
- MatrixTransformationFrameProcessor for the GlFrameProcessor
  implementation
- MatrixTransformation and GlMatrixTransformation for the GlEffect
  specification
- MatrixUtils for the static matrix helpers

PiperOrigin-RevId: 446236384
This commit is contained in:
hschlueter 2022-05-03 18:58:59 +01:00 committed by Ian Baker
parent 534cfc7968
commit c94035278c
12 changed files with 371 additions and 331 deletions

View File

@ -18,39 +18,38 @@ package androidx.media3.demo.transformer;
import android.graphics.Matrix; import android.graphics.Matrix;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.transformer.AdvancedFrameProcessor; import androidx.media3.transformer.GlMatrixTransformation;
import androidx.media3.transformer.GlFrameProcessor; import androidx.media3.transformer.MatrixTransformation;
/** /**
* Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying * Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
* transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}. * MatrixTransformation MatrixTransformations} that create video effects by applying transformation
* matrices to the individual video frames.
*/ */
/* package */ final class AdvancedFrameProcessorFactory { /* package */ final class MatrixTransformationFactory {
/** /**
* Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value * Returns a {@link MatrixTransformation} that rescales the frames over the first {@value
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases * #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
* linearly in size from a single point to filling the full output frame. * linearly in size from a single point to filling the full output frame.
*/ */
public static GlFrameProcessor createZoomInTransitionFrameProcessor() { public static MatrixTransformation createZoomInTransition() {
return new AdvancedFrameProcessor( return MatrixTransformationFactory::calculateZoomInTransitionMatrix;
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix);
} }
/** /**
* Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse. * Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an
* ellipse.
*/ */
public static GlFrameProcessor createDizzyCropFrameProcessor() { public static MatrixTransformation createDizzyCropEffect() {
return new AdvancedFrameProcessor( return MatrixTransformationFactory::calculateDizzyCropMatrix;
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix);
} }
/** /**
* Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies * Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and
* perspective projection to 2D. * applies perspective projection to 2D.
*/ */
public static GlFrameProcessor createSpin3dFrameProcessor() { public static GlMatrixTransformation createSpin3dEffect() {
return new AdvancedFrameProcessor( return MatrixTransformationFactory::calculate3dSpinMatrix;
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix);
} }
private static final float ZOOM_DURATION_SECONDS = 2f; private static final float ZOOM_DURATION_SECONDS = 2f;

View File

@ -246,7 +246,7 @@ public final class TransformerActivity extends AppCompatActivity {
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS); bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
if (selectedEffects != null) { if (selectedEffects != null) {
if (selectedEffects[0]) { if (selectedEffects[0]) {
effects.add(AdvancedFrameProcessorFactory::createDizzyCropFrameProcessor); effects.add(MatrixTransformationFactory.createDizzyCropEffect());
} }
if (selectedEffects[1]) { if (selectedEffects[1]) {
effects.add( effects.add(
@ -261,13 +261,13 @@ public final class TransformerActivity extends AppCompatActivity {
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS))); bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
} }
if (selectedEffects[2]) { if (selectedEffects[2]) {
effects.add(AdvancedFrameProcessorFactory::createSpin3dFrameProcessor); effects.add(MatrixTransformationFactory.createSpin3dEffect());
} }
if (selectedEffects[3]) { if (selectedEffects[3]) {
effects.add(BitmapOverlayFrameProcessor::new); effects.add(BitmapOverlayFrameProcessor::new);
} }
if (selectedEffects[4]) { if (selectedEffects[4]) {
effects.add(AdvancedFrameProcessorFactory::createZoomInTransitionFrameProcessor); effects.add(MatrixTransformationFactory.createZoomInTransition());
} }
transformerBuilder.setVideoFrameEffects(effects.build()); transformerBuilder.setVideoFrameEffects(effects.build());
} }

View File

@ -124,13 +124,14 @@ public final class FrameProcessorChainPixelTest {
} }
@Test @Test
public void processData_withAdvancedFrameProcessor_translateRight_producesExpectedOutput() public void processData_withMatrixTransformation_translateRight_producesExpectedOutput()
throws Exception { throws Exception {
String testId = "processData_withAdvancedFrameProcessor_translateRight"; String testId = "processData_withMatrixTransformation_translateRight";
Matrix translateRightMatrix = new Matrix(); Matrix translateRightMatrix = new Matrix();
translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0);
GlFrameProcessor glFrameProcessor = new AdvancedFrameProcessor(translateRightMatrix); setUpAndPrepareFirstFrame(
setUpAndPrepareFirstFrame(DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, () -> glFrameProcessor); DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
(MatrixTransformation) (long presentationTimeNs) -> translateRightMatrix);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd(); Bitmap actualBitmap = processFirstFrameAndEnd();
@ -145,19 +146,15 @@ public final class FrameProcessorChainPixelTest {
} }
@Test @Test
public void processData_withAdvancedAndScaleToFitFrameProcessors_producesExpectedOutput() public void processData_withMatrixAndScaleToFitTransformation_producesExpectedOutput()
throws Exception { throws Exception {
String testId = "processData_withAdvancedAndScaleToFitFrameProcessors"; String testId = "processData_withMatrixAndScaleToFitTransformation";
Matrix translateRightMatrix = new Matrix(); Matrix translateRightMatrix = new Matrix();
translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0);
GlFrameProcessor translateRightFrameProcessor =
new AdvancedFrameProcessor(translateRightMatrix);
GlFrameProcessor rotate45FrameProcessor =
new ScaleToFitFrameProcessor.Builder().setRotationDegrees(45).build();
setUpAndPrepareFirstFrame( setUpAndPrepareFirstFrame(
DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
() -> translateRightFrameProcessor, (MatrixTransformation) (long presentationTimeUs) -> translateRightMatrix,
() -> rotate45FrameProcessor); () -> new ScaleToFitFrameProcessor.Builder().setRotationDegrees(45).build());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_THEN_ROTATE_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_THEN_ROTATE_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd(); Bitmap actualBitmap = processFirstFrameAndEnd();
@ -172,19 +169,15 @@ public final class FrameProcessorChainPixelTest {
} }
@Test @Test
public void processData_withScaleToFitAndAdvancedFrameProcessors_producesExpectedOutput() public void processData_withScaleToFitAndMatrixTransformation_producesExpectedOutput()
throws Exception { throws Exception {
String testId = "processData_withScaleToFitAndAdvancedFrameProcessors"; String testId = "processData_withScaleToFitAndMatrixTransformation";
GlFrameProcessor rotate45FrameProcessor =
new ScaleToFitFrameProcessor.Builder().setRotationDegrees(45).build();
Matrix translateRightMatrix = new Matrix(); Matrix translateRightMatrix = new Matrix();
translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0);
GlFrameProcessor translateRightFrameProcessor =
new AdvancedFrameProcessor(translateRightMatrix);
setUpAndPrepareFirstFrame( setUpAndPrepareFirstFrame(
DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
() -> rotate45FrameProcessor, () -> new ScaleToFitFrameProcessor.Builder().setRotationDegrees(45).build(),
() -> translateRightFrameProcessor); (MatrixTransformation) (long presentationTimeUs) -> translateRightMatrix);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_THEN_TRANSLATE_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_THEN_TRANSLATE_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd(); Bitmap actualBitmap = processFirstFrameAndEnd();
@ -199,12 +192,11 @@ public final class FrameProcessorChainPixelTest {
} }
@Test @Test
public void processData_withPresentationFrameProcessor_setResolution_producesExpectedOutput() public void processData_withPresentation_setResolution_producesExpectedOutput() throws Exception {
throws Exception { String testId = "processData_withPresentation_setResolution";
String testId = "processData_withPresentationFrameProcessor_setResolution"; setUpAndPrepareFirstFrame(
GlFrameProcessor glFrameProcessor = DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
new PresentationFrameProcessor.Builder().setResolution(480).build(); () -> new PresentationFrameProcessor.Builder().setResolution(480).build());
setUpAndPrepareFirstFrame(DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, () -> glFrameProcessor);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd(); Bitmap actualBitmap = processFirstFrameAndEnd();
@ -219,12 +211,12 @@ public final class FrameProcessorChainPixelTest {
} }
@Test @Test
public void processData_withScaleToFitFrameProcessor_rotate45_producesExpectedOutput() public void processData_withScaleToFitTransformation_rotate45_producesExpectedOutput()
throws Exception { throws Exception {
String testId = "processData_withScaleToFitFrameProcessor_rotate45"; String testId = "processData_withScaleToFitTransformation_rotate45";
GlFrameProcessor glFrameProcessor = setUpAndPrepareFirstFrame(
new ScaleToFitFrameProcessor.Builder().setRotationDegrees(45).build(); DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
setUpAndPrepareFirstFrame(DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO, () -> glFrameProcessor); () -> new ScaleToFitFrameProcessor.Builder().setRotationDegrees(45).build());
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE45_SCALE_TO_FIT_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE45_SCALE_TO_FIT_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd(); Bitmap actualBitmap = processFirstFrameAndEnd();

View File

@ -34,7 +34,7 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** /**
* Pixel test for frame processing via {@link AdvancedFrameProcessor}. * Pixel test for frame processing via {@link MatrixTransformationFrameProcessor}.
* *
* <p>Expected images are taken from an emulator, so tests on different emulators or physical * <p>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 * devices may fail. To test on other devices, please increase the {@link
@ -42,7 +42,7 @@ import org.junit.runner.RunWith;
* as recommended in {@link FrameProcessorChainPixelTest}. * as recommended in {@link FrameProcessorChainPixelTest}.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class AdvancedFrameProcessorPixelTest { public final class MatrixTransformationFrameProcessorPixelTest {
public static final String ORIGINAL_PNG_ASSET_PATH = public static final String ORIGINAL_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/original.png"; "media/bitmap/sample_mp4_first_frame/original.png";
public static final String TRANSLATE_RIGHT_PNG_ASSET_PATH = public static final String TRANSLATE_RIGHT_PNG_ASSET_PATH =
@ -58,7 +58,7 @@ public final class AdvancedFrameProcessorPixelTest {
private final EGLDisplay eglDisplay = GlUtil.createEglDisplay(); private final EGLDisplay eglDisplay = GlUtil.createEglDisplay();
private final EGLContext eglContext = GlUtil.createEglContext(eglDisplay); private final EGLContext eglContext = GlUtil.createEglContext(eglDisplay);
private @MonotonicNonNull GlFrameProcessor advancedFrameProcessor; private @MonotonicNonNull GlFrameProcessor matrixTransformationFrameProcessor;
private int inputTexId; private int inputTexId;
private int outputTexId; private int outputTexId;
private int width; private int width;
@ -80,8 +80,8 @@ public final class AdvancedFrameProcessorPixelTest {
@After @After
public void release() { public void release() {
if (advancedFrameProcessor != null) { if (matrixTransformationFrameProcessor != null) {
advancedFrameProcessor.release(); matrixTransformationFrameProcessor.release();
} }
GlUtil.destroyEglContext(eglDisplay, eglContext); GlUtil.destroyEglContext(eglDisplay, eglContext);
} }
@ -90,11 +90,13 @@ public final class AdvancedFrameProcessorPixelTest {
public void drawFrame_noEdits_producesExpectedOutput() throws Exception { public void drawFrame_noEdits_producesExpectedOutput() throws Exception {
String testId = "drawFrame_noEdits"; String testId = "drawFrame_noEdits";
Matrix identityMatrix = new Matrix(); Matrix identityMatrix = new Matrix();
advancedFrameProcessor = new AdvancedFrameProcessor(identityMatrix); matrixTransformationFrameProcessor =
advancedFrameProcessor.initialize(getApplicationContext(), inputTexId, width, height); new MatrixTransformationFrameProcessor((long presentationTimeUs) -> identityMatrix);
matrixTransformationFrameProcessor.initialize(
getApplicationContext(), inputTexId, width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
advancedFrameProcessor.drawFrame(/* presentationTimeUs= */ 0); matrixTransformationFrameProcessor.drawFrame(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
@ -112,11 +114,13 @@ public final class AdvancedFrameProcessorPixelTest {
String testId = "drawFrame_translateRight"; String testId = "drawFrame_translateRight";
Matrix translateRightMatrix = new Matrix(); Matrix translateRightMatrix = new Matrix();
translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0);
advancedFrameProcessor = new AdvancedFrameProcessor(translateRightMatrix); matrixTransformationFrameProcessor =
advancedFrameProcessor.initialize(getApplicationContext(), inputTexId, width, height); new MatrixTransformationFrameProcessor((long presentationTimeUs) -> translateRightMatrix);
matrixTransformationFrameProcessor.initialize(
getApplicationContext(), inputTexId, width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_PNG_ASSET_PATH);
advancedFrameProcessor.drawFrame(/* presentationTimeUs= */ 0); matrixTransformationFrameProcessor.drawFrame(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
@ -134,11 +138,13 @@ public final class AdvancedFrameProcessorPixelTest {
String testId = "drawFrame_scaleNarrow"; String testId = "drawFrame_scaleNarrow";
Matrix scaleNarrowMatrix = new Matrix(); Matrix scaleNarrowMatrix = new Matrix();
scaleNarrowMatrix.postScale(.5f, 1.2f); scaleNarrowMatrix.postScale(.5f, 1.2f);
advancedFrameProcessor = new AdvancedFrameProcessor(scaleNarrowMatrix); matrixTransformationFrameProcessor =
advancedFrameProcessor.initialize(getApplicationContext(), inputTexId, width, height); new MatrixTransformationFrameProcessor((long presentationTimeUs) -> scaleNarrowMatrix);
matrixTransformationFrameProcessor.initialize(
getApplicationContext(), inputTexId, width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(SCALE_NARROW_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(SCALE_NARROW_PNG_ASSET_PATH);
advancedFrameProcessor.drawFrame(/* presentationTimeUs= */ 0); matrixTransformationFrameProcessor.drawFrame(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
@ -156,11 +162,13 @@ public final class AdvancedFrameProcessorPixelTest {
String testId = "drawFrame_rotate90"; String testId = "drawFrame_rotate90";
Matrix rotate90Matrix = new Matrix(); Matrix rotate90Matrix = new Matrix();
rotate90Matrix.postRotate(/* degrees= */ 90); rotate90Matrix.postRotate(/* degrees= */ 90);
advancedFrameProcessor = new AdvancedFrameProcessor(rotate90Matrix); matrixTransformationFrameProcessor =
advancedFrameProcessor.initialize(getApplicationContext(), inputTexId, width, height); new MatrixTransformationFrameProcessor((long presentationTimeUs) -> rotate90Matrix);
matrixTransformationFrameProcessor.initialize(
getApplicationContext(), inputTexId, width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_PNG_ASSET_PATH); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_PNG_ASSET_PATH);
advancedFrameProcessor.drawFrame(/* presentationTimeUs= */ 0); matrixTransformationFrameProcessor.drawFrame(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);

View File

@ -1,206 +0,0 @@
/*
* 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.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.UnstableApi;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Applies a transformation matrix in the vertex shader, and copies input pixels into an output
* frame based on their locations after applying this matrix.
*
* <p>Operations are done on normalized device coordinates (-1 to 1 on x and y axes). No automatic
* adjustments (like done in {@link ScaleToFitFrameProcessor}) are applied on the transformation.
* Width and height are not modified.
*
* <p>The background color of the output frame will be black.
*/
@UnstableApi
@SuppressWarnings("FunctionalInterfaceClash") // b/228192298
public final class AdvancedFrameProcessor implements GlFrameProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
/** Updates the transformation {@link android.opengl.Matrix} for each frame. */
public interface GlMatrixProvider {
/**
* Updates the transformation {@link android.opengl.Matrix} to apply to the frame with the given
* timestamp in place.
*/
float[] getGlMatrixArray(long presentationTimeUs);
}
/** Provides a {@link android.graphics.Matrix} for each frame. */
public interface MatrixProvider extends GlMatrixProvider {
/**
* Returns the transformation {@link android.graphics.Matrix} to apply to the frame with the
* given timestamp.
*/
android.graphics.Matrix getMatrix(long presentationTimeUs);
@Override
default float[] getGlMatrixArray(long presentationTimeUs) {
return AdvancedFrameProcessor.getGlMatrixArray(getMatrix(presentationTimeUs));
}
}
private static final String VERTEX_SHADER_TRANSFORMATION_PATH =
"shaders/vertex_shader_transformation_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl";
/**
* Returns a 4x4, column-major {@link android.opengl.Matrix} float array, from an input {@link
* android.graphics.Matrix}.
*
* <p>This is useful for converting to the 4x4 column-major format commonly used in OpenGL.
*/
private static float[] getGlMatrixArray(android.graphics.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.
*
* <p>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.
* <br>
* Input format: [a, b, c, d, e, f, g, h, i] <br>
* 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 final GlMatrixProvider matrixProvider;
private @MonotonicNonNull Size size;
private @MonotonicNonNull GlProgram glProgram;
/**
* Creates a new instance.
*
* @param transformationMatrix The transformation {@link android.graphics.Matrix} to apply to each
* frame. Operations are done on normalized device coordinates (-1 to 1 on x and y), and no
* automatic adjustments are applied on the transformation matrix.
*/
public AdvancedFrameProcessor(android.graphics.Matrix transformationMatrix) {
this(getGlMatrixArray(transformationMatrix));
}
/**
* Creates a new instance.
*
* @param matrixProvider A {@link MatrixProvider} that provides the transformation matrix to apply
* to each frame.
*/
public AdvancedFrameProcessor(MatrixProvider matrixProvider) {
this.matrixProvider = matrixProvider;
}
/**
* Creates a new instance.
*
* @param transformationMatrix The 4x4 transformation {@link android.opengl.Matrix} to apply to
* each frame. Operations are done on normalized device coordinates (-1 to 1 on x and y), and
* no automatic adjustments are applied on the transformation matrix.
*/
public AdvancedFrameProcessor(float[] transformationMatrix) {
this(/* matrixProvider= */ (long presentationTimeUs) -> transformationMatrix.clone());
checkArgument(
transformationMatrix.length == 16, "A 4x4 transformation matrix must have 16 elements.");
}
/**
* Creates a new instance.
*
* @param matrixProvider A {@link GlMatrixProvider} that updates the transformation matrix for
* each frame.
*/
public AdvancedFrameProcessor(GlMatrixProvider matrixProvider) {
this.matrixProvider = matrixProvider;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
size = new Size(inputWidth, inputHeight);
// TODO(b/205002913): check the loaded program is consistent with the attributes and uniforms
// expected in the code.
glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
// 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(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(size);
}
@Override
public void drawFrame(long presentationTimeUs) {
checkStateNotNull(glProgram).use();
glProgram.setFloatsUniform(
"uTransformationMatrix", matrixProvider.getGlMatrixArray(presentationTimeUs));
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
}

View File

@ -0,0 +1,55 @@
/*
* 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 android.opengl.Matrix;
import android.util.Size;
import androidx.media3.common.util.UnstableApi;
/**
* Specifies a 4x4 transformation {@link Matrix} to apply in the vertex shader for each frame.
*
* <p>The matrix is applied to points given in normalized device coordinates (-1 to 1 on x, y, and z
* axes). Transformed pixels that are moved outside of the normal device coordinate range are
* clipped.
*
* <p>Output frame pixels outside of the transformed input frame will be black.
*/
@UnstableApi
public interface GlMatrixTransformation extends GlEffect {
/**
* Configures the input and output dimensions.
*
* <p>Must be called before {@link #getGlMatrixArray(long)}.
*
* @param inputWidth The input frame width, in pixels.
* @param inputHeight The input frame height, in pixels.
* @return The output frame {@link Size}, in pixels.
*/
default Size configure(int inputWidth, int inputHeight) {
return new Size(inputWidth, inputHeight);
}
/**
* Returns the 4x4 transformation {@link Matrix} to apply to the frame with the given timestamp.
*/
float[] getGlMatrixArray(long presentationTimeUs);
@Override
default GlFrameProcessor toGlFrameProcessor() {
return new MatrixTransformationFrameProcessor(this);
}
}

View File

@ -0,0 +1,41 @@
/*
* 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 android.graphics.Matrix;
import androidx.media3.common.util.UnstableApi;
/**
* Specifies a 3x3 transformation {@link Matrix} to apply in the vertex shader for each frame.
*
* <p>The matrix is applied to points given in normalized device coordinates (-1 to 1 on x and y
* axes). Transformed pixels that are moved outside of the normal device coordinate range are
* clipped.
*
* <p>Output frame pixels outside of the transformed input frame will be black.
*/
@UnstableApi
public interface MatrixTransformation extends GlMatrixTransformation {
/**
* Returns the 3x3 transformation {@link Matrix} to apply to the frame with the given timestamp.
*/
Matrix getMatrix(long presentationTimeUs);
@Override
default float[] getGlMatrixArray(long presentationTimeUs) {
return MatrixUtils.getGlMatrixArray(getMatrix(presentationTimeUs));
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.UnstableApi;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Applies a transformation matrix in the vertex shader, and copies input pixels into an output
* frame based on their locations after applying this matrix.
*
* <p>Operations are done on normalized device coordinates (-1 to 1 on x and y axes). No automatic
* adjustments (like done in {@link ScaleToFitFrameProcessor}) are applied on the transformation.
*
* <p>The background color of the output frame will be black.
*/
// TODO(b/227625423): Compose multiple transformation matrices in a single shader with clipping
// after each matrix.
@UnstableApi
@SuppressWarnings("FunctionalInterfaceClash") // b/228192298
public final class MatrixTransformationFrameProcessor implements GlFrameProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_TRANSFORMATION_PATH =
"shaders/vertex_shader_transformation_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl";
private final GlMatrixTransformation matrixTransformation;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
/**
* Creates a new instance.
*
* @param matrixTransformation A {@link MatrixTransformation} that specifies the transformation
* matrix to use for each frame.
*/
public MatrixTransformationFrameProcessor(MatrixTransformation matrixTransformation) {
this.matrixTransformation = matrixTransformation;
}
/**
* Creates a new instance.
*
* @param matrixTransformation A {@link GlMatrixTransformation} that specifies the transformation
* matrix to use for each frame.
*/
public MatrixTransformationFrameProcessor(GlMatrixTransformation matrixTransformation) {
this.matrixTransformation = matrixTransformation;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
outputSize = matrixTransformation.configure(inputWidth, inputHeight);
// TODO(b/205002913): check the loaded program is consistent with the attributes and uniforms
// expected in the code.
glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
// 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(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) {
checkStateNotNull(glProgram).use();
float[] transformationMatrix = matrixTransformation.getGlMatrixArray(presentationTimeUs);
checkState(
transformationMatrix.length == 16, "A 4x4 transformation matrix must have 16 elements");
glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrix);
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
}

View File

@ -0,0 +1,64 @@
/*
* 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;
/** Utility functions for working with matrices. */
/* package */ class MatrixUtils {
/**
* Returns a 4x4, column-major {@link android.opengl.Matrix} float array, from an input {@link
* android.graphics.Matrix}.
*
* <p>This is useful for converting to the 4x4 column-major format commonly used in OpenGL.
*/
public static float[] getGlMatrixArray(android.graphics.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.
*
* <p>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.
* <br>
* Input format: [a, b, c, d, e, f, g, h, i] <br>
* 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;
}
/** Class only contains static methods. */
private MatrixUtils() {}
}

View File

@ -48,6 +48,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <p>The background color of the output frame will be black. * <p>The background color of the output frame will be black.
*/ */
@UnstableApi @UnstableApi
// TODO(b/227625423): Implement MatrixTransformation instead of wrapping
// MatrixTransformationFrameProcessor.
public final class PresentationFrameProcessor implements GlFrameProcessor { public final class PresentationFrameProcessor implements GlFrameProcessor {
/** /**
* Strategies controlling the layout of input pixels in the output frame. * Strategies controlling the layout of input pixels in the output frame.
@ -237,7 +239,7 @@ public final class PresentationFrameProcessor implements GlFrameProcessor {
private float outputHeight; private float outputHeight;
private @MonotonicNonNull Size outputSize; private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Matrix transformationMatrix; private @MonotonicNonNull Matrix transformationMatrix;
private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; private @MonotonicNonNull MatrixTransformationFrameProcessor matrixTransformationFrameProcessor;
/** Creates a new instance. */ /** Creates a new instance. */
private PresentationFrameProcessor( private PresentationFrameProcessor(
@ -265,8 +267,11 @@ public final class PresentationFrameProcessor implements GlFrameProcessor {
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight) public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException { throws IOException {
configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight);
advancedFrameProcessor = new AdvancedFrameProcessor(transformationMatrix); matrixTransformationFrameProcessor =
advancedFrameProcessor.initialize(context, inputTexId, inputWidth, inputHeight); new MatrixTransformationFrameProcessor(
/* matrixTransformation= */ (long presentationTimeUs) ->
checkStateNotNull(transformationMatrix));
matrixTransformationFrameProcessor.initialize(context, inputTexId, inputWidth, inputHeight);
} }
@Override @Override
@ -279,13 +284,13 @@ public final class PresentationFrameProcessor implements GlFrameProcessor {
@Override @Override
public void drawFrame(long presentationTimeUs) { public void drawFrame(long presentationTimeUs) {
checkStateNotNull(advancedFrameProcessor).drawFrame(presentationTimeUs); checkStateNotNull(matrixTransformationFrameProcessor).drawFrame(presentationTimeUs);
} }
@Override @Override
public void release() { public void release() {
if (advancedFrameProcessor != null) { if (matrixTransformationFrameProcessor != null) {
advancedFrameProcessor.release(); matrixTransformationFrameProcessor.release();
} }
} }

View File

@ -39,6 +39,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>The background color of the output frame will be black. * <p>The background color of the output frame will be black.
*/ */
@UnstableApi @UnstableApi
// TODO(b/227625423): Implement MatrixTransformation instead of wrapping
// MatrixTransformationFrameProcessor.
public final class ScaleToFitFrameProcessor implements GlFrameProcessor { public final class ScaleToFitFrameProcessor implements GlFrameProcessor {
/** A builder for {@link ScaleToFitFrameProcessor} instances. */ /** A builder for {@link ScaleToFitFrameProcessor} instances. */
@ -95,7 +97,7 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor {
private final Matrix transformationMatrix; private final Matrix transformationMatrix;
private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; private @MonotonicNonNull MatrixTransformationFrameProcessor matrixTransformationFrameProcessor;
private @MonotonicNonNull Size outputSize; private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Matrix adjustedTransformationMatrix; private @MonotonicNonNull Matrix adjustedTransformationMatrix;
@ -116,8 +118,11 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor {
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight) public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException { throws IOException {
configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight);
advancedFrameProcessor = new AdvancedFrameProcessor(adjustedTransformationMatrix); matrixTransformationFrameProcessor =
advancedFrameProcessor.initialize(context, inputTexId, inputWidth, inputHeight); new MatrixTransformationFrameProcessor(
/* matrixTransformation= */ (long presentationTimeUs) ->
checkStateNotNull(adjustedTransformationMatrix));
matrixTransformationFrameProcessor.initialize(context, inputTexId, inputWidth, inputHeight);
} }
@Override @Override
@ -127,13 +132,13 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor {
@Override @Override
public void drawFrame(long presentationTimeUs) { public void drawFrame(long presentationTimeUs) {
checkStateNotNull(advancedFrameProcessor).drawFrame(presentationTimeUs); checkStateNotNull(matrixTransformationFrameProcessor).drawFrame(presentationTimeUs);
} }
@Override @Override
public void release() { public void release() {
if (advancedFrameProcessor != null) { if (matrixTransformationFrameProcessor != null) {
advancedFrameProcessor.release(); matrixTransformationFrameProcessor.release();
} }
} }

View File

@ -1,44 +0,0 @@
/*
* 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 org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link AdvancedFrameProcessor}.
*
* <p>See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link
* AdvancedFrameProcessor} given a transformation matrix.
*/
@RunWith(AndroidJUnit4.class)
public final class AdvancedFrameProcessorTest {
@Test
public void construct_withInvalidMatrixSize_throwsException() {
assertThrows(
IllegalArgumentException.class,
() -> new AdvancedFrameProcessor(/* transformationMatrix= */ new float[4]));
}
@Test
public void construct_withValidMatrixSize_completesSuccessfully() {
new AdvancedFrameProcessor(/* transformationMatrix= */ new float[16]);
}
}