Implement text and drawable overlays

Implements milestone 1.2 of the [overlays implementation plan](https://docs.google.com/document/d/1EcP2GN8k8N74hHZyD0KTqm9oQo5-W1dZMqIVyqVGtlo/edit#bookmark=id.76uzcie1dg9d)

PiperOrigin-RevId: 493282868
This commit is contained in:
tofunmi 2022-12-06 14:03:13 +00:00 committed by Ian Baker
parent cc43ddb528
commit e6cb502bc6
5 changed files with 275 additions and 0 deletions

View File

@ -27,10 +27,14 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.Matrix;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.Pair;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlUtil;
@ -60,6 +64,10 @@ public class OverlayTextureProcessorPixelTest {
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_default.png";
public static final String OVERLAY_BITMAP_SCALED =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_scaled.png";
public static final String OVERLAY_TEXT_DEFAULT =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_text_default.png";
public static final String OVERLAY_TEXT_TRANSLATE =
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_text_translate.png";
private final Context context = getApplicationContext();
@ -159,6 +167,65 @@ public class OverlayTextureProcessorPixelTest {
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_textOverlay_blendsTextIntoFrame() throws Exception {
String testId = "drawFrame_textOverlay";
SpannableString overlayText = new SpannableString(/* source= */ "Text styling");
overlayText.setSpan(
new ForegroundColorSpan(Color.GRAY),
/* start= */ 0,
/* end= */ 4,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
TextOverlay staticTextOverlay = TextOverlay.createStaticTextOverlay(overlayText);
overlayTextureProcessor =
new OverlayEffect(ImmutableList.of(staticTextOverlay))
.toGlTextureProcessor(context, /* useHdr= */ false);
Pair<Integer, Integer> outputSize = overlayTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.first, outputSize.second);
Bitmap expectedBitmap = readBitmap(OVERLAY_TEXT_DEFAULT);
overlayTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromCurrentGlFramebuffer(outputSize.first, outputSize.second);
maybeSaveTestBitmapToCacheDirectory(testId, /* bitmapLabel= */ "actual", actualBitmap);
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_translatedTextOverlay_blendsTextIntoFrame() throws Exception {
String testId = "drawFrame_translatedTextOverlay";
float[] translateMatrix = GlUtil.create4x4IdentityMatrix();
Matrix.translateM(translateMatrix, /* mOffset= */ 0, /* x= */ 0.5f, /* y= */ 0.5f, /* z= */ 1);
SpannableString overlayText = new SpannableString(/* source= */ "Text styling");
overlayText.setSpan(
new ForegroundColorSpan(Color.GRAY),
/* start= */ 0,
/* end= */ 4,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
OverlaySettings overlaySettings =
new OverlaySettings.Builder().setMatrix(translateMatrix).build();
TextOverlay staticTextOverlay =
TextOverlay.createStaticTextOverlay(overlayText, overlaySettings);
overlayTextureProcessor =
new OverlayEffect(ImmutableList.of(staticTextOverlay))
.toGlTextureProcessor(context, /* useHdr= */ false);
Pair<Integer, Integer> outputSize = overlayTextureProcessor.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.first, outputSize.second);
Bitmap expectedBitmap = readBitmap(OVERLAY_TEXT_TRANSLATE);
overlayTextureProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromCurrentGlFramebuffer(outputSize.first, outputSize.second);
maybeSaveTestBitmapToCacheDirectory(testId, /* bitmapLabel= */ "actual", actualBitmap);
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException {
int outputTexId =
GlUtil.createTexture(

View File

@ -0,0 +1,79 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.effect;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.media3.common.util.UnstableApi;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Creates a {@link TextureOverlay} from {@link Drawable}.
*
* <p>Uses a canvas to draw {@link DrawableOverlay} onto {@link BitmapOverlay}, which is then
* displayed on each frame.
*/
@UnstableApi
public abstract class DrawableOverlay extends BitmapOverlay {
private @MonotonicNonNull Bitmap lastBitmap;
private @MonotonicNonNull Drawable lastDrawable;
/**
* Returns the overlay {@link Drawable} displayed at the specified timestamp.
*
* @param presentationTimeUs The presentation timestamp of the current frame, in microseconds.
*/
public abstract Drawable getDrawable(long presentationTimeUs);
@Override
public Bitmap getBitmap(long presentationTimeUs) {
Drawable overlayDrawable = getDrawable(presentationTimeUs);
// TODO(b/227625365): Drawable doesn't implement the equals method, so investigate other methods
// of
// detecting the need to redraw the bitmap.
if (!overlayDrawable.equals(lastDrawable)) {
lastDrawable = overlayDrawable;
lastBitmap =
Bitmap.createBitmap(
lastDrawable.getIntrinsicWidth(),
lastDrawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(lastBitmap);
lastDrawable.draw(canvas);
}
return checkNotNull(lastBitmap);
}
/**
* Creates a {@link TextOverlay} that shows the {@code overlayDrawable} with the same {@link
* OverlaySettings} throughout the whole video.
*
* @param overlaySettings The {@link OverlaySettings} configuring how the overlay is displayed on
* the frames.
*/
public static DrawableOverlay createStaticDrawableOverlay(
Drawable drawable, OverlaySettings overlaySettings) {
return new DrawableOverlay() {
@Override
public Drawable getDrawable(long presentationTimeUs) {
return drawable;
}
};
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.effect;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.SDK_INT;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.text.Layout;
import android.text.SpannableString;
import android.text.StaticLayout;
import android.text.TextPaint;
import androidx.annotation.DoNotInline;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.UnstableApi;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Creates a {@link TextureOverlay} from text.
*
* <p>Uses a {@link SpannableString} store the text and support advanced per-character text styling.
*/
@UnstableApi
public abstract class TextOverlay extends BitmapOverlay {
public static final int TEXT_SIZE_PIXELS = 100;
private @MonotonicNonNull Bitmap lastBitmap;
private @MonotonicNonNull SpannableString lastText;
/**
* Returns the overlay text displayed at the specified timestamp.
*
* @param presentationTimeUs The presentation timestamp of the current frame, in microseconds.
*/
public abstract SpannableString getText(long presentationTimeUs);
@SuppressLint("InlinedApi") // Inlined Layout constants.
@Override
public Bitmap getBitmap(long presentationTimeUs) {
SpannableString overlayText = getText(presentationTimeUs);
if (!overlayText.equals(lastText)) {
lastText = overlayText;
TextPaint textPaint = new TextPaint();
textPaint.setTextSize(TEXT_SIZE_PIXELS);
StaticLayout staticLayout;
int width = (int) textPaint.measureText(overlayText, /* start= */ 0, overlayText.length());
if (SDK_INT >= 23) {
staticLayout = Api23.getStaticLayout(overlayText, textPaint, width);
} else {
staticLayout =
new StaticLayout(
overlayText,
textPaint,
width,
Layout.Alignment.ALIGN_NORMAL,
Layout.DEFAULT_LINESPACING_MULTIPLIER,
Layout.DEFAULT_LINESPACING_ADDITION,
/* includepad= */ true);
}
lastBitmap =
Bitmap.createBitmap(
staticLayout.getWidth(), staticLayout.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(checkNotNull(lastBitmap));
staticLayout.draw(canvas);
}
return checkNotNull(lastBitmap);
}
/**
* Creates a {@link TextOverlay} that shows the {@code overlayText} with the same default settings
* in {@link OverlaySettings} throughout the whole video.
*/
public static TextOverlay createStaticTextOverlay(SpannableString overlayText) {
return new TextOverlay() {
@Override
public SpannableString getText(long presentationTimeUs) {
return overlayText;
}
};
}
/**
* Creates a {@link TextOverlay} that shows the {@code overlayText} with the same {@link
* OverlaySettings} throughout the whole video.
*
* @param overlaySettings The {@link OverlaySettings} configuring how the overlay is displayed on
* the frames.
*/
public static TextOverlay createStaticTextOverlay(
SpannableString overlayText, OverlaySettings overlaySettings) {
return new TextOverlay() {
@Override
public SpannableString getText(long presentationTimeUs) {
return overlayText;
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
return overlaySettings;
}
};
}
@RequiresApi(23)
private static final class Api23 {
@DoNotInline
public static StaticLayout getStaticLayout(
SpannableString text, TextPaint textPaint, int width) {
return StaticLayout.Builder.obtain(
text, /* start= */ 0, /* end= */ text.length(), textPaint, width)
.build();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB