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:
parent
cc43ddb528
commit
e6cb502bc6
@ -27,10 +27,14 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
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.opengl.Matrix;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.media3.common.FrameProcessingException;
|
import androidx.media3.common.FrameProcessingException;
|
||||||
import androidx.media3.common.util.GlUtil;
|
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";
|
"media/bitmap/sample_mp4_first_frame/electrical_colors/overlay_bitmap_default.png";
|
||||||
public static final String OVERLAY_BITMAP_SCALED =
|
public 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";
|
||||||
|
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();
|
private final Context context = getApplicationContext();
|
||||||
|
|
||||||
@ -159,6 +167,65 @@ public class OverlayTextureProcessorPixelTest {
|
|||||||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
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 {
|
private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException {
|
||||||
int outputTexId =
|
int outputTexId =
|
||||||
GlUtil.createTexture(
|
GlUtil.createTexture(
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 534 KiB |
Loading…
x
Reference in New Issue
Block a user