Commit 23b0610a by hschlueter Committed by microkatz

Move program initialization to texture processor constructor.

Once the more advanced GlTextureProcessor interface exists,
it will be possible to change the output size of a GlTextureProcessor
between frames. To keep the re-configuration based on the frame sizes
minimal, things indepedent of the frame size, such as the GlProgram,
can be initialized in the constructor.

PiperOrigin-RevId: 451997584
(cherry picked from commit 54d44d38)
parent f7fc04dc
Showing with 264 additions and 250 deletions
......@@ -35,7 +35,6 @@ import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
import java.util.Locale;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
......@@ -57,16 +56,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Paint paint;
private final Bitmap overlayBitmap;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas;
private final GlProgram glProgram;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Bitmap logoBitmap;
private @MonotonicNonNull GlProgram glProgram;
public BitmapOverlayProcessor() {
/**
* Creates a new instance.
*
* @throws IOException If a problem occurs while reading shader files.
*/
public BitmapOverlayProcessor(Context context) throws IOException {
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
......@@ -75,19 +78,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
overlayBitmap =
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
outputSize = new Size(inputWidth, inputHeight);
try {
logoBitmap =
......@@ -106,19 +96,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
public Size configure(int inputWidth, int inputHeight) {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
......@@ -137,6 +135,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
......
......@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.transformerdemo;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
......@@ -26,7 +25,6 @@ import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
......@@ -41,14 +39,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
private static final float DIMMING_PERIOD_US = 5_600_000f;
private float centerX;
private float centerY;
private float minInnerRadius;
private float deltaInnerRadius;
private float outerRadius;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
private final GlProgram glProgram;
private final float minInnerRadius;
private final float deltaInnerRadius;
/**
* Creates a new instance.
......@@ -61,29 +54,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*
* <p>The parameters are given in normalized texture coordinates from 0 to 1.
*
* @param context The {@link Context}.
* @param centerX The x-coordinate of the center of the effect.
* @param centerY The y-coordinate of the center of the effect.
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
* @param outerRadius The radius after which all pixels are black.
* @throws IOException If a problem occurs while reading shader files.
*/
public PeriodicVignetteProcessor(
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
Context context,
float centerX,
float centerY,
float minInnerRadius,
float maxInnerRadius,
float outerRadius)
throws IOException {
checkArgument(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius);
this.centerX = centerX;
this.centerY = centerY;
this.minInnerRadius = minInnerRadius;
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
this.outerRadius = outerRadius;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
outputSize = new Size(inputWidth, inputHeight);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
......@@ -94,14 +85,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
public Size configure(int inputWidth, int inputHeight) {
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius =
minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
......
......@@ -19,6 +19,7 @@ import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
......@@ -275,12 +276,13 @@ public final class TransformerActivity extends AppCompatActivity {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.transformerdemo.MediaPipeProcessor");
Constructor<?> constructor =
clazz.getConstructor(String.class, String.class, String.class);
clazz.getConstructor(Context.class, String.class, String.class, String.class);
effects.add(
() -> {
(Context context) -> {
try {
return (SingleFrameGlTextureProcessor)
constructor.newInstance(
context,
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
/* inputStreamName= */ "input_video",
/* outputStreamName= */ "output_video");
......@@ -295,8 +297,9 @@ public final class TransformerActivity extends AppCompatActivity {
}
if (selectedEffects[2]) {
effects.add(
() ->
(Context context) ->
new PeriodicVignetteProcessor(
context,
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
/* minInnerRadius= */ bundle.getFloat(
......
......@@ -63,49 +63,36 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl";
private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl";
private final String graphName;
private final String inputStreamName;
private final String outputStreamName;
private final ConditionVariable frameProcessorConditionVariable;
private final FrameProcessor frameProcessor;
private final GlProgram glProgram;
private @MonotonicNonNull FrameProcessor frameProcessor;
private int inputWidth;
private int inputHeight;
private int inputTexId;
private @MonotonicNonNull GlProgram glProgram;
private @MonotonicNonNull TextureFrame outputFrame;
private @MonotonicNonNull RuntimeException frameProcessorPendingError;
/**
* Creates a new texture processor that wraps a MediaPipe graph.
*
* @param context The {@link Context}.
* @param graphName Name of a MediaPipe graph asset to load.
* @param inputStreamName Name of the input video stream in the graph.
* @param outputStreamName Name of the input video stream in the graph.
* @throws IOException If a problem occurs while reading shader files or initializing MediaPipe
* resources.
*/
public MediaPipeProcessor(String graphName, String inputStreamName, String outputStreamName) {
checkState(LOADER.isAvailable());
this.graphName = graphName;
this.inputStreamName = inputStreamName;
this.outputStreamName = outputStreamName;
frameProcessorConditionVariable = new ConditionVariable();
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
public MediaPipeProcessor(
Context context, String graphName, String inputStreamName, String outputStreamName)
throws IOException {
this.inputTexId = inputTexId;
this.inputWidth = inputWidth;
this.inputHeight = inputHeight;
glProgram = new GlProgram(context, COPY_VERTEX_SHADER_NAME, COPY_FRAGMENT_SHADER_NAME);
checkState(LOADER.isAvailable());
frameProcessorConditionVariable = new ConditionVariable();
AndroidAssetUtil.initializeNativeAssetManager(context);
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor =
new FrameProcessor(
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName);
// Unblock drawFrame when there is an output frame or an error.
frameProcessor.setConsumer(
frame -> {
......@@ -117,15 +104,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
frameProcessorPendingError = error;
frameProcessorConditionVariable.open();
});
glProgram = new GlProgram(context, COPY_VERTEX_SHADER_NAME, COPY_FRAGMENT_SHADER_NAME);
}
@Override
public Size getOutputSize() {
public Size configure(int inputWidth, int inputHeight) {
this.inputWidth = inputWidth;
this.inputHeight = inputHeight;
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
frameProcessorConditionVariable.close();
// Pass the input frame to MediaPipe.
......
......@@ -122,7 +122,7 @@ public final class FrameProcessorChainTest {
throws FrameProcessingException {
ImmutableList.Builder<GlEffect> effects = new ImmutableList.Builder<>();
for (Size element : textureProcessorOutputSizes) {
effects.add(() -> new FakeTextureProcessor(element));
effects.add((Context context) -> new FakeTextureProcessor(element));
}
return FrameProcessorChain.create(
getApplicationContext(),
......@@ -144,15 +144,12 @@ public final class FrameProcessorChainTest {
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight) {}
@Override
public Size getOutputSize() {
public Size configure(int inputWidth, int inputHeight) {
return outputSize;
}
@Override
public void drawFrame(long presentationTimeNs) {}
public void drawFrame(int inputTexId, long presentationTimeNs) {}
@Override
public void release() {}
......
......@@ -19,6 +19,7 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.android.exoplayer2.transformer.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.opengl.EGLContext;
......@@ -56,9 +57,10 @@ public final class MatrixTransformationProcessorPixelTest {
GlUtil.glAssertionsEnabled = true;
}
private final Context context = getApplicationContext();
private final EGLDisplay eglDisplay = GlUtil.createEglDisplay();
private final EGLContext eglContext = GlUtil.createEglContext(eglDisplay);
private @MonotonicNonNull SingleFrameGlTextureProcessor matrixTransformationProcessor;
private @MonotonicNonNull SingleFrameGlTextureProcessor matrixTransformationFrameProcessor;
private int inputTexId;
private int outputTexId;
private int width;
......@@ -80,8 +82,8 @@ public final class MatrixTransformationProcessorPixelTest {
@After
public void release() {
if (matrixTransformationProcessor != null) {
matrixTransformationProcessor.release();
if (matrixTransformationFrameProcessor != null) {
matrixTransformationFrameProcessor.release();
}
GlUtil.destroyEglContext(eglDisplay, eglContext);
}
......@@ -90,12 +92,12 @@ public final class MatrixTransformationProcessorPixelTest {
public void drawFrame_noEdits_producesExpectedOutput() throws Exception {
String testId = "drawFrame_noEdits";
Matrix identityMatrix = new Matrix();
matrixTransformationProcessor =
new MatrixTransformationProcessor((long presentationTimeUs) -> identityMatrix);
matrixTransformationProcessor.initialize(getApplicationContext(), inputTexId, width, height);
matrixTransformationFrameProcessor =
new MatrixTransformationProcessor(context, (long presentationTimeUs) -> identityMatrix);
matrixTransformationFrameProcessor.configure(width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
matrixTransformationProcessor.drawFrame(/* presentationTimeUs= */ 0);
matrixTransformationFrameProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......@@ -113,12 +115,13 @@ public final class MatrixTransformationProcessorPixelTest {
String testId = "drawFrame_translateRight";
Matrix translateRightMatrix = new Matrix();
translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0);
matrixTransformationProcessor =
new MatrixTransformationProcessor((long presentationTimeUs) -> translateRightMatrix);
matrixTransformationProcessor.initialize(getApplicationContext(), inputTexId, width, height);
matrixTransformationFrameProcessor =
new MatrixTransformationProcessor(
context, /* matrixTransformation= */ (long presentationTimeUs) -> translateRightMatrix);
matrixTransformationFrameProcessor.configure(width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_PNG_ASSET_PATH);
matrixTransformationProcessor.drawFrame(/* presentationTimeUs= */ 0);
matrixTransformationFrameProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......@@ -136,12 +139,13 @@ public final class MatrixTransformationProcessorPixelTest {
String testId = "drawFrame_scaleNarrow";
Matrix scaleNarrowMatrix = new Matrix();
scaleNarrowMatrix.postScale(.5f, 1.2f);
matrixTransformationProcessor =
new MatrixTransformationProcessor((long presentationTimeUs) -> scaleNarrowMatrix);
matrixTransformationProcessor.initialize(getApplicationContext(), inputTexId, width, height);
matrixTransformationFrameProcessor =
new MatrixTransformationProcessor(
context, /* matrixTransformation= */ (long presentationTimeUs) -> scaleNarrowMatrix);
matrixTransformationFrameProcessor.configure(width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(SCALE_NARROW_PNG_ASSET_PATH);
matrixTransformationProcessor.drawFrame(/* presentationTimeUs= */ 0);
matrixTransformationFrameProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......@@ -159,12 +163,13 @@ public final class MatrixTransformationProcessorPixelTest {
String testId = "drawFrame_rotate90";
Matrix rotate90Matrix = new Matrix();
rotate90Matrix.postRotate(/* degrees= */ 90);
matrixTransformationProcessor =
new MatrixTransformationProcessor((long presentationTimeUs) -> rotate90Matrix);
matrixTransformationProcessor.initialize(getApplicationContext(), inputTexId, width, height);
matrixTransformationFrameProcessor =
new MatrixTransformationProcessor(
context, /* matrixTransformation= */ (long presentationTimeUs) -> rotate90Matrix);
matrixTransformationFrameProcessor.configure(width, height);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_PNG_ASSET_PATH);
matrixTransformationProcessor.drawFrame(/* presentationTimeUs= */ 0);
matrixTransformationFrameProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......
......@@ -24,7 +24,6 @@ import android.util.Size;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Copies frames from an external texture and applies color transformations for HDR if needed. */
/* package */ class ExternalTextureProcessor implements SingleFrameGlTextureProcessor {
......@@ -49,22 +48,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
1.683f, -0.652f, 0.0f,
};
private final boolean enableExperimentalHdrEditing;
private final GlProgram glProgram;
private @MonotonicNonNull Size size;
private @MonotonicNonNull GlProgram glProgram;
public ExternalTextureProcessor(boolean enableExperimentalHdrEditing) {
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
/**
* Creates a new instance.
*
* @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal.
* @throws IOException If a problem occurs while reading shader files.
*/
public ExternalTextureProcessor(Context context, boolean enableExperimentalHdrEditing)
throws IOException {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
size = new Size(inputWidth, inputHeight);
String vertexShaderFilePath =
enableExperimentalHdrEditing
? VERTEX_SHADER_TEX_TRANSFORM_ES3_PATH
......@@ -74,7 +67,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH
: FRAGMENT_SHADER_COPY_EXTERNAL_PATH;
glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath);
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",
......@@ -87,8 +79,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public Size getOutputSize() {
return checkStateNotNull(size);
public Size configure(int inputWidth, int inputHeight) {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
return new Size(inputWidth, inputHeight);
}
/**
......@@ -104,10 +99,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
checkStateNotNull(glProgram);
try {
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
......
......@@ -171,22 +171,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
ExternalTextureProcessor externalTextureProcessor =
new ExternalTextureProcessor(enableExperimentalHdrEditing);
new ExternalTextureProcessor(context, enableExperimentalHdrEditing);
ImmutableList<SingleFrameGlTextureProcessor> textureProcessors =
getTextureProcessors(externalTextureProcessor, pixelWidthHeightRatio, effects);
getTextureProcessors(context, externalTextureProcessor, pixelWidthHeightRatio, effects);
// Initialize texture processors.
int inputExternalTexId = GlUtil.createExternalTexture();
externalTextureProcessor.initialize(context, inputExternalTexId, inputWidth, inputHeight);
int[] framebuffers = new int[textureProcessors.size() - 1];
Size inputSize = externalTextureProcessor.getOutputSize();
Size outputSize = externalTextureProcessor.configure(inputWidth, inputHeight);
ImmutableList.Builder<TextureInfo> intermediateTextures = new ImmutableList.Builder<>();
for (int i = 1; i < textureProcessors.size(); i++) {
int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight());
framebuffers[i - 1] = GlUtil.createFboForTexture(inputTexId);
int texId = GlUtil.createTexture(outputSize.getWidth(), outputSize.getHeight());
int fboId = GlUtil.createFboForTexture(texId);
intermediateTextures.add(
new TextureInfo(texId, fboId, outputSize.getWidth(), outputSize.getHeight()));
SingleFrameGlTextureProcessor textureProcessor = textureProcessors.get(i);
textureProcessor.initialize(context, inputTexId, inputSize.getWidth(), inputSize.getHeight());
inputSize = textureProcessor.getOutputSize();
outputSize = textureProcessor.configure(outputSize.getWidth(), outputSize.getHeight());
}
return new FrameProcessorChain(
eglDisplay,
......@@ -194,16 +193,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
singleThreadExecutorService,
inputExternalTexId,
streamOffsetUs,
framebuffers,
intermediateTextures.build(),
textureProcessors,
outputSize,
listener,
enableExperimentalHdrEditing);
}
private static ImmutableList<SingleFrameGlTextureProcessor> getTextureProcessors(
Context context,
ExternalTextureProcessor externalTextureProcessor,
float pixelWidthHeightRatio,
List<GlEffect> effects) {
List<GlEffect> effects)
throws IOException {
ImmutableList.Builder<SingleFrameGlTextureProcessor> textureProcessors =
new ImmutableList.Builder<SingleFrameGlTextureProcessor>().add(externalTextureProcessor);
......@@ -233,15 +235,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ImmutableList<GlMatrixTransformation> matrixTransformations =
matrixTransformationListBuilder.build();
if (!matrixTransformations.isEmpty()) {
textureProcessors.add(new MatrixTransformationProcessor(matrixTransformations));
textureProcessors.add(new MatrixTransformationProcessor(context, matrixTransformations));
matrixTransformationListBuilder = new ImmutableList.Builder<>();
}
textureProcessors.add(effect.toGlTextureProcessor());
textureProcessors.add(effect.toGlTextureProcessor(context));
}
ImmutableList<GlMatrixTransformation> matrixTransformations =
matrixTransformationListBuilder.build();
if (!matrixTransformations.isEmpty()) {
textureProcessors.add(new MatrixTransformationProcessor(matrixTransformations));
textureProcessors.add(new MatrixTransformationProcessor(context, matrixTransformations));
}
return textureProcessors.build();
......@@ -265,11 +267,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final ConcurrentLinkedQueue<Future<?>> futures;
/** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */
private final AtomicInteger pendingFrameCount;
/** Wraps the {@link #inputSurfaceTexture}. */
private final Surface inputSurface;
/** Associated with an OpenGL external texture. */
private final SurfaceTexture inputSurfaceTexture;
/** Identifier of the OpenGL texture associated with the input {@link SurfaceTexture}. */
private final int inputExternalTexId;
/** Transformation matrix associated with the {@link #inputSurfaceTexture}. */
private final float[] textureTransformMatrix;
......@@ -278,12 +281,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* SingleFrameGlTextureProcessor SingleFrameGlTextureProcessors} at indices >= 1.
*/
private final ImmutableList<SingleFrameGlTextureProcessor> textureProcessors;
/**
* Identifiers of a framebuffer object associated with the intermediate textures that receive
* output from the previous {@link SingleFrameGlTextureProcessor}, and provide input for the
* following {@link SingleFrameGlTextureProcessor}.
* {@link TextureInfo} instances describing the intermediate textures that receive output from the
* previous {@link SingleFrameGlTextureProcessor}, and provide input for the following {@link
* SingleFrameGlTextureProcessor}.
*/
private final int[] framebuffers;
private final ImmutableList<TextureInfo> intermediateTextures;
/** The last texture processor's output {@link Size}. */
private final Size recommendedOutputSize;
private final Listener listener;
......@@ -318,8 +324,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ExecutorService singleThreadExecutorService,
int inputExternalTexId,
long streamOffsetUs,
int[] framebuffers,
ImmutableList<TextureInfo> intermediateTextures,
ImmutableList<SingleFrameGlTextureProcessor> textureProcessors,
Size recommendedOutputSize,
Listener listener,
boolean enableExperimentalHdrEditing) {
checkState(!textureProcessors.isEmpty());
......@@ -327,9 +334,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.eglDisplay = eglDisplay;
this.eglContext = eglContext;
this.singleThreadExecutorService = singleThreadExecutorService;
this.inputExternalTexId = inputExternalTexId;
this.streamOffsetUs = streamOffsetUs;
this.framebuffers = framebuffers;
this.intermediateTextures = intermediateTextures;
this.textureProcessors = textureProcessors;
this.recommendedOutputSize = recommendedOutputSize;
this.listener = listener;
this.stopProcessing = new AtomicBoolean();
this.enableExperimentalHdrEditing = enableExperimentalHdrEditing;
......@@ -350,7 +359,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* SurfaceView) output surface}.
*/
public Size getOutputSize() {
return getLast(textureProcessors).getOutputSize();
return recommendedOutputSize;
}
/**
......@@ -493,37 +502,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
((ExternalTextureProcessor) textureProcessors.get(0))
.setTextureTransformMatrix(textureTransformMatrix);
int inputTexId = inputExternalTexId;
for (int i = 0; i < textureProcessors.size() - 1; i++) {
if (stopProcessing.get()) {
return;
}
Size intermediateSize = textureProcessors.get(i).getOutputSize();
TextureInfo outputTexture = intermediateTextures.get(i);
GlUtil.focusFramebuffer(
eglDisplay,
eglContext,
outputEglSurface,
framebuffers[i],
intermediateSize.getWidth(),
intermediateSize.getHeight());
outputTexture.fboId,
outputTexture.width,
outputTexture.height);
clearOutputFrame();
textureProcessors.get(i).drawFrame(presentationTimeUs);
textureProcessors.get(i).drawFrame(inputTexId, presentationTimeUs);
inputTexId = outputTexture.texId;
}
GlUtil.focusEglSurface(eglDisplay, eglContext, outputEglSurface, outputWidth, outputHeight);
clearOutputFrame();
getLast(textureProcessors).drawFrame(presentationTimeUs);
getLast(textureProcessors).drawFrame(inputTexId, presentationTimeUs);
EGLExt.eglPresentationTimeANDROID(eglDisplay, outputEglSurface, inputFrameTimeNs);
EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
if (debugSurfaceViewWrapper != null) {
long framePresentationTimeUs = presentationTimeUs;
long finalPresentationTimeUs = presentationTimeUs;
int finalInputTexId = inputTexId;
debugSurfaceViewWrapper.maybeRenderToSurfaceView(
() -> {
clearOutputFrame();
try {
getLast(textureProcessors).drawFrame(framePresentationTimeUs);
getLast(textureProcessors).drawFrame(finalInputTexId, finalPresentationTimeUs);
} catch (FrameProcessingException e) {
Log.d(TAG, "Error rendering to debug preview", e);
}
......
......@@ -15,16 +15,19 @@
*/
package com.google.android.exoplayer2.transformer;
import android.content.Context;
import java.io.IOException;
/**
* Interface for a video frame effect with a {@link SingleFrameGlTextureProcessor} implementation.
*
* <p>Implementations contain information specifying the effect and can be {@linkplain
* #toGlTextureProcessor() converted} to a {@link SingleFrameGlTextureProcessor} which applies the
* effect.
* #toGlTextureProcessor(Context) converted} to a {@link SingleFrameGlTextureProcessor} which
* applies the effect.
*/
public interface GlEffect {
/** Returns a {@link SingleFrameGlTextureProcessor} that applies the effect. */
// TODO(b/227625423): use GlTextureProcessor here once this interface exists.
SingleFrameGlTextureProcessor toGlTextureProcessor();
SingleFrameGlTextureProcessor toGlTextureProcessor(Context context) throws IOException;
}
......@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.transformer;
import android.content.Context;
import android.opengl.Matrix;
import android.util.Size;
import java.io.IOException;
/**
* Specifies a 4x4 transformation {@link Matrix} to apply in the vertex shader for each frame.
......@@ -47,7 +49,7 @@ public interface GlMatrixTransformation extends GlEffect {
float[] getGlMatrixArray(long presentationTimeUs);
@Override
default SingleFrameGlTextureProcessor toGlTextureProcessor() {
return new MatrixTransformationProcessor(this);
default SingleFrameGlTextureProcessor toGlTextureProcessor(Context context) throws IOException {
return new MatrixTransformationProcessor(context, this);
}
}
......@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
......@@ -28,7 +27,6 @@ import com.google.android.exoplayer2.util.GlUtil;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Applies a sequence of transformation matrices in the vertex shader, and copies input pixels into
......@@ -82,37 +80,45 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
private ImmutableList<float[]> visiblePolygon;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
private final GlProgram glProgram;
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param matrixTransformation A {@link MatrixTransformation} that specifies the transformation
* matrix to use for each frame.
* @throws IOException If a problem occurs while reading shader files.
*/
public MatrixTransformationProcessor(MatrixTransformation matrixTransformation) {
this(ImmutableList.of(matrixTransformation));
public MatrixTransformationProcessor(Context context, MatrixTransformation matrixTransformation)
throws IOException {
this(context, ImmutableList.of(matrixTransformation));
}
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param matrixTransformation A {@link GlMatrixTransformation} that specifies the transformation
* matrix to use for each frame.
* @throws IOException If a problem occurs while reading shader files.
*/
public MatrixTransformationProcessor(GlMatrixTransformation matrixTransformation) {
this(ImmutableList.of(matrixTransformation));
public MatrixTransformationProcessor(Context context, GlMatrixTransformation matrixTransformation)
throws IOException {
this(context, ImmutableList.of(matrixTransformation));
}
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param matrixTransformations The {@link GlMatrixTransformation GlMatrixTransformations} to
* apply to each frame in order.
* @throws IOException If a problem occurs while reading shader files.
*/
public MatrixTransformationProcessor(
ImmutableList<GlMatrixTransformation> matrixTransformations) {
Context context, ImmutableList<GlMatrixTransformation> matrixTransformations)
throws IOException {
this.matrixTransformations = matrixTransformations;
transformationMatrixCache = new float[matrixTransformations.size()][16];
......@@ -120,38 +126,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
tempResultMatrix = new float[16];
Matrix.setIdentityM(compositeTransformationMatrix, /* smOffset= */ 0);
visiblePolygon = NDC_SQUARE;
glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH);
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
public Size configure(int inputWidth, int inputHeight) {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
outputSize = new Size(inputWidth, inputHeight);
Size outputSize = new Size(inputWidth, inputHeight);
for (int i = 0; i < matrixTransformations.size(); i++) {
outputSize =
matrixTransformations.get(i).configure(outputSize.getWidth(), outputSize.getHeight());
}
glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
return outputSize;
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
updateCompositeTransformationMatrixAndVisiblePolygon(presentationTimeUs);
if (visiblePolygon.size() < 3) {
return; // Need at least three visible vertices for a triangle.
}
try {
checkStateNotNull(glProgram).use();
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.setFloatsUniform("uTransformationMatrix", compositeTransformationMatrix);
glProgram.setBufferAttribute(
"aFramePosition",
......@@ -168,9 +169,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
glProgram.delete();
}
/**
......
......@@ -15,9 +15,7 @@
*/
package com.google.android.exoplayer2.transformer;
import android.content.Context;
import android.util.Size;
import java.io.IOException;
/**
* Manages a GLSL shader program for processing a frame. Implementations generally copy input pixels
......@@ -26,53 +24,44 @@ import java.io.IOException;
* <p>Methods must be called in the following order:
*
* <ol>
* <li>The constructor, for implementation-specific arguments.
* <li>{@link #initialize(Context, int, int, int)}, to set up graphics initialization.
* <li>{@link #drawFrame(long)}, to process one frame.
* <li>{@link #configure(int, int)}, to configure the frame processor based on the input
* dimensions.
* <li>{@link #drawFrame(int, long)}, to process one frame.
* <li>{@link #release()}, upon conclusion of processing.
* </ol>
*
* <p>All methods in this class must be called on the thread that owns the OpenGL context.
*/
// TODO(b/227625423): Add GlTextureProcessor interface for async texture processors and make this an
// abstract class with a default implementation of GlTextureProcessor methods.
public interface SingleFrameGlTextureProcessor {
/**
* Performs all initialization that requires OpenGL, such as, loading and compiling a GLSL shader
* program.
* Configures the texture processor based on the input dimensions.
*
* <p>This method may only be called if there is a current OpenGL context.
* <p>This method can be called multiple times.
*
* @param context The {@link Context}.
* @param inputTexId Identifier of a 2D OpenGL texture.
* @param inputWidth The input width, in pixels.
* @param inputHeight The input height, in pixels.
* @throws IOException If an error occurs while reading resources.
*/
void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException;
/**
* Returns the output {@link Size} of frames processed through {@link #drawFrame(long)}.
*
* <p>This method may only be called after the texture processor has been {@link
* #initialize(Context, int, int, int) initialized}.
* @return The output {@link Size} of frames processed through {@link #drawFrame(int, long)}.
*/
Size getOutputSize();
Size configure(int inputWidth, int inputHeight);
/**
* Draws one frame.
*
* <p>This method may only be called after the texture processor has been {@link
* #initialize(Context, int, int, int) initialized}. The caller is responsible for focussing the
* correct render target before calling this method.
* <p>This method may only be called after the texture processor has been {@link #configure(int,
* int) configured}. The caller is responsible for focussing the correct render target before
* calling this method.
*
* <p>A minimal implementation should tell OpenGL to use its shader program, bind the shader
* program's vertex attributes and uniforms, and issue a drawing command.
*
* @param inputTexId Identifier of a 2D OpenGL texture containing the input frame.
* @param presentationTimeUs The presentation timestamp of the current frame, in microseconds.
* @throws FrameProcessingException If an error occurs while processing or drawing the frame.
*/
void drawFrame(long presentationTimeUs) throws FrameProcessingException;
void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException;
/** Releases all resources. */
void release();
......
/*
* 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 com.google.android.exoplayer2.transformer;
/** Contains information describing an OpenGL texture. */
/* package */ final class TextureInfo {
/** The OpenGL texture identifier. */
public final int texId;
/** Identifier of a framebuffer object associated with the texture. */
public final int fboId;
/** The width of the texture, in pixels. */
public final int width;
/** The height of the texture, in pixels. */
public final int height;
/**
* Creates a new instance.
*
* @param texId The OpenGL texture identifier.
* @param fboId Identifier of a framebuffer object associated with the texture.
* @param width The width of the texture, in pixels.
* @param height The height of the texture, in pixels.
*/
public TextureInfo(int texId, int fboId, int width, int height) {
this.texId = texId;
this.fboId = fboId;
this.width = width;
this.height = height;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment