Update OverlaySettings to explicitly state overlay transformations

Alters the OverlayShaderProgram implementation to support rotations, however we need to apply the transformations separately in order for them to work as expected so the matrix is removed from the interface in favour of explicit methods.

Adds a rotation test to ensure this ability doesn't regress

PiperOrigin-RevId: 549890847
This commit is contained in:
tofunmi 2023-07-21 10:43:55 +01:00 committed by Rohit Singh
parent d658de5944
commit 7eee15ecb4
8 changed files with 351 additions and 114 deletions

View File

@ -43,6 +43,7 @@
* Remove `TransformationRequest.HdrMode` annotation type and its * Remove `TransformationRequest.HdrMode` annotation type and its
associated constants. Use `Composition.HdrMode` and its associated associated constants. Use `Composition.HdrMode` and its associated
constants instead. constants instead.
* Simplify the `OverlaySettings` to fix rotation issues.
* Track Selection: * Track Selection:
* Extractors: * Extractors:
* MPEG-TS: Ensure the last frame is rendered by passing the last access * MPEG-TS: Ensure the last frame is rendered by passing the last access

View File

@ -16,12 +16,10 @@
package androidx.media3.demo.transformer; package androidx.media3.demo.transformer;
import android.graphics.Color; import android.graphics.Color;
import android.opengl.Matrix;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.GlUtil;
import androidx.media3.effect.OverlaySettings; import androidx.media3.effect.OverlaySettings;
import androidx.media3.effect.TextOverlay; import androidx.media3.effect.TextOverlay;
import androidx.media3.effect.TextureOverlay; import androidx.media3.effect.TextureOverlay;
@ -36,13 +34,12 @@ import java.util.Locale;
private final OverlaySettings overlaySettings; private final OverlaySettings overlaySettings;
public TimerOverlay() { public TimerOverlay() {
float[] positioningMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(
positioningMatrix, /* mOffset= */ 0, /* x= */ -0.7f, /* y= */ -0.95f, /* z= */ 1);
overlaySettings = overlaySettings =
new OverlaySettings.Builder() new OverlaySettings.Builder()
.setAnchor(/* x= */ -1f, /* y= */ -1f) // Place the timer in the bottom left corner of the screen with some padding from the
.setMatrix(positioningMatrix) // edges.
.setOverlayAnchor(/* x= */ 1f, /* y= */ 1f)
.setVideoFrameAnchor(/* x= */ -0.7f, /* y= */ -0.95f)
.build(); .build();
} }

View File

@ -31,7 +31,6 @@ import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.opengl.Matrix;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.Spannable; import android.text.Spannable;
@ -58,7 +57,6 @@ import androidx.media3.common.audio.ChannelMixingAudioProcessor;
import androidx.media3.common.audio.ChannelMixingMatrix; import androidx.media3.common.audio.ChannelMixingMatrix;
import androidx.media3.common.audio.SonicAudioProcessor; import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.datasource.DataSourceBitmapLoader;
import androidx.media3.effect.BitmapOverlay; import androidx.media3.effect.BitmapOverlay;
@ -580,13 +578,12 @@ public final class TransformerActivity extends AppCompatActivity {
private OverlayEffect createOverlayEffectFromBundle(Bundle bundle, boolean[] selectedEffects) { private OverlayEffect createOverlayEffectFromBundle(Bundle bundle, boolean[] selectedEffects) {
ImmutableList.Builder<TextureOverlay> overlaysBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder<TextureOverlay> overlaysBuilder = new ImmutableList.Builder<>();
if (selectedEffects[ConfigurationActivity.OVERLAY_LOGO_AND_TIMER_INDEX]) { if (selectedEffects[ConfigurationActivity.OVERLAY_LOGO_AND_TIMER_INDEX]) {
float[] logoPositioningMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(
logoPositioningMatrix, /* mOffset= */ 0, /* x= */ -0.95f, /* y= */ -0.95f, /* z= */ 1);
OverlaySettings logoSettings = OverlaySettings logoSettings =
new OverlaySettings.Builder() new OverlaySettings.Builder()
.setMatrix(logoPositioningMatrix) // Place the logo in the bottom left corner of the screen with some padding from the
.setAnchor(/* x= */ -1f, /* y= */ -1f) // edges.
.setOverlayAnchor(/* x= */ 1f, /* y= */ 1f)
.setVideoFrameAnchor(/* x= */ -0.95f, /* y= */ -0.95f)
.build(); .build();
Drawable logo; Drawable logo;
try { try {

View File

@ -31,7 +31,6 @@ import android.graphics.Color;
import android.opengl.EGLContext; import android.opengl.EGLContext;
import android.opengl.EGLDisplay; import android.opengl.EGLDisplay;
import android.opengl.EGLSurface; import android.opengl.EGLSurface;
import android.opengl.Matrix;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
@ -64,10 +63,15 @@ public class OverlayShaderProgramPixelTest {
"media/bitmap/sample_mp4_first_frame/electrical_colors/original.png"; "media/bitmap/sample_mp4_first_frame/electrical_colors/original.png";
private static final String OVERLAY_BITMAP_DEFAULT = private static final String OVERLAY_BITMAP_DEFAULT =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_default.png"; "media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_default.png";
private static final String OVERLAY_BITMAP_ANCHORED = private static final String OVERLAY_BITMAP_DOUBLY_ANCHORED =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_anchored.png"; "media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_anchored.png";
private static final String OVERLAY_BITMAP_OVERLAY_ANCHORED =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_overlayAnchored.png";
private static final String OVERLAY_BITMAP_SCALED = private static final String OVERLAY_BITMAP_SCALED =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_scaled.png"; "media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_scaled.png";
private static final String OVERLAY_BITMAP_ROTATED90 =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_rotated90.png";
private static final String OVERLAY_BITMAP_TRANSLUCENT = private static final String OVERLAY_BITMAP_TRANSLUCENT =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_translucent.png"; "media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_translucent.png";
private static final String OVERLAY_TEXT_DEFAULT = private static final String OVERLAY_TEXT_DEFAULT =
@ -154,39 +158,23 @@ public class OverlayShaderProgramPixelTest {
} }
@Test @Test
public void drawFrame_scaledBitmapOverlay_letterboxStretchesOverlay() throws Exception { public void drawFrame_anchoredAndTranslatedBitmapOverlay_blendsBitmapIntoTopLeftOfFrame()
String testId = "drawFrame_scaledBitmapOverlay"; throws Exception {
String testId = "drawFrame_anchoredAndTranslatedBitmapOverlay";
Bitmap overlayBitmap = readBitmap(OVERLAY_PNG_ASSET_PATH); Bitmap overlayBitmap = readBitmap(OVERLAY_PNG_ASSET_PATH);
float[] scaleMatrix = GlUtil.create4x4IdentityMatrix(); OverlaySettings overlaySettings =
OverlaySettings overlaySettings = new OverlaySettings.Builder().setMatrix(scaleMatrix).build(); new OverlaySettings.Builder()
.setOverlayAnchor(/* x= */ 1f, /* y= */ -1f)
.setVideoFrameAnchor(/* x= */ -1f, /* y= */ 1f)
.build();
BitmapOverlay staticBitmapOverlay = BitmapOverlay staticBitmapOverlay =
new BitmapOverlay() { BitmapOverlay.createStaticBitmapOverlay(overlayBitmap, overlaySettings);
@Override
public Bitmap getBitmap(long presentationTimeUs) {
return overlayBitmap;
}
@Override
public void configure(Size videoSize) {
Matrix.scaleM(
scaleMatrix,
/* mOffset= */ 0,
/* x= */ videoSize.getWidth() / (float) overlayBitmap.getWidth(),
/* y= */ 1,
/* z= */ 1);
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
return overlaySettings;
}
};
overlayShaderProgram = overlayShaderProgram =
new OverlayEffect(ImmutableList.of(staticBitmapOverlay)) new OverlayEffect(ImmutableList.of(staticBitmapOverlay))
.toGlShaderProgram(context, /* useHdr= */ false); .toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = overlayShaderProgram.configure(inputWidth, inputHeight); Size outputSize = overlayShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(OVERLAY_BITMAP_SCALED); Bitmap expectedBitmap = readBitmap(OVERLAY_BITMAP_DOUBLY_ANCHORED);
overlayShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0); overlayShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
@ -199,13 +187,12 @@ public class OverlayShaderProgramPixelTest {
} }
@Test @Test
public void drawFrame_anchoredBitmapOverlay_blendsBitmapIntoTopLeftOfFrame() throws Exception { public void drawFrame_overlayAnchoredOnlyBitmapOverlay_anchorsOverlayFromTopLeftCornerOfFrame()
String testId = "drawFrame_anchoredBitmapOverlay"; throws Exception {
String testId = "drawFrame_anchoredAndTranslatedBitmapOverlay";
Bitmap overlayBitmap = readBitmap(OVERLAY_PNG_ASSET_PATH); Bitmap overlayBitmap = readBitmap(OVERLAY_PNG_ASSET_PATH);
float[] translateMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(translateMatrix, /* mOffset= */ 0, /* x= */ -1f, /* y= */ 1f, /* z= */ 1);
OverlaySettings overlaySettings = OverlaySettings overlaySettings =
new OverlaySettings.Builder().setMatrix(translateMatrix).setAnchor(-1f, 1f).build(); new OverlaySettings.Builder().setOverlayAnchor(/* x= */ 1f, /* y= */ -1f).build();
BitmapOverlay staticBitmapOverlay = BitmapOverlay staticBitmapOverlay =
BitmapOverlay.createStaticBitmapOverlay(overlayBitmap, overlaySettings); BitmapOverlay.createStaticBitmapOverlay(overlayBitmap, overlaySettings);
overlayShaderProgram = overlayShaderProgram =
@ -213,7 +200,31 @@ public class OverlayShaderProgramPixelTest {
.toGlShaderProgram(context, /* useHdr= */ false); .toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = overlayShaderProgram.configure(inputWidth, inputHeight); Size outputSize = overlayShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight()); setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(OVERLAY_BITMAP_ANCHORED); Bitmap expectedBitmap = readBitmap(OVERLAY_BITMAP_OVERLAY_ANCHORED);
overlayShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_rotatedBitmapOverlay_blendsBitmapRotated90degrees() throws Exception {
String testId = "drawFrame_rotatedBitmapOverlay";
Bitmap overlayBitmap = readBitmap(OVERLAY_PNG_ASSET_PATH);
OverlaySettings overlaySettings = new OverlaySettings.Builder().setRotationDegrees(90f).build();
BitmapOverlay staticBitmapOverlay =
BitmapOverlay.createStaticBitmapOverlay(overlayBitmap, overlaySettings);
overlayShaderProgram =
new OverlayEffect(ImmutableList.of(staticBitmapOverlay))
.toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = overlayShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(OVERLAY_BITMAP_ROTATED90);
overlayShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0); overlayShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
@ -333,10 +344,9 @@ public class OverlayShaderProgramPixelTest {
} }
@Test @Test
public void drawFrame_translatedTextOverlay_blendsTextIntoFrame() throws Exception { public void drawFrame_anchoredTextOverlay_blendsTextIntoTheTopRightQuadrantOfFrame()
String testId = "drawFrame_translatedTextOverlay"; throws Exception {
float[] translateMatrix = GlUtil.create4x4IdentityMatrix(); String testId = "drawFrame_anchoredTextOverlay";
Matrix.translateM(translateMatrix, /* mOffset= */ 0, /* x= */ 0.5f, /* y= */ 0.5f, /* z= */ 1);
SpannableString overlayText = new SpannableString(/* source= */ "Text styling"); SpannableString overlayText = new SpannableString(/* source= */ "Text styling");
overlayText.setSpan( overlayText.setSpan(
new ForegroundColorSpan(Color.GRAY), new ForegroundColorSpan(Color.GRAY),
@ -344,7 +354,7 @@ public class OverlayShaderProgramPixelTest {
/* end= */ 4, /* end= */ 4,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
OverlaySettings overlaySettings = OverlaySettings overlaySettings =
new OverlaySettings.Builder().setMatrix(translateMatrix).build(); new OverlaySettings.Builder().setVideoFrameAnchor(0.5f, 0.5f).build();
TextOverlay staticTextOverlay = TextOverlay staticTextOverlay =
TextOverlay.createStaticTextOverlay(overlayText, overlaySettings); TextOverlay.createStaticTextOverlay(overlayText, overlaySettings);
overlayShaderProgram = overlayShaderProgram =
@ -367,8 +377,6 @@ public class OverlayShaderProgramPixelTest {
@Test @Test
public void drawFrame_multipleOverlays_blendsBothIntoFrame() throws Exception { public void drawFrame_multipleOverlays_blendsBothIntoFrame() throws Exception {
String testId = "drawFrame_multipleOverlays"; String testId = "drawFrame_multipleOverlays";
float[] translateMatrix1 = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(translateMatrix1, /* mOffset= */ 0, /* x= */ 0.5f, /* y= */ 0.5f, /* z= */ 1);
SpannableString overlayText = new SpannableString(/* source= */ "Overlay 1"); SpannableString overlayText = new SpannableString(/* source= */ "Overlay 1");
overlayText.setSpan( overlayText.setSpan(
new ForegroundColorSpan(Color.GRAY), new ForegroundColorSpan(Color.GRAY),
@ -376,7 +384,7 @@ public class OverlayShaderProgramPixelTest {
/* end= */ 4, /* end= */ 4,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
OverlaySettings overlaySettings1 = OverlaySettings overlaySettings1 =
new OverlaySettings.Builder().setMatrix(translateMatrix1).build(); new OverlaySettings.Builder().setVideoFrameAnchor(0.5f, 0.5f).build();
TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings1); TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings1);
Bitmap bitmap = readBitmap(OVERLAY_PNG_ASSET_PATH); Bitmap bitmap = readBitmap(OVERLAY_PNG_ASSET_PATH);
OverlaySettings overlaySettings2 = new OverlaySettings.Builder().setAlpha(0.5f).build(); OverlaySettings overlaySettings2 = new OverlaySettings.Builder().setAlpha(0.5f).build();
@ -407,15 +415,12 @@ public class OverlayShaderProgramPixelTest {
/* start= */ 0, /* start= */ 0,
/* end= */ overlayText.length(), /* end= */ overlayText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
float[] scaleTextMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.scaleM(scaleTextMatrix, /* mOffset= */ 0, /* x= */ 0.5f, /* y= */ 0.5f, /* z= */ 1);
OverlaySettings overlaySettings1 = OverlaySettings overlaySettings1 =
new OverlaySettings.Builder().setMatrix(scaleTextMatrix).build(); new OverlaySettings.Builder().setScale(/* x= */ 0.5f, /* y= */ 0.5f).build();
TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings1); TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings1);
Bitmap bitmap = readBitmap(OVERLAY_PNG_ASSET_PATH); Bitmap bitmap = readBitmap(OVERLAY_PNG_ASSET_PATH);
float[] scaleMatrix = GlUtil.create4x4IdentityMatrix(); OverlaySettings overlaySettings2 =
Matrix.scaleM(scaleMatrix, /* mOffset= */ 0, /* x= */ 3, /* y= */ 3, /* z= */ 1); new OverlaySettings.Builder().setScale(/* x= */ 3, /* y= */ 3).build();
OverlaySettings overlaySettings2 = new OverlaySettings.Builder().setMatrix(scaleMatrix).build();
BitmapOverlay bitmapOverlay = BitmapOverlay.createStaticBitmapOverlay(bitmap, overlaySettings2); BitmapOverlay bitmapOverlay = BitmapOverlay.createStaticBitmapOverlay(bitmap, overlaySettings2);
overlayShaderProgram = overlayShaderProgram =
@ -435,6 +440,57 @@ public class OverlayShaderProgramPixelTest {
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
} }
@Test
public void drawFrame_scaledBitmapOverlay_letterboxStretchesOverlay() throws Exception {
String testId = "drawFrame_scaledBitmapOverlay";
Bitmap overlayBitmap = readBitmap(OVERLAY_PNG_ASSET_PATH);
overlayShaderProgram =
new OverlayEffect(ImmutableList.of(new LetterBoxStretchedBitmapOverlay(overlayBitmap)))
.toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = overlayShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(OVERLAY_BITMAP_SCALED);
overlayShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
private static final class LetterBoxStretchedBitmapOverlay extends BitmapOverlay {
private final OverlaySettings.Builder overlaySettingsBuilder;
private final Bitmap overlayBitmap;
private @MonotonicNonNull OverlaySettings overlaySettings;
public LetterBoxStretchedBitmapOverlay(Bitmap overlayBitmap) {
this.overlayBitmap = overlayBitmap;
overlaySettingsBuilder = new OverlaySettings.Builder();
}
@Override
public Bitmap getBitmap(long presentationTimeUs) {
return overlayBitmap;
}
@Override
public void configure(Size videoSize) {
overlaySettingsBuilder.setScale(
/* x= */ videoSize.getWidth() / (float) overlayBitmap.getWidth(), /* y= */ 1);
overlaySettings = overlaySettingsBuilder.build();
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
return checkNotNull(overlaySettings);
}
}
private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException { private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException {
int outputTexId = int outputTexId =
GlUtil.createTexture( GlUtil.createTexture(

View File

@ -19,37 +19,50 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.media3.common.util.GlUtil;
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 an {@link TextureOverlay} is displayed on the screen. */ /** Contains information to control how a {@link TextureOverlay} is displayed on the screen. */
@UnstableApi @UnstableApi
public final class OverlaySettings { public final class OverlaySettings {
public final boolean useHdr; public final boolean useHdr;
public final float alpha; public final float alpha;
public final float[] matrix; public final Pair<Float, Float> videoFrameAnchor;
public final Pair<Float, Float> anchor; public final Pair<Float, Float> overlayAnchor;
public final Pair<Float, Float> scale;
public final float rotationDegrees;
private OverlaySettings(boolean useHdr, float alpha, float[] matrix, Pair<Float, Float> anchor) { private OverlaySettings(
boolean useHdr,
float alpha,
Pair<Float, Float> videoFrameAnchor,
Pair<Float, Float> overlayAnchor,
Pair<Float, Float> scale,
float rotationDegrees) {
this.useHdr = useHdr; this.useHdr = useHdr;
this.alpha = alpha; this.alpha = alpha;
this.matrix = matrix; this.videoFrameAnchor = videoFrameAnchor;
this.anchor = anchor; this.overlayAnchor = overlayAnchor;
this.scale = scale;
this.rotationDegrees = rotationDegrees;
} }
/** 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;
private float alpha; private float alpha;
private float[] matrix; private Pair<Float, Float> videoFrameAnchor;
private Pair<Float, Float> anchor; private Pair<Float, Float> overlayAnchor;
private Pair<Float, Float> scale;
private float rotationDegrees;
/** Creates a new {@link Builder}. */ /** Creates a new {@link Builder}. */
public Builder() { public Builder() {
alpha = 1f; alpha = 1f;
matrix = GlUtil.create4x4IdentityMatrix(); videoFrameAnchor = Pair.create(0f, 0f);
anchor = Pair.create(0f, 0f); overlayAnchor = Pair.create(0f, 0f);
scale = Pair.create(1f, 1f);
rotationDegrees = 0f;
} }
/** /**
@ -64,18 +77,6 @@ public final class OverlaySettings {
return this; return this;
} }
/**
* Sets the {@link android.opengl.Matrix} used to transform the overlay before applying it to a
* frame.
*
* <p>Set to always return the identity matrix by default.
*/
@CanIgnoreReturnValue
public Builder setMatrix(float[] matrix) {
this.matrix = matrix;
return this;
}
/** /**
* Sets the alpha value of the overlay, altering its transparency. * Sets the alpha value of the overlay, altering its transparency.
* *
@ -91,28 +92,79 @@ public final class OverlaySettings {
} }
/** /**
* Sets the coordinates for the anchor point of the overlay. * Sets the coordinates for the anchor point of the overlay within the video frame.
* *
* <p>The anchor point is the point inside the overlay that the overlay is positioned from. * <p>The coordinates are specified in Normalised Device Coordinates (NDCs) relative to the
* video frame. Set to always return {@code (0,0)} (the center of the video frame) by default.
* *
* <p>The coordinates are specified in Normalised Device Coordinates (NDCs). Set to always * <p>For example, a value of {@code (+1,+1)} will move the overlay's {@linkplain
* return {@code (0,0)} (the center) by default. * #setOverlayAnchor anchor point} to the top right corner of the video frame.
* *
* @param x The NDC x-coordinate in the range [-1, 1]. * @param x The NDC x-coordinate in the range [-1, 1].
* @param y The NDC y-coordinate in the range [-1, 1]. * @param y The NDC y-coordinate in the range [-1, 1].
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setAnchor( public Builder setVideoFrameAnchor(
@FloatRange(from = -1, to = 1) float x, @FloatRange(from = -1, to = 1) float y) { @FloatRange(from = -1, to = 1) float x, @FloatRange(from = -1, to = 1) float y) {
checkArgument(-1 <= x && x <= 1); checkArgument(-1 <= x && x <= 1);
checkArgument(-1 <= y && y <= 1); checkArgument(-1 <= y && y <= 1);
this.anchor = Pair.create(x, y); this.videoFrameAnchor = Pair.create(x, y);
return this;
}
/**
* Sets the coordinates for the anchor point of the overlay.
*
* <p>The anchor point is the point inside the overlay that is placed on the {@linkplain
* #setVideoFrameAnchor video frame anchor}
*
* <p>The coordinates are specified in Normalised Device Coordinates (NDCs) relative to the
* overlay frame. Set to return {@code (0,0)} (the center of the overlay) by default.
*
* <p>For example, a value of {@code (+1,-1)} will result in the overlay being positioned from
* the bottom right corner of its frame.
*
* @param x The NDC x-coordinate in the range [-1, 1].
* @param y The NDC y-coordinate in the range [-1, 1].
*/
@CanIgnoreReturnValue
public Builder setOverlayAnchor(
@FloatRange(from = -1, to = 1) float x, @FloatRange(from = -1, to = 1) float y) {
checkArgument(-1 <= x && x <= 1);
checkArgument(-1 <= y && y <= 1);
this.overlayAnchor = Pair.create(x, y);
return this;
}
/**
* Sets the scaling of the overlay.
*
* @param x The desired scaling in the x axis of the overlay.
* @param y The desired scaling in the y axis of the overlay.
*/
@CanIgnoreReturnValue
public Builder setScale(float x, float y) {
this.scale = Pair.create(x, y);
return this;
}
/**
* Sets the rotation of the overlay, counter-clockwise.
*
* <p>The overlay is rotated at the center of its frame.
*
* @param rotationDegree The desired degrees of rotation, counter-clockwise.
*/
@CanIgnoreReturnValue
public Builder setRotationDegrees(float rotationDegree) {
this.rotationDegrees = rotationDegree;
return this; return this;
} }
/** Creates an instance of {@link OverlaySettings}, using defaults if values are unset. */ /** Creates an instance of {@link OverlaySettings}, using defaults if values are unset. */
public OverlaySettings build() { public OverlaySettings build() {
return new OverlaySettings(useHdr, alpha, matrix, anchor); return new OverlaySettings(
useHdr, alpha, videoFrameAnchor, overlayAnchor, scale, rotationDegrees);
} }
} }
} }

View File

@ -35,9 +35,16 @@ import com.google.common.collect.ImmutableList;
private final GlProgram glProgram; private final GlProgram glProgram;
private final ImmutableList<TextureOverlay> overlays; private final ImmutableList<TextureOverlay> overlays;
private final float[] videoFrameAnchorMatrix;
private final float[] videoFrameAnchorMatrixInv;
private final float[] aspectRatioMatrix; private final float[] aspectRatioMatrix;
private final float[] overlayMatrix; private final float[] scaleMatrix;
private final float[] anchorMatrix; private final float[] scaleMatrixInv;
private final float[] overlayAnchorMatrix;
private final float[] overlayAnchorMatrixInv;
private final float[] rotateMatrix;
private final float[] overlayAspectRatioMatrix;
private final float[] overlayAspectRatioMatrixInv;
private final float[] transformationMatrix; private final float[] transformationMatrix;
private int videoWidth; private int videoWidth;
@ -63,8 +70,15 @@ import com.google.common.collect.ImmutableList;
"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;
aspectRatioMatrix = GlUtil.create4x4IdentityMatrix(); aspectRatioMatrix = GlUtil.create4x4IdentityMatrix();
overlayMatrix = GlUtil.create4x4IdentityMatrix(); videoFrameAnchorMatrix = GlUtil.create4x4IdentityMatrix();
anchorMatrix = GlUtil.create4x4IdentityMatrix(); videoFrameAnchorMatrixInv = GlUtil.create4x4IdentityMatrix();
overlayAnchorMatrix = GlUtil.create4x4IdentityMatrix();
overlayAnchorMatrixInv = GlUtil.create4x4IdentityMatrix();
rotateMatrix = GlUtil.create4x4IdentityMatrix();
scaleMatrix = GlUtil.create4x4IdentityMatrix();
scaleMatrixInv = GlUtil.create4x4IdentityMatrix();
overlayAspectRatioMatrix = GlUtil.create4x4IdentityMatrix();
overlayAspectRatioMatrixInv = GlUtil.create4x4IdentityMatrix();
transformationMatrix = GlUtil.create4x4IdentityMatrix(); transformationMatrix = GlUtil.create4x4IdentityMatrix();
try { try {
glProgram = glProgram =
@ -105,43 +119,163 @@ import com.google.common.collect.ImmutableList;
texUnitIndex); texUnitIndex);
GlUtil.setToIdentity(aspectRatioMatrix); GlUtil.setToIdentity(aspectRatioMatrix);
GlUtil.setToIdentity(videoFrameAnchorMatrix);
GlUtil.setToIdentity(videoFrameAnchorMatrixInv);
GlUtil.setToIdentity(overlayAnchorMatrix);
GlUtil.setToIdentity(overlayAnchorMatrixInv);
GlUtil.setToIdentity(scaleMatrix);
GlUtil.setToIdentity(scaleMatrixInv);
GlUtil.setToIdentity(rotateMatrix);
GlUtil.setToIdentity(overlayAspectRatioMatrix);
GlUtil.setToIdentity(overlayAspectRatioMatrixInv);
GlUtil.setToIdentity(transformationMatrix);
// Anchor point of overlay within output frame.
Pair<Float, Float> videoFrameAnchor =
overlay.getOverlaySettings(presentationTimeUs).videoFrameAnchor;
Matrix.translateM(
videoFrameAnchorMatrix,
MATRIX_OFFSET,
videoFrameAnchor.first,
videoFrameAnchor.second,
/* z= */ 0f);
Matrix.invertM(
videoFrameAnchorMatrixInv, MATRIX_OFFSET, videoFrameAnchorMatrix, MATRIX_OFFSET);
Matrix.scaleM( Matrix.scaleM(
aspectRatioMatrix, aspectRatioMatrix,
MATRIX_OFFSET, MATRIX_OFFSET,
videoWidth / (float) overlay.getTextureSize(presentationTimeUs).getWidth(), videoWidth / (float) overlay.getTextureSize(presentationTimeUs).getWidth(),
videoHeight / (float) overlay.getTextureSize(presentationTimeUs).getHeight(), videoHeight / (float) overlay.getTextureSize(presentationTimeUs).getHeight(),
/* z= */ 1); /* z= */ 1f);
Matrix.invertM(
overlayMatrix, // Scale the image.
Pair<Float, Float> scale = overlay.getOverlaySettings(presentationTimeUs).scale;
Matrix.setIdentityM(scaleMatrix, MATRIX_OFFSET);
Matrix.scaleM(
scaleMatrix,
MATRIX_OFFSET, MATRIX_OFFSET,
overlay.getOverlaySettings(presentationTimeUs).matrix, scaleMatrix,
MATRIX_OFFSET); MATRIX_OFFSET,
Pair<Float, Float> overlayAnchor = overlay.getOverlaySettings(presentationTimeUs).anchor; scale.first,
GlUtil.setToIdentity(anchorMatrix); scale.second,
/* z= */ 1f);
Matrix.invertM(scaleMatrixInv, MATRIX_OFFSET, scaleMatrix, MATRIX_OFFSET);
// Translate the overlay within its frame.
Pair<Float, Float> overlayAnchor =
overlay.getOverlaySettings(presentationTimeUs).overlayAnchor;
Matrix.setIdentityM(overlayAnchorMatrix, MATRIX_OFFSET);
Matrix.translateM( Matrix.translateM(
anchorMatrix, overlayAnchorMatrix,
/* mOffset= */ 0, MATRIX_OFFSET,
overlayAnchor.first overlayAnchor.first,
* overlay.getTextureSize(presentationTimeUs).getWidth() overlayAnchor.second,
/ videoWidth, /* z= */ 0f);
overlayAnchor.second Matrix.invertM(overlayAnchorMatrixInv, MATRIX_OFFSET, overlayAnchorMatrix, MATRIX_OFFSET);
* overlay.getTextureSize(presentationTimeUs).getHeight()
/ videoHeight, // Rotate the image.
/* z= */ 1); Matrix.setIdentityM(rotateMatrix, MATRIX_OFFSET);
Matrix.rotateM(
rotateMatrix,
MATRIX_OFFSET,
rotateMatrix,
MATRIX_OFFSET,
overlay.getOverlaySettings(presentationTimeUs).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(
overlayAspectRatioMatrix,
MATRIX_OFFSET,
(float) overlay.getTextureSize(presentationTimeUs).getHeight()
/ (float) overlay.getTextureSize(presentationTimeUs).getWidth(),
/* y= */ 1f,
/* z= */ 1f);
Matrix.invertM(
overlayAspectRatioMatrixInv, MATRIX_OFFSET, overlayAspectRatioMatrix, MATRIX_OFFSET);
// Rotation needs to be agnostic of the scaling matrix and the aspect ratios.
Matrix.multiplyMM( Matrix.multiplyMM(
transformationMatrix, transformationMatrix,
MATRIX_OFFSET, MATRIX_OFFSET,
overlayMatrix, transformationMatrix,
MATRIX_OFFSET, MATRIX_OFFSET,
anchorMatrix, scaleMatrixInv,
MATRIX_OFFSET); MATRIX_OFFSET);
Matrix.multiplyMM( 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,
MATRIX_OFFSET);
// Correct for aspect ratio of image in output frame.
Matrix.multiplyMM(
transformationMatrix,
MATRIX_OFFSET,
transformationMatrix, transformationMatrix,
MATRIX_OFFSET, MATRIX_OFFSET,
aspectRatioMatrix, aspectRatioMatrix,
MATRIX_OFFSET);
// Anchor position in output frame.
Matrix.multiplyMM(
transformationMatrix,
MATRIX_OFFSET, MATRIX_OFFSET,
transformationMatrix, transformationMatrix,
MATRIX_OFFSET,
videoFrameAnchorMatrixInv,
MATRIX_OFFSET); MATRIX_OFFSET);
glProgram.setFloatsUniform( glProgram.setFloatsUniform(
Util.formatInvariant("uTransformationMatrix%d", texUnitIndex), transformationMatrix); Util.formatInvariant("uTransformationMatrix%d", texUnitIndex), transformationMatrix);