From 57bc2152100c7de92fa677b0e4b35e0d17335b22 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Fri, 1 Sep 2023 05:52:01 -0700 Subject: [PATCH] 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 --- .../media3/effect/DefaultVideoCompositor.java | 72 ++++++-- .../media3/effect/OverlayMatrixProvider.java | 157 ++++++++-------- .../media3/effect/OverlaySettings.java | 20 +- .../media3/effect/OverlayShaderProgram.java | 8 +- .../effect/SamplerOverlayMatrixProvider.java | 56 ++++++ .../media3/effect/VideoCompositor.java | 21 ++- .../output_different_dimensions.png | Bin 0 -> 10372 bytes .../output_picture_in_picture.png | Bin 0 -> 12275 bytes .../output_stacked.png | Bin 0 -> 28070 bytes .../DefaultVideoCompositorPixelTest.java | 171 +++++++++++++++++- 10 files changed, 389 insertions(+), 116 deletions(-) create mode 100644 libraries/effect/src/main/java/androidx/media3/effect/SamplerOverlayMatrixProvider.java create mode 100644 libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_different_dimensions.png create mode 100644 libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_picture_in_picture.png create mode 100644 libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_stacked.png diff --git a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoCompositor.java b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoCompositor.java index 0534a71d77..ccbe001669 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoCompositor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoCompositor.java @@ -36,6 +36,7 @@ import androidx.media3.common.util.GlProgram; import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.Log; import androidx.media3.common.util.LongArrayQueue; +import androidx.media3.common.util.Size; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; @@ -50,8 +51,8 @@ import java.util.concurrent.ExecutorService; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * A basic {@link VideoCompositor} implementation that takes in frames from exactly 2 input sources' - * streams and combines them into one output stream. + * A basic {@link VideoCompositor} implementation that takes in frames from exactly 2 SDR input + * sources' streams and combines them into one output stream. * *

The first {@linkplain #registerInputSource registered source} will be the primary stream, * 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. // * If the primary stream ends, consider setting the secondary stream as the new primary stream, // 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 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 TAG = "DefaultVideoCompositor"; 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 final Context context; private final VideoCompositor.Listener listener; private final GlTextureProducer.Listener textureOutputListener; private final GlObjectsProvider glObjectsProvider; + private final VideoCompositor.Settings settings; + private final OverlayMatrixProvider overlayMatrixProvider; private final VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor; @GuardedBy("this") @@ -100,6 +117,7 @@ public final class DefaultVideoCompositor implements VideoCompositor { public DefaultVideoCompositor( Context context, GlObjectsProvider glObjectsProvider, + VideoCompositor.Settings settings, @Nullable ExecutorService executorService, VideoCompositor.Listener listener, GlTextureProducer.Listener textureOutputListener, @@ -108,6 +126,8 @@ public final class DefaultVideoCompositor implements VideoCompositor { this.listener = listener; this.textureOutputListener = textureOutputListener; this.glObjectsProvider = glObjectsProvider; + this.settings = settings; + this.overlayMatrixProvider = new OverlayMatrixProvider(); inputSources = new ArrayList<>(); outputTexturePool = @@ -180,7 +200,11 @@ public final class DefaultVideoCompositor implements VideoCompositor { checkState(!inputSource.isInputEnded); InputFrameInfo inputFrameInfo = - new InputFrameInfo(textureProducer, inputTexture, presentationTimeUs); + new InputFrameInfo( + textureProducer, + inputTexture, + presentationTimeUs, + settings.getOverlaySettings(inputId, presentationTimeUs)); inputSource.frameInfos.add(inputFrameInfo); if (inputId == PRIMARY_INPUT_ID) { @@ -277,17 +301,17 @@ public final class DefaultVideoCompositor implements VideoCompositor { ensureGlProgramConfigured(); - // TODO: b/262694346 - Allow different input frame dimensions. 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++) { - GlTextureInfo textureToComposite = framesToComposite.get(i).texture; - checkState(primaryInputTexture.width == textureToComposite.width); - checkState(primaryInputTexture.height == textureToComposite.height); + ImmutableList.Builder inputSizes = new ImmutableList.Builder<>(); + for (int i = 0; i < framesToComposite.size(); i++) { + GlTextureInfo texture = framesToComposite.get(i).texture; + 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(); long outputPresentationTimestampUs = primaryInputFrame.presentationTimeUs; outputTextureTimestamps.add(outputPresentationTimestampUs); @@ -394,16 +418,18 @@ public final class DefaultVideoCompositor implements VideoCompositor { GlUtil.getNormalizedCoordinateBounds(), GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); glProgram.setFloatsUniform("uTexTransformationMatrix", GlUtil.create4x4IdentityMatrix()); - glProgram.setFloatsUniform("uTransformationMatrix", GlUtil.create4x4IdentityMatrix()); } catch (IOException e) { throw new VideoFrameProcessingException(e); } } + // Enhanced for-loops are discouraged in media3.effect due to short-lived allocations. + @SuppressWarnings("ListReverse") private void drawFrame(List framesToComposite, GlTextureInfo outputTexture) throws GlUtil.GlException { GlUtil.focusFramebufferUsingCurrentContext( outputTexture.fboId, outputTexture.width, outputTexture.height); + overlayMatrixProvider.configure(new Size(outputTexture.width, outputTexture.height)); GlUtil.clearFocusedBuffers(); GlProgram glProgram = checkNotNull(this.glProgram); @@ -423,7 +449,7 @@ public final class DefaultVideoCompositor implements VideoCompositor { // Draw textures from back to front. for (int i = framesToComposite.size() - 1; i >= 0; i--) { - blendOntoFocusedTexture(framesToComposite.get(i).texture.texId); + blendOntoFocusedTexture(framesToComposite.get(i)); } GLES20.glDisable(GLES20.GL_BLEND); @@ -431,9 +457,16 @@ public final class DefaultVideoCompositor implements VideoCompositor { GlUtil.checkGlError(); } - private void blendOntoFocusedTexture(int texId) throws GlUtil.GlException { + private void blendOntoFocusedTexture(InputFrameInfo inputFrameInfo) throws GlUtil.GlException { 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(); // The four-vertex triangle strip forms a quad. @@ -480,12 +513,17 @@ public final class DefaultVideoCompositor implements VideoCompositor { public final GlTextureProducer textureProducer; public final GlTextureInfo texture; public final long presentationTimeUs; + public final OverlaySettings overlaySettings; public InputFrameInfo( - GlTextureProducer textureProducer, GlTextureInfo texture, long presentationTimeUs) { + GlTextureProducer textureProducer, + GlTextureInfo texture, + long presentationTimeUs, + OverlaySettings overlaySettings) { this.textureProducer = textureProducer; this.texture = texture; this.presentationTimeUs = presentationTimeUs; + this.overlaySettings = overlaySettings; } } } diff --git a/libraries/effect/src/main/java/androidx/media3/effect/OverlayMatrixProvider.java b/libraries/effect/src/main/java/androidx/media3/effect/OverlayMatrixProvider.java index 035dec0b97..91df8d8205 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/OverlayMatrixProvider.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/OverlayMatrixProvider.java @@ -15,7 +15,7 @@ */ 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.util.Pair; @@ -23,15 +23,14 @@ import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.Size; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/* package */ final class OverlayMatrixProvider { - private static final int MATRIX_OFFSET = 0; +/** Provides a matrix for {@link OverlaySettings}, to be applied on a vertex. */ +/* package */ class OverlayMatrixProvider { + protected static final int MATRIX_OFFSET = 0; private final float[] videoFrameAnchorMatrix; - private final float[] videoFrameAnchorMatrixInv; private final float[] aspectRatioMatrix; private final float[] scaleMatrix; private final float[] scaleMatrixInv; private final float[] overlayAnchorMatrix; - private final float[] overlayAnchorMatrixInv; private final float[] rotateMatrix; private final float[] overlayAspectRatioMatrix; private final float[] overlayAspectRatioMatrixInv; @@ -41,9 +40,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public OverlayMatrixProvider() { aspectRatioMatrix = GlUtil.create4x4IdentityMatrix(); videoFrameAnchorMatrix = GlUtil.create4x4IdentityMatrix(); - videoFrameAnchorMatrixInv = GlUtil.create4x4IdentityMatrix(); overlayAnchorMatrix = GlUtil.create4x4IdentityMatrix(); - overlayAnchorMatrixInv = GlUtil.create4x4IdentityMatrix(); rotateMatrix = GlUtil.create4x4IdentityMatrix(); scaleMatrix = GlUtil.create4x4IdentityMatrix(); scaleMatrixInv = GlUtil.create4x4IdentityMatrix(); @@ -56,6 +53,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.backgroundSize = backgroundSize; } + /** + * Returns the transformation matrix. + * + *

This instance must be {@linkplain #configure configured} before this method is called. + */ public float[] getTransformationMatrix(Size overlaySize, OverlaySettings overlaySettings) { reset(); @@ -67,44 +69,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; videoFrameAnchor.first, videoFrameAnchor.second, /* z= */ 0f); - Matrix.invertM(videoFrameAnchorMatrixInv, MATRIX_OFFSET, videoFrameAnchorMatrix, MATRIX_OFFSET); + checkStateNotNull(backgroundSize); Matrix.scaleM( aspectRatioMatrix, MATRIX_OFFSET, - checkNotNull(backgroundSize).getWidth() / (float) overlaySize.getWidth(), - checkNotNull(backgroundSize).getHeight() / (float) overlaySize.getHeight(), + (float) overlaySize.getWidth() / backgroundSize.getWidth(), + (float) overlaySize.getHeight() / backgroundSize.getHeight(), /* z= */ 1f); // Scale the image. Pair scale = overlaySettings.scale; - Matrix.scaleM( - scaleMatrix, - MATRIX_OFFSET, - scaleMatrix, - MATRIX_OFFSET, - scale.first, - scale.second, - /* z= */ 1f); + Matrix.scaleM(scaleMatrix, MATRIX_OFFSET, scale.first, scale.second, /* z= */ 1f); 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 overlayAnchor = overlaySettings.overlayAnchor; Matrix.translateM( - overlayAnchorMatrix, MATRIX_OFFSET, overlayAnchor.first, overlayAnchor.second, /* z= */ 0f); - Matrix.invertM(overlayAnchorMatrixInv, MATRIX_OFFSET, overlayAnchorMatrix, MATRIX_OFFSET); + overlayAnchorMatrix, + MATRIX_OFFSET, + -1 * overlayAnchor.first, + -1 * overlayAnchor.second, + /* z= */ 0f); // Rotate the image. Matrix.rotateM( - rotateMatrix, - MATRIX_OFFSET, rotateMatrix, MATRIX_OFFSET, overlaySettings.rotationDegrees, /* x= */ 0f, /* y= */ 0f, /* z= */ 1f); - Matrix.invertM(rotateMatrix, MATRIX_OFFSET, rotateMatrix, MATRIX_OFFSET); // Rotation matrix needs to account for overlay aspect ratio to prevent stretching. Matrix.scaleM( @@ -116,67 +112,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Matrix.invertM( overlayAspectRatioMatrixInv, MATRIX_OFFSET, overlayAspectRatioMatrix, MATRIX_OFFSET); - // Rotation needs to be agnostic of the scaling matrix and the aspect ratios. - // transformationMatrix = scaleMatrixInv * overlayAspectRatioMatrix * rotateMatrix * - // overlayAspectRatioInv * scaleMatrix * overlayAnchorMatrixInv * scaleMatrixInv * - // aspectRatioMatrix * videoFrameAnchorMatrixInv - Matrix.multiplyMM( - transformationMatrix, - MATRIX_OFFSET, - transformationMatrix, - MATRIX_OFFSET, - scaleMatrixInv, - MATRIX_OFFSET); + // transformationMatrix = videoFrameAnchorMatrix * aspectRatioMatrix + // * scaleMatrix * overlayAnchorMatrix * scaleMatrixInv + // * overlayAspectRatioMatrix * rotateMatrix * overlayAspectRatioMatrixInv + // * scaleMatrix. + // Anchor position in output frame. Matrix.multiplyMM( transformationMatrix, MATRIX_OFFSET, transformationMatrix, MATRIX_OFFSET, - overlayAspectRatioMatrix, - 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, + videoFrameAnchorMatrix, MATRIX_OFFSET); // Correct for aspect ratio of image in output frame. @@ -188,23 +135,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; aspectRatioMatrix, MATRIX_OFFSET); - // Anchor position in output frame. Matrix.multiplyMM( transformationMatrix, MATRIX_OFFSET, transformationMatrix, MATRIX_OFFSET, - videoFrameAnchorMatrixInv, + scaleMatrix, 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; } private void reset() { GlUtil.setToIdentity(aspectRatioMatrix); GlUtil.setToIdentity(videoFrameAnchorMatrix); - GlUtil.setToIdentity(videoFrameAnchorMatrixInv); GlUtil.setToIdentity(overlayAnchorMatrix); - GlUtil.setToIdentity(overlayAnchorMatrixInv); GlUtil.setToIdentity(scaleMatrix); GlUtil.setToIdentity(scaleMatrixInv); GlUtil.setToIdentity(rotateMatrix); diff --git a/libraries/effect/src/main/java/androidx/media3/effect/OverlaySettings.java b/libraries/effect/src/main/java/androidx/media3/effect/OverlaySettings.java index 18b3f5881c..97d9d17186 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/OverlaySettings.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/OverlaySettings.java @@ -22,7 +22,10 @@ import androidx.annotation.FloatRange; import androidx.media3.common.util.UnstableApi; 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 public final class OverlaySettings { public final boolean useHdr; @@ -47,6 +50,11 @@ public final class OverlaySettings { 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. */ public static final class Builder { private boolean useHdr; @@ -65,6 +73,15 @@ public final class OverlaySettings { 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 * 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; } + // 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. * diff --git a/libraries/effect/src/main/java/androidx/media3/effect/OverlayShaderProgram.java b/libraries/effect/src/main/java/androidx/media3/effect/OverlayShaderProgram.java index 4c3d281aad..7a9dd98444 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/OverlayShaderProgram.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/OverlayShaderProgram.java @@ -29,7 +29,7 @@ import com.google.common.collect.ImmutableList; /* package */ final class OverlayShaderProgram extends BaseGlShaderProgram { private final GlProgram glProgram; - private final OverlayMatrixProvider overlayMatrixProvider; + private final SamplerOverlayMatrixProvider samplerOverlayMatrixProvider; private final ImmutableList overlays; /** @@ -49,7 +49,7 @@ import com.google.common.collect.ImmutableList; overlays.size() <= 15, "OverlayShaderProgram does not support more than 15 overlays in the same instance."); this.overlays = overlays; - this.overlayMatrixProvider = new OverlayMatrixProvider(); + this.samplerOverlayMatrixProvider = new SamplerOverlayMatrixProvider(); try { glProgram = new GlProgram(createVertexShader(overlays.size()), createFragmentShader(overlays.size())); @@ -66,7 +66,7 @@ import com.google.common.collect.ImmutableList; @Override public Size configure(int inputWidth, int inputHeight) { Size videoSize = new Size(inputWidth, inputHeight); - overlayMatrixProvider.configure(/* backgroundSize= */ videoSize); + samplerOverlayMatrixProvider.configure(/* backgroundSize= */ videoSize); for (TextureOverlay overlay : overlays) { overlay.configure(videoSize); } @@ -91,7 +91,7 @@ import com.google.common.collect.ImmutableList; glProgram.setFloatsUniform( Util.formatInvariant("uTransformationMatrix%d", texUnitIndex), - overlayMatrixProvider.getTransformationMatrix(overlaySize, overlaySettings)); + samplerOverlayMatrixProvider.getTransformationMatrix(overlaySize, overlaySettings)); glProgram.setFloatUniform( Util.formatInvariant("uOverlayAlphaScale%d", texUnitIndex), diff --git a/libraries/effect/src/main/java/androidx/media3/effect/SamplerOverlayMatrixProvider.java b/libraries/effect/src/main/java/androidx/media3/effect/SamplerOverlayMatrixProvider.java new file mode 100644 index 0000000000..5fe575c1dd --- /dev/null +++ b/libraries/effect/src/main/java/androidx/media3/effect/SamplerOverlayMatrixProvider.java @@ -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; + } +} diff --git a/libraries/effect/src/main/java/androidx/media3/effect/VideoCompositor.java b/libraries/effect/src/main/java/androidx/media3/effect/VideoCompositor.java index c108b6fc62..63734893e7 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/VideoCompositor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/VideoCompositor.java @@ -17,10 +17,12 @@ package androidx.media3.effect; import androidx.media3.common.GlTextureInfo; import androidx.media3.common.VideoFrameProcessingException; +import androidx.media3.common.util.Size; 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. * *

Input and output are provided via OpenGL textures. @@ -41,6 +43,23 @@ public interface VideoCompositor extends GlTextureProducer { 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 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 * source, to be used in {@link #queueInputTexture}. diff --git a/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_different_dimensions.png b/libraries/test_data/src/test/assets/media/bitmap/CompositorTestTimestamps/output_different_dimensions.png new file mode 100644 index 0000000000000000000000000000000000000000..a97da198414a74189bbedd2388a84f93f4dd0515 GIT binary patch literal 10372 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 NyLVLNK4Fdg{|8lSh$a94 literal 0 HcmV?d00001 diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java index f9e6fa0342..ddc818a018 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/DefaultVideoCompositorPixelTest.java @@ -24,6 +24,7 @@ import static androidx.media3.test.utils.VideoFrameProcessorTestRunner.createTim import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.Math.max; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.graphics.Bitmap; @@ -44,6 +45,7 @@ import androidx.media3.common.Effect; import androidx.media3.common.GlObjectsProvider; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import androidx.media3.effect.AlphaScale; import androidx.media3.effect.DefaultGlObjectsProvider; @@ -51,6 +53,7 @@ import androidx.media3.effect.DefaultVideoCompositor; import androidx.media3.effect.DefaultVideoFrameProcessor; import androidx.media3.effect.OverlayEffect; import androidx.media3.effect.OverlaySettings; +import androidx.media3.effect.Presentation; import androidx.media3.effect.RgbFilter; import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.effect.TextOverlay; @@ -60,6 +63,7 @@ import androidx.media3.test.utils.TextureBitmapReader; import androidx.media3.test.utils.VideoFrameProcessorTestRunner; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -103,15 +107,15 @@ public final class DefaultVideoCompositorPixelTest { private static final String ORIGINAL_PNG_ASSET_PATH = "media/bitmap/input_images/media3test_srgb.png"; private static final String TEST_DIRECTORY = "media/bitmap/CompositorTestTimestamps/"; - - private @MonotonicNonNull String testId; - private @MonotonicNonNull VideoCompositorTestRunner compositorTestRunner; private static final ImmutableList> TWO_INPUT_COMPOSITOR_EFFECT_LISTS = ImmutableList.of( ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)), ImmutableList.of( new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build())); + private @MonotonicNonNull String testId; + private @MonotonicNonNull VideoCompositorTestRunner compositorTestRunner; + @Before @EnsuresNonNull("testId") public void setUpTestId() { @@ -125,6 +129,8 @@ public final class DefaultVideoCompositorPixelTest { } } + // Tests for alpha and frame alpha/occlusion. + @Test @RequiresNonNull("testId") public void compositeTwoInputs_withOneFrameFromEach_differentTimestamp_matchesExpectedBitmap() @@ -155,12 +161,13 @@ public final class DefaultVideoCompositorPixelTest { @RequiresNonNull("testId") public void compositeTwoInputs_withPrimaryTransparent_differentTimestamp_matchesExpectedBitmap() throws Exception { - ImmutableList> inputEffects = + ImmutableList> inputEffectLists = ImmutableList.of( ImmutableList.of(new AlphaScale(0f)), ImmutableList.of( new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build())); - compositorTestRunner = new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffects); + compositorTestRunner = + new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffectLists); compositorTestRunner.queueBitmapToInput( /* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L)); @@ -186,12 +193,13 @@ public final class DefaultVideoCompositorPixelTest { @RequiresNonNull("testId") public void compositeTwoInputs_withPrimaryOpaque_differentTimestamp_matchesExpectedBitmap() throws Exception { - ImmutableList> inputEffects = + ImmutableList> inputEffectLists = ImmutableList.of( ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(100f)), ImmutableList.of( new ScaleAndRotateTransformation.Builder().setRotationDegrees(180).build())); - compositorTestRunner = new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffects); + compositorTestRunner = + new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffectLists); compositorTestRunner.queueBitmapToInput( /* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L)); @@ -217,11 +225,12 @@ public final class DefaultVideoCompositorPixelTest { @RequiresNonNull("testId") public void compositeTwoInputs_withSecondaryTransparent_differentTimestamp_matchesExpectedBitmap() throws Exception { - ImmutableList> inputEffects = + ImmutableList> inputEffectLists = ImmutableList.of( ImmutableList.of(RgbFilter.createGrayscaleFilter(), new AlphaScale(0.7f)), ImmutableList.of(new AlphaScale(0f))); - compositorTestRunner = new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffects); + compositorTestRunner = + new VideoCompositorTestRunner(testId, useSharedExecutor, inputEffectLists); compositorTestRunner.queueBitmapToInput( /* inputId= */ 0, /* timestamps= */ ImmutableList.of(0L)); @@ -243,6 +252,8 @@ public final class DefaultVideoCompositorPixelTest { ImmutableList.of("0s_1s_transparent")); } + // Tests for mixing different frame rates and timestamps. + @Test @RequiresNonNull("testId") 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")); } + // Tests for "many" inputs/frames. + @Test @RequiresNonNull("testId") public void compositeTwoInputs_withTenFramesFromEach_matchesExpectedFrameCount() @@ -468,6 +481,8 @@ public final class DefaultVideoCompositorPixelTest { assertThat(compositorTestRunner.getCompositedTimestamps()).hasSize(numberOfFramesToQueue); } + // Tests for different amounts of inputs. + @Test @RequiresNonNull("testId") 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")); } + // Tests for different layouts. + + @Test + @RequiresNonNull("testId") + public void compositeTwoInputs_pictureInPicture_matchesExpectedBitmap() throws Exception { + ImmutableList> inputEffectLists = + ImmutableList.of(ImmutableList.of(), ImmutableList.of(RgbFilter.createGrayscaleFilter())); + VideoCompositor.Settings pictureInPictureSettings = + new VideoCompositor.Settings() { + @Override + public Size getOutputSize(List 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> 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 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> 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 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. * @@ -544,7 +673,7 @@ public final class DefaultVideoCompositorPixelTest { 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 useSharedExecutor Whether to use a shared executor for {@link @@ -559,6 +688,27 @@ public final class DefaultVideoCompositorPixelTest { boolean useSharedExecutor, ImmutableList> inputEffectLists) 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> inputEffectLists, + VideoCompositor.Settings settings) + throws GlUtil.GlException, VideoFrameProcessingException { this.testId = testId; timeoutMs = inputEffectLists.size() * VIDEO_FRAME_PROCESSING_WAIT_MS; sharedExecutorService = @@ -575,6 +725,7 @@ public final class DefaultVideoCompositorPixelTest { new DefaultVideoCompositor( getApplicationContext(), glObjectsProvider, + settings, sharedExecutorService, new VideoCompositor.Listener() { @Override