Skip to content

Commit

Permalink
Transformer GL: Create setTransformationMatrix().
Browse files Browse the repository at this point in the history
Allows a transformation matrix to be input into Transformer,
to apply vertex transformations like cropping, rotation,
and other transformations built into android.graphics.Matrix.

Not building out into a VertexTransformation class yet, as
that class structure wouldn't make sense until we can modify
resolution, per TODOs.

PiperOrigin-RevId: 413384409
  • Loading branch information
dway123 authored and tonihei committed Dec 2, 2021
1 parent 34975a7 commit 0578b2e
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.media.Image;
import android.media.ImageReader;
Expand Down Expand Up @@ -84,9 +85,14 @@ public void setUp() throws Exception {
int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
frameEditorOutputImageReader =
ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1);
Matrix identityMatrix = new Matrix();
frameEditor =
FrameEditor.create(
getApplicationContext(), width, height, frameEditorOutputImageReader.getSurface());
getApplicationContext(),
width,
height,
identityMatrix,
frameEditorOutputImageReader.getSurface());

// Queue the first video frame from the extractor.
String mimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.transformer.mh;

import static com.google.android.exoplayer2.transformer.mh.AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING;
import static com.google.android.exoplayer2.transformer.mh.AndroidTestUtil.runTransformer;

import android.content.Context;
import android.graphics.Matrix;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.transformer.Transformer;
import org.junit.Test;
import org.junit.runner.RunWith;

/** {@link Transformer} instrumentation test for setting a transformation matrix. */
@RunWith(AndroidJUnit4.class)
public class SetTransformationMatrixTransformationTest {
@Test
public void setTransformationMatrixTransform() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Matrix transformationMatrix = new Matrix();
transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f);
Transformer transformer =
new Transformer.Builder(context).setTransformationMatrix(transformationMatrix).build();

runTransformer(
context,
/* testId= */ "setTransformationMatrixTransform",
transformer,
REMOTE_MP4_10_SECONDS_URI_STRING,
/* timeoutSeconds= */ 120);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
attribute vec4 a_position;
attribute vec4 a_texcoord;
uniform mat4 tex_transform;
uniform mat4 transformation_matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = (tex_transform * a_texcoord).xy;
v_texcoord = (transformation_matrix * tex_transform * a_texcoord).xy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.android.exoplayer2.transformer;

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLContext;
Expand All @@ -27,10 +28,7 @@
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;

/**
* FrameEditor applies changes to individual video frames. Changes include just resolution for now,
* but may later include brightness, cropping, rotation, etc.
*/
/** FrameEditor applies changes to individual video frames. */
/* package */ final class FrameEditor {

static {
Expand All @@ -43,11 +41,16 @@
* @param context A {@link Context}.
* @param outputWidth The output width in pixels.
* @param outputHeight The output height in pixels.
* @param transformationMatrix The transformation matrix to apply to each frame.
* @param outputSurface The {@link Surface}.
* @return A configured {@code FrameEditor}.
*/
public static FrameEditor create(
Context context, int outputWidth, int outputHeight, Surface outputSurface) {
Context context,
int outputWidth,
int outputHeight,
Matrix transformationMatrix,
Surface outputSurface) {
EGLDisplay eglDisplay = GlUtil.createEglDisplay();
EGLContext eglContext;
try {
Expand All @@ -58,15 +61,16 @@ public static FrameEditor create(
EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface);
GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
int textureId = GlUtil.createExternalTexture();
GlUtil.Program copyProgram;
GlUtil.Program glProgram;
try {
// TODO(internal b/205002913): check the loaded program is consistent with the attributes
// and uniforms expected in the code.
copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH);
glProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH);
} catch (IOException e) {
throw new IllegalStateException(e);
}
copyProgram.setBufferAttribute(

glProgram.setBufferAttribute(
"a_position",
new float[] {
-1.0f, -1.0f, 0.0f, 1.0f,
Expand All @@ -75,7 +79,7 @@ public static FrameEditor create(
1.0f, 1.0f, 0.0f, 1.0f,
},
/* size= */ 4);
copyProgram.setBufferAttribute(
glProgram.setBufferAttribute(
"a_texcoord",
new float[] {
0.0f, 0.0f, 0.0f, 1.0f,
Expand All @@ -84,14 +88,57 @@ public static FrameEditor create(
1.0f, 1.0f, 0.0f, 1.0f,
},
/* size= */ 4);
copyProgram.setSamplerTexIdUniform("tex_sampler", textureId, /* unit= */ 0);
return new FrameEditor(eglDisplay, eglContext, eglSurface, textureId, copyProgram);
glProgram.setSamplerTexIdUniform("tex_sampler", textureId, /* unit= */ 0);

float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix);
glProgram.setFloatsUniform("transformation_matrix", transformationMatrixArray);

return new FrameEditor(eglDisplay, eglContext, eglSurface, textureId, 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 final 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.
*
* <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 final 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;
}

// Predefined shader values.
private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl";
private static final String FRAGMENT_SHADER_FILE_PATH =
"shaders/copy_external_fragment_shader.glsl";
private static final String VERTEX_SHADER_FILE_PATH = "shaders/vertex_shader.glsl";
private static final String FRAGMENT_SHADER_FILE_PATH = "shaders/fragment_shader.glsl";

private final float[] textureTransformMatrix;
private final EGLDisplay eglDisplay;
Expand All @@ -101,7 +148,7 @@ public static FrameEditor create(
private final SurfaceTexture inputSurfaceTexture;
private final Surface inputSurface;

private final GlUtil.Program copyProgram;
private final GlUtil.Program glProgram;

private volatile boolean hasInputData;

Expand All @@ -110,12 +157,12 @@ private FrameEditor(
EGLContext eglContext,
EGLSurface eglSurface,
int textureId,
GlUtil.Program copyProgram) {
GlUtil.Program glProgram) {
this.eglDisplay = eglDisplay;
this.eglContext = eglContext;
this.eglSurface = eglSurface;
this.textureId = textureId;
this.copyProgram = copyProgram;
this.glProgram = glProgram;
textureTransformMatrix = new float[16];
inputSurfaceTexture = new SurfaceTexture(textureId);
inputSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> hasInputData = true);
Expand All @@ -135,12 +182,12 @@ public boolean hasInputData() {
return hasInputData;
}

/** Processes pending input data. */
/** Processes pending input frame. */
public void processData() {
inputSurfaceTexture.updateTexImage();
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
copyProgram.setFloatsUniform("tex_transform", textureTransformMatrix);
copyProgram.bindAttributesAndUniforms();
glProgram.setFloatsUniform("tex_transform", textureTransformMatrix);
glProgram.bindAttributesAndUniforms();
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
long surfaceTextureTimestampNs = inputSurfaceTexture.getTimestamp();
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTextureTimestampNs);
Expand All @@ -150,7 +197,7 @@ public void processData() {

/** Releases all resources. */
public void release() {
copyProgram.delete();
glProgram.delete();
GlUtil.deleteTexture(textureId);
GlUtil.destroyEglContext(eglDisplay, eglContext);
inputSurfaceTexture.release();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.android.exoplayer2.transformer;

import android.graphics.Matrix;
import androidx.annotation.Nullable;

/** A media transformation configuration. */
Expand All @@ -25,6 +26,7 @@
public final boolean removeVideo;
public final boolean flattenForSlowMotion;
public final int outputHeight;
public final Matrix transformationMatrix;
public final String containerMimeType;
@Nullable public final String audioMimeType;
@Nullable public final String videoMimeType;
Expand All @@ -34,13 +36,15 @@ public Transformation(
boolean removeVideo,
boolean flattenForSlowMotion,
int outputHeight,
Matrix transformationMatrix,
String containerMimeType,
@Nullable String audioMimeType,
@Nullable String videoMimeType) {
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.flattenForSlowMotion = flattenForSlowMotion;
this.outputHeight = outputHeight;
this.transformationMatrix = transformationMatrix;
this.containerMimeType = containerMimeType;
this.audioMimeType = audioMimeType;
this.videoMimeType = videoMimeType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static java.lang.Math.min;

import android.content.Context;
import android.graphics.Matrix;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Handler;
Expand Down Expand Up @@ -100,6 +101,7 @@ public static final class Builder {
private boolean removeVideo;
private boolean flattenForSlowMotion;
private int outputHeight;
private Matrix transformationMatrix;
private String containerMimeType;
@Nullable private String audioMimeType;
@Nullable private String videoMimeType;
Expand All @@ -112,6 +114,7 @@ public static final class Builder {
public Builder() {
muxerFactory = new FrameworkMuxer.Factory();
outputHeight = Format.NO_VALUE;
transformationMatrix = new Matrix();
containerMimeType = MimeTypes.VIDEO_MP4;
listener = new Listener() {};
looper = Util.getCurrentOrMainLooper();
Expand All @@ -127,6 +130,7 @@ public Builder(Context context) {
this.context = context.getApplicationContext();
muxerFactory = new FrameworkMuxer.Factory();
outputHeight = Format.NO_VALUE;
transformationMatrix = new Matrix();
containerMimeType = MimeTypes.VIDEO_MP4;
listener = new Listener() {};
looper = Util.getCurrentOrMainLooper();
Expand All @@ -142,6 +146,7 @@ private Builder(Transformer transformer) {
this.removeVideo = transformer.transformation.removeVideo;
this.flattenForSlowMotion = transformer.transformation.flattenForSlowMotion;
this.outputHeight = transformer.transformation.outputHeight;
this.transformationMatrix = transformer.transformation.transformationMatrix;
this.containerMimeType = transformer.transformation.containerMimeType;
this.audioMimeType = transformer.transformation.audioMimeType;
this.videoMimeType = transformer.transformation.videoMimeType;
Expand Down Expand Up @@ -258,6 +263,26 @@ public Builder setResolution(int outputHeight) {
return this;
}

/**
* Sets the transformation matrix. The default value is to apply no change.
*
* <p>This can be used to perform operations supported by {@link Matrix}, like scaling and
* rotating the video.
*
* <p>For now, resolution will not be affected by this method.
*
* @param transformationMatrix The transformation to apply to video frames.
* @return This builder.
*/
public Builder setTransformationMatrix(Matrix transformationMatrix) {
// TODO(Internal b/201293185): After {@link #setResolution} supports arbitrary resolutions,
// allow transformations to change the resolution, by scaling to the appropriate min/max
// values. This will also be required to create the VertexTransformation class, in order to
// have aspect ratio helper methods (which require resolution to change).
this.transformationMatrix = transformationMatrix;
return this;
}

/**
* @deprecated This feature will be removed in a following release and the MIME type of the
* output will always be MP4.
Expand Down Expand Up @@ -409,6 +434,7 @@ public Transformer build() {
removeVideo,
flattenForSlowMotion,
outputHeight,
transformationMatrix,
containerMimeType,
audioMimeType,
videoMimeType);
Expand Down
Loading

0 comments on commit 0578b2e

Please sign in to comment.