Compositor: Implement OverlaySettings and custom in/out size support.
Implement VideoCompositor support of: * Different input and output sizes * CompositorSettings, to customize output size based on input texture sizes * OverlaySettings, to place an input frame in an arbitrary position on the output frame. Also, refactor Overlay's matrix logic to make it more reusable between Compositor and Overlays PiperOrigin-RevId: 561931854
This commit is contained in:
parent
6c2713f153
commit
57bc215210
@ -36,6 +36,7 @@ import androidx.media3.common.util.GlProgram;
|
|||||||
import androidx.media3.common.util.GlUtil;
|
import androidx.media3.common.util.GlUtil;
|
||||||
import androidx.media3.common.util.Log;
|
import androidx.media3.common.util.Log;
|
||||||
import androidx.media3.common.util.LongArrayQueue;
|
import androidx.media3.common.util.LongArrayQueue;
|
||||||
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -50,8 +51,8 @@ import java.util.concurrent.ExecutorService;
|
|||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic {@link VideoCompositor} implementation that takes in frames from exactly 2 input sources'
|
* A basic {@link VideoCompositor} implementation that takes in frames from exactly 2 SDR input
|
||||||
* streams and combines them into one output stream.
|
* sources' streams and combines them into one output stream.
|
||||||
*
|
*
|
||||||
* <p>The first {@linkplain #registerInputSource registered source} will be the primary stream,
|
* <p>The first {@linkplain #registerInputSource registered source} will be the primary stream,
|
||||||
* which is used to determine the output frames' timestamps and dimensions.
|
* which is used to determine the output frames' timestamps and dimensions.
|
||||||
@ -62,17 +63,33 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
// * Use a lock to synchronize inputFrameInfos more narrowly, to reduce blocking.
|
// * Use a lock to synchronize inputFrameInfos more narrowly, to reduce blocking.
|
||||||
// * If the primary stream ends, consider setting the secondary stream as the new primary stream,
|
// * If the primary stream ends, consider setting the secondary stream as the new primary stream,
|
||||||
// so that secondary stream frames aren't dropped.
|
// so that secondary stream frames aren't dropped.
|
||||||
|
// * Add support for HDR input.
|
||||||
|
|
||||||
|
/** A default implementation of {@link VideoCompositor.Settings}. */
|
||||||
|
public static final class Settings implements VideoCompositor.Settings {
|
||||||
|
@Override
|
||||||
|
public Size getOutputSize(List<Size> inputSizes) {
|
||||||
|
return inputSizes.get(PRIMARY_INPUT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs) {
|
||||||
|
return new OverlaySettings.Builder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final String THREAD_NAME = "Effect:DefaultVideoCompositor:GlThread";
|
private static final String THREAD_NAME = "Effect:DefaultVideoCompositor:GlThread";
|
||||||
private static final String TAG = "DefaultVideoCompositor";
|
private static final String TAG = "DefaultVideoCompositor";
|
||||||
private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl";
|
private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl";
|
||||||
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl";
|
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_alpha_scale_es2.glsl";
|
||||||
private static final int PRIMARY_INPUT_ID = 0;
|
private static final int PRIMARY_INPUT_ID = 0;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final VideoCompositor.Listener listener;
|
private final VideoCompositor.Listener listener;
|
||||||
private final GlTextureProducer.Listener textureOutputListener;
|
private final GlTextureProducer.Listener textureOutputListener;
|
||||||
private final GlObjectsProvider glObjectsProvider;
|
private final GlObjectsProvider glObjectsProvider;
|
||||||
|
private final VideoCompositor.Settings settings;
|
||||||
|
private final OverlayMatrixProvider overlayMatrixProvider;
|
||||||
private final VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor;
|
private final VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor;
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
@ -100,6 +117,7 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
public DefaultVideoCompositor(
|
public DefaultVideoCompositor(
|
||||||
Context context,
|
Context context,
|
||||||
GlObjectsProvider glObjectsProvider,
|
GlObjectsProvider glObjectsProvider,
|
||||||
|
VideoCompositor.Settings settings,
|
||||||
@Nullable ExecutorService executorService,
|
@Nullable ExecutorService executorService,
|
||||||
VideoCompositor.Listener listener,
|
VideoCompositor.Listener listener,
|
||||||
GlTextureProducer.Listener textureOutputListener,
|
GlTextureProducer.Listener textureOutputListener,
|
||||||
@ -108,6 +126,8 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.textureOutputListener = textureOutputListener;
|
this.textureOutputListener = textureOutputListener;
|
||||||
this.glObjectsProvider = glObjectsProvider;
|
this.glObjectsProvider = glObjectsProvider;
|
||||||
|
this.settings = settings;
|
||||||
|
this.overlayMatrixProvider = new OverlayMatrixProvider();
|
||||||
|
|
||||||
inputSources = new ArrayList<>();
|
inputSources = new ArrayList<>();
|
||||||
outputTexturePool =
|
outputTexturePool =
|
||||||
@ -180,7 +200,11 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
checkState(!inputSource.isInputEnded);
|
checkState(!inputSource.isInputEnded);
|
||||||
|
|
||||||
InputFrameInfo inputFrameInfo =
|
InputFrameInfo inputFrameInfo =
|
||||||
new InputFrameInfo(textureProducer, inputTexture, presentationTimeUs);
|
new InputFrameInfo(
|
||||||
|
textureProducer,
|
||||||
|
inputTexture,
|
||||||
|
presentationTimeUs,
|
||||||
|
settings.getOverlaySettings(inputId, presentationTimeUs));
|
||||||
inputSource.frameInfos.add(inputFrameInfo);
|
inputSource.frameInfos.add(inputFrameInfo);
|
||||||
|
|
||||||
if (inputId == PRIMARY_INPUT_ID) {
|
if (inputId == PRIMARY_INPUT_ID) {
|
||||||
@ -277,17 +301,17 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
|
|
||||||
ensureGlProgramConfigured();
|
ensureGlProgramConfigured();
|
||||||
|
|
||||||
// TODO: b/262694346 - Allow different input frame dimensions.
|
|
||||||
InputFrameInfo primaryInputFrame = framesToComposite.get(PRIMARY_INPUT_ID);
|
InputFrameInfo primaryInputFrame = framesToComposite.get(PRIMARY_INPUT_ID);
|
||||||
GlTextureInfo primaryInputTexture = primaryInputFrame.texture;
|
|
||||||
outputTexturePool.ensureConfigured(
|
|
||||||
glObjectsProvider, primaryInputTexture.width, primaryInputTexture.height);
|
|
||||||
|
|
||||||
for (int i = 1; i < framesToComposite.size(); i++) {
|
ImmutableList.Builder<Size> inputSizes = new ImmutableList.Builder<>();
|
||||||
GlTextureInfo textureToComposite = framesToComposite.get(i).texture;
|
for (int i = 0; i < framesToComposite.size(); i++) {
|
||||||
checkState(primaryInputTexture.width == textureToComposite.width);
|
GlTextureInfo texture = framesToComposite.get(i).texture;
|
||||||
checkState(primaryInputTexture.height == textureToComposite.height);
|
inputSizes.add(new Size(texture.width, texture.height));
|
||||||
}
|
}
|
||||||
|
Size outputSize = settings.getOutputSize(inputSizes.build());
|
||||||
|
outputTexturePool.ensureConfigured(
|
||||||
|
glObjectsProvider, outputSize.getWidth(), outputSize.getHeight());
|
||||||
|
|
||||||
GlTextureInfo outputTexture = outputTexturePool.useTexture();
|
GlTextureInfo outputTexture = outputTexturePool.useTexture();
|
||||||
long outputPresentationTimestampUs = primaryInputFrame.presentationTimeUs;
|
long outputPresentationTimestampUs = primaryInputFrame.presentationTimeUs;
|
||||||
outputTextureTimestamps.add(outputPresentationTimestampUs);
|
outputTextureTimestamps.add(outputPresentationTimestampUs);
|
||||||
@ -394,16 +418,18 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
GlUtil.getNormalizedCoordinateBounds(),
|
GlUtil.getNormalizedCoordinateBounds(),
|
||||||
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||||
glProgram.setFloatsUniform("uTexTransformationMatrix", GlUtil.create4x4IdentityMatrix());
|
glProgram.setFloatsUniform("uTexTransformationMatrix", GlUtil.create4x4IdentityMatrix());
|
||||||
glProgram.setFloatsUniform("uTransformationMatrix", GlUtil.create4x4IdentityMatrix());
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new VideoFrameProcessingException(e);
|
throw new VideoFrameProcessingException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced for-loops are discouraged in media3.effect due to short-lived allocations.
|
||||||
|
@SuppressWarnings("ListReverse")
|
||||||
private void drawFrame(List<InputFrameInfo> framesToComposite, GlTextureInfo outputTexture)
|
private void drawFrame(List<InputFrameInfo> framesToComposite, GlTextureInfo outputTexture)
|
||||||
throws GlUtil.GlException {
|
throws GlUtil.GlException {
|
||||||
GlUtil.focusFramebufferUsingCurrentContext(
|
GlUtil.focusFramebufferUsingCurrentContext(
|
||||||
outputTexture.fboId, outputTexture.width, outputTexture.height);
|
outputTexture.fboId, outputTexture.width, outputTexture.height);
|
||||||
|
overlayMatrixProvider.configure(new Size(outputTexture.width, outputTexture.height));
|
||||||
GlUtil.clearFocusedBuffers();
|
GlUtil.clearFocusedBuffers();
|
||||||
|
|
||||||
GlProgram glProgram = checkNotNull(this.glProgram);
|
GlProgram glProgram = checkNotNull(this.glProgram);
|
||||||
@ -423,7 +449,7 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
|
|
||||||
// Draw textures from back to front.
|
// Draw textures from back to front.
|
||||||
for (int i = framesToComposite.size() - 1; i >= 0; i--) {
|
for (int i = framesToComposite.size() - 1; i >= 0; i--) {
|
||||||
blendOntoFocusedTexture(framesToComposite.get(i).texture.texId);
|
blendOntoFocusedTexture(framesToComposite.get(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
GLES20.glDisable(GLES20.GL_BLEND);
|
GLES20.glDisable(GLES20.GL_BLEND);
|
||||||
@ -431,9 +457,16 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
GlUtil.checkGlError();
|
GlUtil.checkGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void blendOntoFocusedTexture(int texId) throws GlUtil.GlException {
|
private void blendOntoFocusedTexture(InputFrameInfo inputFrameInfo) throws GlUtil.GlException {
|
||||||
GlProgram glProgram = checkNotNull(this.glProgram);
|
GlProgram glProgram = checkNotNull(this.glProgram);
|
||||||
glProgram.setSamplerTexIdUniform("uTexSampler", texId, /* texUnitIndex= */ 0);
|
GlTextureInfo inputTexture = inputFrameInfo.texture;
|
||||||
|
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexture.texId, /* texUnitIndex= */ 0);
|
||||||
|
float[] transformationMatrix =
|
||||||
|
overlayMatrixProvider.getTransformationMatrix(
|
||||||
|
/* overlaySize= */ new Size(inputTexture.width, inputTexture.height),
|
||||||
|
inputFrameInfo.overlaySettings);
|
||||||
|
glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrix);
|
||||||
|
glProgram.setFloatUniform("uAlphaScale", inputFrameInfo.overlaySettings.alphaScale);
|
||||||
glProgram.bindAttributesAndUniforms();
|
glProgram.bindAttributesAndUniforms();
|
||||||
|
|
||||||
// The four-vertex triangle strip forms a quad.
|
// The four-vertex triangle strip forms a quad.
|
||||||
@ -480,12 +513,17 @@ public final class DefaultVideoCompositor implements VideoCompositor {
|
|||||||
public final GlTextureProducer textureProducer;
|
public final GlTextureProducer textureProducer;
|
||||||
public final GlTextureInfo texture;
|
public final GlTextureInfo texture;
|
||||||
public final long presentationTimeUs;
|
public final long presentationTimeUs;
|
||||||
|
public final OverlaySettings overlaySettings;
|
||||||
|
|
||||||
public InputFrameInfo(
|
public InputFrameInfo(
|
||||||
GlTextureProducer textureProducer, GlTextureInfo texture, long presentationTimeUs) {
|
GlTextureProducer textureProducer,
|
||||||
|
GlTextureInfo texture,
|
||||||
|
long presentationTimeUs,
|
||||||
|
OverlaySettings overlaySettings) {
|
||||||
this.textureProducer = textureProducer;
|
this.textureProducer = textureProducer;
|
||||||
this.texture = texture;
|
this.texture = texture;
|
||||||
this.presentationTimeUs = presentationTimeUs;
|
this.presentationTimeUs = presentationTimeUs;
|
||||||
|
this.overlaySettings = overlaySettings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.effect;
|
package androidx.media3.effect;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
|
||||||
import android.opengl.Matrix;
|
import android.opengl.Matrix;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
@ -23,15 +23,14 @@ import androidx.media3.common.util.GlUtil;
|
|||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/* package */ final class OverlayMatrixProvider {
|
/** Provides a matrix for {@link OverlaySettings}, to be applied on a vertex. */
|
||||||
private static final int MATRIX_OFFSET = 0;
|
/* package */ class OverlayMatrixProvider {
|
||||||
|
protected static final int MATRIX_OFFSET = 0;
|
||||||
private final float[] videoFrameAnchorMatrix;
|
private final float[] videoFrameAnchorMatrix;
|
||||||
private final float[] videoFrameAnchorMatrixInv;
|
|
||||||
private final float[] aspectRatioMatrix;
|
private final float[] aspectRatioMatrix;
|
||||||
private final float[] scaleMatrix;
|
private final float[] scaleMatrix;
|
||||||
private final float[] scaleMatrixInv;
|
private final float[] scaleMatrixInv;
|
||||||
private final float[] overlayAnchorMatrix;
|
private final float[] overlayAnchorMatrix;
|
||||||
private final float[] overlayAnchorMatrixInv;
|
|
||||||
private final float[] rotateMatrix;
|
private final float[] rotateMatrix;
|
||||||
private final float[] overlayAspectRatioMatrix;
|
private final float[] overlayAspectRatioMatrix;
|
||||||
private final float[] overlayAspectRatioMatrixInv;
|
private final float[] overlayAspectRatioMatrixInv;
|
||||||
@ -41,9 +40,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
public OverlayMatrixProvider() {
|
public OverlayMatrixProvider() {
|
||||||
aspectRatioMatrix = GlUtil.create4x4IdentityMatrix();
|
aspectRatioMatrix = GlUtil.create4x4IdentityMatrix();
|
||||||
videoFrameAnchorMatrix = GlUtil.create4x4IdentityMatrix();
|
videoFrameAnchorMatrix = GlUtil.create4x4IdentityMatrix();
|
||||||
videoFrameAnchorMatrixInv = GlUtil.create4x4IdentityMatrix();
|
|
||||||
overlayAnchorMatrix = GlUtil.create4x4IdentityMatrix();
|
overlayAnchorMatrix = GlUtil.create4x4IdentityMatrix();
|
||||||
overlayAnchorMatrixInv = GlUtil.create4x4IdentityMatrix();
|
|
||||||
rotateMatrix = GlUtil.create4x4IdentityMatrix();
|
rotateMatrix = GlUtil.create4x4IdentityMatrix();
|
||||||
scaleMatrix = GlUtil.create4x4IdentityMatrix();
|
scaleMatrix = GlUtil.create4x4IdentityMatrix();
|
||||||
scaleMatrixInv = GlUtil.create4x4IdentityMatrix();
|
scaleMatrixInv = GlUtil.create4x4IdentityMatrix();
|
||||||
@ -56,6 +53,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
this.backgroundSize = backgroundSize;
|
this.backgroundSize = backgroundSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the transformation matrix.
|
||||||
|
*
|
||||||
|
* <p>This instance must be {@linkplain #configure configured} before this method is called.
|
||||||
|
*/
|
||||||
public float[] getTransformationMatrix(Size overlaySize, OverlaySettings overlaySettings) {
|
public float[] getTransformationMatrix(Size overlaySize, OverlaySettings overlaySettings) {
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
@ -67,44 +69,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
videoFrameAnchor.first,
|
videoFrameAnchor.first,
|
||||||
videoFrameAnchor.second,
|
videoFrameAnchor.second,
|
||||||
/* z= */ 0f);
|
/* z= */ 0f);
|
||||||
Matrix.invertM(videoFrameAnchorMatrixInv, MATRIX_OFFSET, videoFrameAnchorMatrix, MATRIX_OFFSET);
|
|
||||||
|
|
||||||
|
checkStateNotNull(backgroundSize);
|
||||||
Matrix.scaleM(
|
Matrix.scaleM(
|
||||||
aspectRatioMatrix,
|
aspectRatioMatrix,
|
||||||
MATRIX_OFFSET,
|
MATRIX_OFFSET,
|
||||||
checkNotNull(backgroundSize).getWidth() / (float) overlaySize.getWidth(),
|
(float) overlaySize.getWidth() / backgroundSize.getWidth(),
|
||||||
checkNotNull(backgroundSize).getHeight() / (float) overlaySize.getHeight(),
|
(float) overlaySize.getHeight() / backgroundSize.getHeight(),
|
||||||
/* z= */ 1f);
|
/* z= */ 1f);
|
||||||
|
|
||||||
// Scale the image.
|
// Scale the image.
|
||||||
Pair<Float, Float> scale = overlaySettings.scale;
|
Pair<Float, Float> scale = overlaySettings.scale;
|
||||||
Matrix.scaleM(
|
Matrix.scaleM(scaleMatrix, MATRIX_OFFSET, scale.first, scale.second, /* z= */ 1f);
|
||||||
scaleMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
scaleMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
scale.first,
|
|
||||||
scale.second,
|
|
||||||
/* z= */ 1f);
|
|
||||||
Matrix.invertM(scaleMatrixInv, MATRIX_OFFSET, scaleMatrix, MATRIX_OFFSET);
|
Matrix.invertM(scaleMatrixInv, MATRIX_OFFSET, scaleMatrix, MATRIX_OFFSET);
|
||||||
|
|
||||||
// Translate the overlay within its frame.
|
// Translate the overlay within its frame. To position the overlay's anchor at the correct
|
||||||
|
// position, it must be translated the opposite direction by the same magnitude.
|
||||||
Pair<Float, Float> overlayAnchor = overlaySettings.overlayAnchor;
|
Pair<Float, Float> overlayAnchor = overlaySettings.overlayAnchor;
|
||||||
Matrix.translateM(
|
Matrix.translateM(
|
||||||
overlayAnchorMatrix, MATRIX_OFFSET, overlayAnchor.first, overlayAnchor.second, /* z= */ 0f);
|
overlayAnchorMatrix,
|
||||||
Matrix.invertM(overlayAnchorMatrixInv, MATRIX_OFFSET, overlayAnchorMatrix, MATRIX_OFFSET);
|
MATRIX_OFFSET,
|
||||||
|
-1 * overlayAnchor.first,
|
||||||
|
-1 * overlayAnchor.second,
|
||||||
|
/* z= */ 0f);
|
||||||
|
|
||||||
// Rotate the image.
|
// Rotate the image.
|
||||||
Matrix.rotateM(
|
Matrix.rotateM(
|
||||||
rotateMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
rotateMatrix,
|
rotateMatrix,
|
||||||
MATRIX_OFFSET,
|
MATRIX_OFFSET,
|
||||||
overlaySettings.rotationDegrees,
|
overlaySettings.rotationDegrees,
|
||||||
/* x= */ 0f,
|
/* x= */ 0f,
|
||||||
/* y= */ 0f,
|
/* y= */ 0f,
|
||||||
/* z= */ 1f);
|
/* z= */ 1f);
|
||||||
Matrix.invertM(rotateMatrix, MATRIX_OFFSET, rotateMatrix, MATRIX_OFFSET);
|
|
||||||
|
|
||||||
// Rotation matrix needs to account for overlay aspect ratio to prevent stretching.
|
// Rotation matrix needs to account for overlay aspect ratio to prevent stretching.
|
||||||
Matrix.scaleM(
|
Matrix.scaleM(
|
||||||
@ -116,67 +112,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
Matrix.invertM(
|
Matrix.invertM(
|
||||||
overlayAspectRatioMatrixInv, MATRIX_OFFSET, overlayAspectRatioMatrix, MATRIX_OFFSET);
|
overlayAspectRatioMatrixInv, MATRIX_OFFSET, overlayAspectRatioMatrix, MATRIX_OFFSET);
|
||||||
|
|
||||||
// Rotation needs to be agnostic of the scaling matrix and the aspect ratios.
|
// transformationMatrix = videoFrameAnchorMatrix * aspectRatioMatrix
|
||||||
// transformationMatrix = scaleMatrixInv * overlayAspectRatioMatrix * rotateMatrix *
|
// * scaleMatrix * overlayAnchorMatrix * scaleMatrixInv
|
||||||
// overlayAspectRatioInv * scaleMatrix * overlayAnchorMatrixInv * scaleMatrixInv *
|
// * overlayAspectRatioMatrix * rotateMatrix * overlayAspectRatioMatrixInv
|
||||||
// aspectRatioMatrix * videoFrameAnchorMatrixInv
|
// * scaleMatrix.
|
||||||
Matrix.multiplyMM(
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
scaleMatrixInv,
|
|
||||||
MATRIX_OFFSET);
|
|
||||||
|
|
||||||
|
// Anchor position in output frame.
|
||||||
Matrix.multiplyMM(
|
Matrix.multiplyMM(
|
||||||
transformationMatrix,
|
transformationMatrix,
|
||||||
MATRIX_OFFSET,
|
MATRIX_OFFSET,
|
||||||
transformationMatrix,
|
transformationMatrix,
|
||||||
MATRIX_OFFSET,
|
MATRIX_OFFSET,
|
||||||
overlayAspectRatioMatrix,
|
videoFrameAnchorMatrix,
|
||||||
MATRIX_OFFSET);
|
|
||||||
|
|
||||||
// Rotation matrix.
|
|
||||||
Matrix.multiplyMM(
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
rotateMatrix,
|
|
||||||
MATRIX_OFFSET);
|
|
||||||
|
|
||||||
Matrix.multiplyMM(
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
overlayAspectRatioMatrixInv,
|
|
||||||
MATRIX_OFFSET);
|
|
||||||
|
|
||||||
Matrix.multiplyMM(
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
scaleMatrix,
|
|
||||||
MATRIX_OFFSET);
|
|
||||||
|
|
||||||
// Translate image.
|
|
||||||
Matrix.multiplyMM(
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
overlayAnchorMatrixInv,
|
|
||||||
MATRIX_OFFSET);
|
|
||||||
|
|
||||||
// Scale image.
|
|
||||||
Matrix.multiplyMM(
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
transformationMatrix,
|
|
||||||
MATRIX_OFFSET,
|
|
||||||
scaleMatrixInv,
|
|
||||||
MATRIX_OFFSET);
|
MATRIX_OFFSET);
|
||||||
|
|
||||||
// Correct for aspect ratio of image in output frame.
|
// Correct for aspect ratio of image in output frame.
|
||||||
@ -188,23 +135,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
aspectRatioMatrix,
|
aspectRatioMatrix,
|
||||||
MATRIX_OFFSET);
|
MATRIX_OFFSET);
|
||||||
|
|
||||||
// Anchor position in output frame.
|
|
||||||
Matrix.multiplyMM(
|
Matrix.multiplyMM(
|
||||||
transformationMatrix,
|
transformationMatrix,
|
||||||
MATRIX_OFFSET,
|
MATRIX_OFFSET,
|
||||||
transformationMatrix,
|
transformationMatrix,
|
||||||
MATRIX_OFFSET,
|
MATRIX_OFFSET,
|
||||||
videoFrameAnchorMatrixInv,
|
scaleMatrix,
|
||||||
MATRIX_OFFSET);
|
MATRIX_OFFSET);
|
||||||
|
Matrix.multiplyMM(
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
overlayAnchorMatrix,
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
Matrix.multiplyMM(
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
scaleMatrixInv,
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
|
||||||
|
// Rotation needs to be agnostic of the scaling matrix and the aspect ratios.
|
||||||
|
Matrix.multiplyMM(
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
overlayAspectRatioMatrix,
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
Matrix.multiplyMM(
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
rotateMatrix,
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
Matrix.multiplyMM(
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
overlayAspectRatioMatrixInv,
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
|
||||||
|
// Scale image.
|
||||||
|
Matrix.multiplyMM(
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
transformationMatrix,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
scaleMatrix,
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
|
||||||
return transformationMatrix;
|
return transformationMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
GlUtil.setToIdentity(aspectRatioMatrix);
|
GlUtil.setToIdentity(aspectRatioMatrix);
|
||||||
GlUtil.setToIdentity(videoFrameAnchorMatrix);
|
GlUtil.setToIdentity(videoFrameAnchorMatrix);
|
||||||
GlUtil.setToIdentity(videoFrameAnchorMatrixInv);
|
|
||||||
GlUtil.setToIdentity(overlayAnchorMatrix);
|
GlUtil.setToIdentity(overlayAnchorMatrix);
|
||||||
GlUtil.setToIdentity(overlayAnchorMatrixInv);
|
|
||||||
GlUtil.setToIdentity(scaleMatrix);
|
GlUtil.setToIdentity(scaleMatrix);
|
||||||
GlUtil.setToIdentity(scaleMatrixInv);
|
GlUtil.setToIdentity(scaleMatrixInv);
|
||||||
GlUtil.setToIdentity(rotateMatrix);
|
GlUtil.setToIdentity(rotateMatrix);
|
||||||
|
@ -22,7 +22,10 @@ import androidx.annotation.FloatRange;
|
|||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
|
||||||
/** Contains information to control how a {@link TextureOverlay} is displayed on the screen. */
|
/**
|
||||||
|
* Contains information to control how an input texture (for example, a {@link VideoCompositor} or
|
||||||
|
* {@link TextureOverlay}) is displayed on a background.
|
||||||
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class OverlaySettings {
|
public final class OverlaySettings {
|
||||||
public final boolean useHdr;
|
public final boolean useHdr;
|
||||||
@ -47,6 +50,11 @@ public final class OverlaySettings {
|
|||||||
this.rotationDegrees = rotationDegrees;
|
this.rotationDegrees = rotationDegrees;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a new {@link Builder} initialized with the values of this instance. */
|
||||||
|
/* package */ Builder buildUpon() {
|
||||||
|
return new Builder(this);
|
||||||
|
}
|
||||||
|
|
||||||
/** A builder for {@link OverlaySettings} instances. */
|
/** A builder for {@link OverlaySettings} instances. */
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private boolean useHdr;
|
private boolean useHdr;
|
||||||
@ -65,6 +73,15 @@ public final class OverlaySettings {
|
|||||||
rotationDegrees = 0f;
|
rotationDegrees = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Builder(OverlaySettings overlaySettings) {
|
||||||
|
this.useHdr = overlaySettings.useHdr;
|
||||||
|
this.alphaScale = overlaySettings.alphaScale;
|
||||||
|
this.videoFrameAnchor = overlaySettings.videoFrameAnchor;
|
||||||
|
this.overlayAnchor = overlaySettings.overlayAnchor;
|
||||||
|
this.scale = overlaySettings.scale;
|
||||||
|
this.rotationDegrees = overlaySettings.rotationDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether input overlay comes from an HDR source. If {@code true}, colors will be in
|
* Sets whether input overlay comes from an HDR source. If {@code true}, colors will be in
|
||||||
* linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
|
* linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
|
||||||
@ -92,6 +109,7 @@ public final class OverlaySettings {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: b/262694346 - Rename this method to setBackgroundAnchor in a follow-up CL.
|
||||||
/**
|
/**
|
||||||
* Sets the coordinates for the anchor point of the overlay within the video frame.
|
* Sets the coordinates for the anchor point of the overlay within the video frame.
|
||||||
*
|
*
|
||||||
|
@ -29,7 +29,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
/* package */ final class OverlayShaderProgram extends BaseGlShaderProgram {
|
/* package */ final class OverlayShaderProgram extends BaseGlShaderProgram {
|
||||||
|
|
||||||
private final GlProgram glProgram;
|
private final GlProgram glProgram;
|
||||||
private final OverlayMatrixProvider overlayMatrixProvider;
|
private final SamplerOverlayMatrixProvider samplerOverlayMatrixProvider;
|
||||||
private final ImmutableList<TextureOverlay> overlays;
|
private final ImmutableList<TextureOverlay> overlays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,7 +49,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
overlays.size() <= 15,
|
overlays.size() <= 15,
|
||||||
"OverlayShaderProgram does not support more than 15 overlays in the same instance.");
|
"OverlayShaderProgram does not support more than 15 overlays in the same instance.");
|
||||||
this.overlays = overlays;
|
this.overlays = overlays;
|
||||||
this.overlayMatrixProvider = new OverlayMatrixProvider();
|
this.samplerOverlayMatrixProvider = new SamplerOverlayMatrixProvider();
|
||||||
try {
|
try {
|
||||||
glProgram =
|
glProgram =
|
||||||
new GlProgram(createVertexShader(overlays.size()), createFragmentShader(overlays.size()));
|
new GlProgram(createVertexShader(overlays.size()), createFragmentShader(overlays.size()));
|
||||||
@ -66,7 +66,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
@Override
|
@Override
|
||||||
public Size configure(int inputWidth, int inputHeight) {
|
public Size configure(int inputWidth, int inputHeight) {
|
||||||
Size videoSize = new Size(inputWidth, inputHeight);
|
Size videoSize = new Size(inputWidth, inputHeight);
|
||||||
overlayMatrixProvider.configure(/* backgroundSize= */ videoSize);
|
samplerOverlayMatrixProvider.configure(/* backgroundSize= */ videoSize);
|
||||||
for (TextureOverlay overlay : overlays) {
|
for (TextureOverlay overlay : overlays) {
|
||||||
overlay.configure(videoSize);
|
overlay.configure(videoSize);
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
|
|
||||||
glProgram.setFloatsUniform(
|
glProgram.setFloatsUniform(
|
||||||
Util.formatInvariant("uTransformationMatrix%d", texUnitIndex),
|
Util.formatInvariant("uTransformationMatrix%d", texUnitIndex),
|
||||||
overlayMatrixProvider.getTransformationMatrix(overlaySize, overlaySettings));
|
samplerOverlayMatrixProvider.getTransformationMatrix(overlaySize, overlaySettings));
|
||||||
|
|
||||||
glProgram.setFloatUniform(
|
glProgram.setFloatUniform(
|
||||||
Util.formatInvariant("uOverlayAlphaScale%d", texUnitIndex),
|
Util.formatInvariant("uOverlayAlphaScale%d", texUnitIndex),
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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.effect;
|
||||||
|
|
||||||
|
import android.opengl.Matrix;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a matrix based on {@link OverlaySettings} to be applied on a texture sampling
|
||||||
|
* coordinate.
|
||||||
|
*/
|
||||||
|
/* package */ final class SamplerOverlayMatrixProvider extends OverlayMatrixProvider {
|
||||||
|
private final float[] transformationMatrixInv;
|
||||||
|
|
||||||
|
public SamplerOverlayMatrixProvider() {
|
||||||
|
super();
|
||||||
|
transformationMatrixInv = GlUtil.create4x4IdentityMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float[] getTransformationMatrix(Size overlaySize, OverlaySettings overlaySettings) {
|
||||||
|
// When sampling from a (for example, texture) sampler, the overlay anchor's x and y coordinates
|
||||||
|
// are flipped.
|
||||||
|
OverlaySettings samplerOverlaySettings =
|
||||||
|
overlaySettings
|
||||||
|
.buildUpon()
|
||||||
|
.setOverlayAnchor(
|
||||||
|
/* x= */ -1 * overlaySettings.overlayAnchor.first,
|
||||||
|
/* y= */ -1 * overlaySettings.overlayAnchor.second)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When sampling from a (for example, texture) sampler, the transformation matrix applied to a
|
||||||
|
// sampler's coordinate should be the inverse of the transformation matrix that would otherwise
|
||||||
|
// be applied to a vertex.
|
||||||
|
Matrix.invertM(
|
||||||
|
transformationMatrixInv,
|
||||||
|
MATRIX_OFFSET,
|
||||||
|
super.getTransformationMatrix(overlaySize, samplerOverlaySettings),
|
||||||
|
MATRIX_OFFSET);
|
||||||
|
return transformationMatrixInv;
|
||||||
|
}
|
||||||
|
}
|
@ -17,10 +17,12 @@ package androidx.media3.effect;
|
|||||||
|
|
||||||
import androidx.media3.common.GlTextureInfo;
|
import androidx.media3.common.GlTextureInfo;
|
||||||
import androidx.media3.common.VideoFrameProcessingException;
|
import androidx.media3.common.VideoFrameProcessingException;
|
||||||
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a video compositor that combines frames from mutliple input sources to produce
|
* Interface for a video compositor that combines frames from multiple input sources to produce
|
||||||
* output frames.
|
* output frames.
|
||||||
*
|
*
|
||||||
* <p>Input and output are provided via OpenGL textures.
|
* <p>Input and output are provided via OpenGL textures.
|
||||||
@ -41,6 +43,23 @@ public interface VideoCompositor extends GlTextureProducer {
|
|||||||
void onEnded();
|
void onEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Settings for the {@link VideoCompositor}. */
|
||||||
|
interface Settings {
|
||||||
|
// TODO: b/262694346 - Consider adding more features, like selecting a:
|
||||||
|
// * custom order for drawing (instead of primary stream on top), and
|
||||||
|
// * different primary source.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an output texture {@link Size}, based on {@code inputSizes}.
|
||||||
|
*
|
||||||
|
* @param inputSizes The {@link Size} of each input frame, ordered by {@code inputId}.
|
||||||
|
*/
|
||||||
|
Size getOutputSize(List<Size> inputSizes);
|
||||||
|
|
||||||
|
/** Returns {@link OverlaySettings} for {@code inputId} at time {@code presentationTimeUs}. */
|
||||||
|
OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new input source, and returns a unique {@code inputId} corresponding to this
|
* Registers a new input source, and returns a unique {@code inputId} corresponding to this
|
||||||
* source, to be used in {@link #queueInputTexture}.
|
* source, to be used in {@link #queueInputTexture}.
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -24,6 +24,7 @@ import static androidx.media3.test.utils.VideoFrameProcessorTestRunner.createTim
|
|||||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
import static java.lang.Math.max;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@ -44,6 +45,7 @@ import androidx.media3.common.Effect;
|
|||||||
import androidx.media3.common.GlObjectsProvider;
|
import androidx.media3.common.GlObjectsProvider;
|
||||||
import androidx.media3.common.VideoFrameProcessingException;
|
import androidx.media3.common.VideoFrameProcessingException;
|
||||||
import androidx.media3.common.util.GlUtil;
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.effect.AlphaScale;
|
import androidx.media3.effect.AlphaScale;
|
||||||
import androidx.media3.effect.DefaultGlObjectsProvider;
|
import androidx.media3.effect.DefaultGlObjectsProvider;
|
||||||
@ -51,6 +53,7 @@ import androidx.media3.effect.DefaultVideoCompositor;
|
|||||||
import androidx.media3.effect.DefaultVideoFrameProcessor;
|
import androidx.media3.effect.DefaultVideoFrameProcessor;
|
||||||
import androidx.media3.effect.OverlayEffect;
|
import androidx.media3.effect.OverlayEffect;
|
||||||
import androidx.media3.effect.OverlaySettings;
|
import androidx.media3.effect.OverlaySettings;
|
||||||
|
import androidx.media3.effect.Presentation;
|
||||||
import androidx.media3.effect.RgbFilter;
|
import androidx.media3.effect.RgbFilter;
|
||||||
import androidx.media3.effect.ScaleAndRotateTransformation;
|
import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||||
import androidx.media3.effect.TextOverlay;
|
import androidx.media3.effect.TextOverlay;
|
||||||
@ -60,6 +63,7 @@ import androidx.media3.test.utils.TextureBitmapReader;
|
|||||||
import androidx.media3.test.utils.VideoFrameProcessorTestRunner;
|
import androidx.media3.test.utils.VideoFrameProcessorTestRunner;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@ -103,15 +107,15 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
private static final String ORIGINAL_PNG_ASSET_PATH =
|
private static final String ORIGINAL_PNG_ASSET_PATH =
|
||||||
"media/bitmap/input_images/media3test_srgb.png";
|
"media/bitmap/input_images/media3test_srgb.png";
|
||||||
private static final String TEST_DIRECTORY = "media/bitmap/CompositorTestTimestamps/";
|
private static final String TEST_DIRECTORY = "media/bitmap/CompositorTestTimestamps/";
|
||||||
|
|
||||||
private @MonotonicNonNull String testId;
|
|
||||||
private @MonotonicNonNull VideoCompositorTestRunner compositorTestRunner;
|
|
||||||
private static final ImmutableList<ImmutableList<Effect>> TWO_INPUT_COMPOSITOR_EFFECT_LISTS =
|
private static final ImmutableList<ImmutableList<Effect>> TWO_INPUT_COMPOSITOR_EFFECT_LISTS =
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)),
|
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)),
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build()));
|
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build()));
|
||||||
|
|
||||||
|
private @MonotonicNonNull String testId;
|
||||||
|
private @MonotonicNonNull VideoCompositorTestRunner compositorTestRunner;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@EnsuresNonNull("testId")
|
@EnsuresNonNull("testId")
|
||||||
public void setUpTestId() {
|
public void setUpTestId() {
|
||||||
@ -125,6 +129,8 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for alpha and frame alpha/occlusion.
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withOneFrameFromEach_differentTimestamp_matchesExpectedBitmap()
|
public void compositeTwoInputs_withOneFrameFromEach_differentTimestamp_matchesExpectedBitmap()
|
||||||
@ -155,12 +161,13 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withPrimaryTransparent_differentTimestamp_matchesExpectedBitmap()
|
public void compositeTwoInputs_withPrimaryTransparent_differentTimestamp_matchesExpectedBitmap()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ImmutableList<ImmutableList<Effect>> inputEffects =
|
ImmutableList<ImmutableList<Effect>> inputEffectLists =
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
ImmutableList.of(new AlphaScale(0f)),
|
ImmutableList.of(new AlphaScale(0f)),
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build()));
|
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build()));
|
||||||
compositorTestRunner = new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffects);
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffectLists);
|
||||||
|
|
||||||
compositorTestRunner.queueBitmapToInput(
|
compositorTestRunner.queueBitmapToInput(
|
||||||
/* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L));
|
/* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L));
|
||||||
@ -186,12 +193,13 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withPrimaryOpaque_differentTimestamp_matchesExpectedBitmap()
|
public void compositeTwoInputs_withPrimaryOpaque_differentTimestamp_matchesExpectedBitmap()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ImmutableList<ImmutableList<Effect>> inputEffects =
|
ImmutableList<ImmutableList<Effect>> inputEffectLists =
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(100f)),
|
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(100f)),
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build()));
|
new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build()));
|
||||||
compositorTestRunner = new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffects);
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffectLists);
|
||||||
|
|
||||||
compositorTestRunner.queueBitmapToInput(
|
compositorTestRunner.queueBitmapToInput(
|
||||||
/* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L));
|
/* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L));
|
||||||
@ -217,11 +225,12 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withSecondaryTransparent_differentTimestamp_matchesExpectedBitmap()
|
public void compositeTwoInputs_withSecondaryTransparent_differentTimestamp_matchesExpectedBitmap()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ImmutableList<ImmutableList<Effect>> inputEffects =
|
ImmutableList<ImmutableList<Effect>> inputEffectLists =
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)),
|
ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)),
|
||||||
ImmutableList.of(new AlphaScale(0f)));
|
ImmutableList.of(new AlphaScale(0f)));
|
||||||
compositorTestRunner = new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffects);
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffectLists);
|
||||||
|
|
||||||
compositorTestRunner.queueBitmapToInput(
|
compositorTestRunner.queueBitmapToInput(
|
||||||
/* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L));
|
/* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L));
|
||||||
@ -243,6 +252,8 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
ImmutableList.of("0s_1s_transparent"));
|
ImmutableList.of("0s_1s_transparent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for mixing different frame rates and timestamps.
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withFiveFramesFromEach_matchesExpectedTimestamps()
|
public void compositeTwoInputs_withFiveFramesFromEach_matchesExpectedTimestamps()
|
||||||
@ -428,6 +439,8 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
ImmutableList.of("0s_1s", "1s_1s", "2s_1s", "3s_3s", "4s_4s"));
|
ImmutableList.of("0s_1s", "1s_1s", "2s_1s", "3s_3s", "4s_4s"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for "many" inputs/frames.
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeTwoInputs_withTenFramesFromEach_matchesExpectedFrameCount()
|
public void compositeTwoInputs_withTenFramesFromEach_matchesExpectedFrameCount()
|
||||||
@ -468,6 +481,8 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
assertThat(compositorTestRunner.getCompositedTimestamps()).hasSize(numberOfFramesToQueue);
|
assertThat(compositorTestRunner.getCompositedTimestamps()).hasSize(numberOfFramesToQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for different amounts of inputs.
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull("testId")
|
@RequiresNonNull("testId")
|
||||||
public void compositeOneInput_matchesExpectedBitmap() throws Exception {
|
public void compositeOneInput_matchesExpectedBitmap() throws Exception {
|
||||||
@ -526,6 +541,120 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
ImmutableList.of("0s_1s_0s", "1s_1s_0s", "2s_1s_2s"));
|
ImmutableList.of("0s_1s_0s", "1s_1s_0s", "2s_1s_2s"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for different layouts.
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull("testId")
|
||||||
|
public void compositeTwoInputs_pictureInPicture_matchesExpectedBitmap() throws Exception {
|
||||||
|
ImmutableList<ImmutableList<Effect>> inputEffectLists =
|
||||||
|
ImmutableList.of(ImmutableList.of(), ImmutableList.of(RgbFilter.createGrayscaleFilter()));
|
||||||
|
VideoCompositor.Settings pictureInPictureSettings =
|
||||||
|
new VideoCompositor.Settings() {
|
||||||
|
@Override
|
||||||
|
public Size getOutputSize(List<Size> inputSizes) {
|
||||||
|
return inputSizes.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs) {
|
||||||
|
if (inputId == 0) {
|
||||||
|
// This tests all OverlaySettings builder variables.
|
||||||
|
return new OverlaySettings.Builder()
|
||||||
|
.setScale(.25f, .5f)
|
||||||
|
.setOverlayAnchor(1, -1)
|
||||||
|
.setVideoFrameAnchor(.9f, -.7f)
|
||||||
|
.setRotationDegrees(20)
|
||||||
|
.setAlphaScale(.5f)
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return new OverlaySettings.Builder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(
|
||||||
|
testId, useSharedExecutor, inputEffectLists, pictureInPictureSettings);
|
||||||
|
|
||||||
|
compositorTestRunner.queueBitmapToAllInputs(1);
|
||||||
|
compositorTestRunner.endCompositing();
|
||||||
|
|
||||||
|
compositorTestRunner.saveAndAssertCompositedBitmapsMatchExpected(
|
||||||
|
ImmutableList.of("picture_in_picture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull("testId")
|
||||||
|
public void compositeTwoInputs_differentDimensions_matchesExpectedBitmap() throws Exception {
|
||||||
|
ImmutableList<ImmutableList<Effect>> inputEffectLists =
|
||||||
|
ImmutableList.of(
|
||||||
|
ImmutableList.of(
|
||||||
|
Presentation.createForWidthAndHeight(100, 100, Presentation.LAYOUT_STRETCH_TO_FIT)),
|
||||||
|
ImmutableList.of(RgbFilter.createGrayscaleFilter()));
|
||||||
|
VideoCompositor.Settings secondStreamAsOutputSizeSettings =
|
||||||
|
new VideoCompositor.Settings() {
|
||||||
|
@Override
|
||||||
|
public Size getOutputSize(List<Size> inputSizes) {
|
||||||
|
return Iterables.getLast(inputSizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs) {
|
||||||
|
return new OverlaySettings.Builder().build();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(
|
||||||
|
testId, useSharedExecutor, inputEffectLists, secondStreamAsOutputSizeSettings);
|
||||||
|
|
||||||
|
compositorTestRunner.queueBitmapToAllInputs(1);
|
||||||
|
compositorTestRunner.endCompositing();
|
||||||
|
|
||||||
|
compositorTestRunner.saveAndAssertCompositedBitmapsMatchExpected(
|
||||||
|
ImmutableList.of("different_dimensions"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull("testId")
|
||||||
|
public void compositeTwoInputs_stacked_matchesExpectedBitmap() throws Exception {
|
||||||
|
ImmutableList<ImmutableList<Effect>> inputEffectLists =
|
||||||
|
ImmutableList.of(
|
||||||
|
ImmutableList.of(RgbFilter.createGrayscaleFilter()),
|
||||||
|
ImmutableList.of(),
|
||||||
|
ImmutableList.of(RgbFilter.createInvertedFilter()));
|
||||||
|
VideoCompositor.Settings stackedFrameSettings =
|
||||||
|
new VideoCompositor.Settings() {
|
||||||
|
private static final int NUMBER_OF_INPUT_STREAMS = 3;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Size getOutputSize(List<Size> inputSizes) {
|
||||||
|
// Return the maximum width and sum of all heights.
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
for (int i = 0; i < inputSizes.size(); i++) {
|
||||||
|
width = max(width, inputSizes.get(i).getWidth());
|
||||||
|
height += inputSizes.get(i).getHeight();
|
||||||
|
}
|
||||||
|
return new Size(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs) {
|
||||||
|
return new OverlaySettings.Builder()
|
||||||
|
.setOverlayAnchor(-1, -1)
|
||||||
|
.setVideoFrameAnchor(-1, -1 + 2f * inputId / NUMBER_OF_INPUT_STREAMS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
compositorTestRunner =
|
||||||
|
new VideoCompositorTestRunner(
|
||||||
|
testId, useSharedExecutor, inputEffectLists, stackedFrameSettings);
|
||||||
|
|
||||||
|
compositorTestRunner.queueBitmapToAllInputs(1);
|
||||||
|
compositorTestRunner.endCompositing();
|
||||||
|
|
||||||
|
compositorTestRunner.saveAndAssertCompositedBitmapsMatchExpected(ImmutableList.of("stacked"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A test runner for {@link DefaultVideoCompositor} tests.
|
* A test runner for {@link DefaultVideoCompositor} tests.
|
||||||
*
|
*
|
||||||
@ -544,7 +673,7 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
private final String testId;
|
private final String testId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance using {@link DefaultVideoCompositor.Settings}.
|
||||||
*
|
*
|
||||||
* @param testId The {@link String} identifier for the test, used to name output files.
|
* @param testId The {@link String} identifier for the test, used to name output files.
|
||||||
* @param useSharedExecutor Whether to use a shared executor for {@link
|
* @param useSharedExecutor Whether to use a shared executor for {@link
|
||||||
@ -559,6 +688,27 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
boolean useSharedExecutor,
|
boolean useSharedExecutor,
|
||||||
ImmutableList<ImmutableList<Effect>> inputEffectLists)
|
ImmutableList<ImmutableList<Effect>> inputEffectLists)
|
||||||
throws GlUtil.GlException, VideoFrameProcessingException {
|
throws GlUtil.GlException, VideoFrameProcessingException {
|
||||||
|
this(testId, useSharedExecutor, inputEffectLists, new DefaultVideoCompositor.Settings());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param testId The {@link String} identifier for the test, used to name output files.
|
||||||
|
* @param useSharedExecutor Whether to use a shared executor for {@link
|
||||||
|
* VideoFrameProcessorTestRunner} and {@link VideoCompositor} instances.
|
||||||
|
* @param inputEffectLists {@link Effect}s to apply for {@link VideoCompositor} input sources.
|
||||||
|
* The size of this outer {@link List} is the amount of inputs. One inner list of {@link
|
||||||
|
* Effect}s is used for each input. For each input, the frame timestamp and {@code inputId}
|
||||||
|
* are overlaid via {@link TextOverlay} prior to its effects being applied.
|
||||||
|
* @param settings The {@link VideoCompositor.Settings}.
|
||||||
|
*/
|
||||||
|
public VideoCompositorTestRunner(
|
||||||
|
String testId,
|
||||||
|
boolean useSharedExecutor,
|
||||||
|
ImmutableList<ImmutableList<Effect>> inputEffectLists,
|
||||||
|
VideoCompositor.Settings settings)
|
||||||
|
throws GlUtil.GlException, VideoFrameProcessingException {
|
||||||
this.testId = testId;
|
this.testId = testId;
|
||||||
timeoutMs = inputEffectLists.size() * VIDEO_FRAME_PROCESSING_WAIT_MS;
|
timeoutMs = inputEffectLists.size() * VIDEO_FRAME_PROCESSING_WAIT_MS;
|
||||||
sharedExecutorService =
|
sharedExecutorService =
|
||||||
@ -575,6 +725,7 @@ public final class DefaultVideoCompositorPixelTest {
|
|||||||
new DefaultVideoCompositor(
|
new DefaultVideoCompositor(
|
||||||
getApplicationContext(),
|
getApplicationContext(),
|
||||||
glObjectsProvider,
|
glObjectsProvider,
|
||||||
|
settings,
|
||||||
sharedExecutorService,
|
sharedExecutorService,
|
||||||
new VideoCompositor.Listener() {
|
new VideoCompositor.Listener() {
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user