Add a CanvasOverlay for easier drawing, and an example in demo app

PiperOrigin-RevId: 687223353
This commit is contained in:
claincly 2024-10-18 02:26:22 -07:00 committed by Copybara-Service
parent 08e6f30b68
commit ba3d2b3fef
6 changed files with 375 additions and 0 deletions

View File

@ -0,0 +1,105 @@
/*
* Copyright 2024 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.demo.transformer;
import static java.lang.Math.toRadians;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import androidx.media3.effect.CanvasOverlay;
import androidx.media3.effect.OverlaySettings;
/* package */ final class ClockOverlay extends CanvasOverlay {
private static final int CLOCK_COLOR = Color.WHITE;
private static final int DIAL_SIZE = 200;
private static final float DIAL_WIDTH = 3.f;
private static final float NEEDLE_WIDTH = 3.f;
private static final int NEEDLE_LENGTH = DIAL_SIZE / 2 - 20;
private static final int CENTRE_X = DIAL_SIZE / 2;
private static final int CENTRE_Y = DIAL_SIZE / 2;
private static final int DIAL_INSET = 5;
private static final RectF DIAL_BOUND =
new RectF(
/* left= */ DIAL_INSET,
/* top= */ DIAL_INSET,
/* right= */ DIAL_SIZE - DIAL_INSET,
/* bottom= */ DIAL_SIZE - DIAL_INSET);
private static final int HUB_SIZE = 5;
private static final float BOTTOM_RIGHT_ANCHOR_X = 1.f;
private static final float BOTTOM_RIGHT_ANCHOR_Y = -1.f;
private static final float ANCHOR_INSET_X = 0.1f;
private static final float ANCHOR_INSET_Y = -0.1f;
private final Paint dialPaint;
private final Paint needlePaint;
private final Paint hubPaint;
public ClockOverlay() {
super(/* useInputFrameSize= */ false);
setCanvasSize(/* width= */ DIAL_SIZE, /* height= */ DIAL_SIZE);
dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
dialPaint.setStyle(Paint.Style.STROKE);
dialPaint.setStrokeWidth(DIAL_WIDTH);
dialPaint.setColor(CLOCK_COLOR);
needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
needlePaint.setStrokeWidth(NEEDLE_WIDTH);
needlePaint.setColor(CLOCK_COLOR);
hubPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
hubPaint.setColor(CLOCK_COLOR);
}
@Override
public void onDraw(Canvas canvas, long presentationTimeUs) {
// Clears the canvas
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Draw the dial
canvas.drawArc(
DIAL_BOUND, /* startAngle= */ 0, /* sweepAngle= */ 360, /* useCenter= */ false, dialPaint);
// Draw the needle
float angle = 6 * presentationTimeUs / 1_000_000.f - 90;
double radians = toRadians(angle);
float startX = CENTRE_X - (float) (10 * Math.cos(radians));
float startY = CENTRE_Y - (float) (10 * Math.sin(radians));
float endX = CENTRE_X + (float) (NEEDLE_LENGTH * Math.cos(radians));
float endY = CENTRE_Y + (float) (NEEDLE_LENGTH * Math.sin(radians));
canvas.drawLine(startX, startY, endX, endY, needlePaint);
// Draw a small hub at the center
canvas.drawCircle(CENTRE_X, CENTRE_Y, HUB_SIZE, hubPaint);
}
@Override
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
return new OverlaySettings.Builder()
.setBackgroundFrameAnchor(
BOTTOM_RIGHT_ANCHOR_X - ANCHOR_INSET_X, BOTTOM_RIGHT_ANCHOR_Y - ANCHOR_INSET_Y)
.setOverlayFrameAnchor(BOTTOM_RIGHT_ANCHOR_X, BOTTOM_RIGHT_ANCHOR_Y)
.build();
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright 2024 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.demo.transformer;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util;
import androidx.media3.effect.CanvasOverlay;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/** Mimics an emitter of confetti, dropping from the center of the frame. */
/* package */ final class ConfettiOverlay extends CanvasOverlay {
private static final ImmutableList<String> CONFETTI_TEXTS =
ImmutableList.of("", "", "", "✦︎", "♥︎", "☕︎");
private static final int EMITTER_POSITION_Y = -50;
private static final int CONFETTI_BASE_SIZE = 30;
private static final int CONFETTI_SIZE_VARIATION = 10;
private final List<Confetti> confettiList;
private final Random random;
private final Paint paint;
private final Handler handler;
@Nullable private Runnable runnable;
private int width;
private int height;
private boolean started;
public ConfettiOverlay() {
super(/* useInputFrameSize= */ true);
confettiList = new ArrayList<>();
random = new Random();
paint = new Paint();
paint.setAntiAlias(true);
handler = new Handler(Util.getCurrentOrMainLooper());
}
@Override
public void configure(Size videoSize) {
super.configure(videoSize);
this.width = videoSize.getWidth();
this.height = videoSize.getHeight();
}
@Override
public synchronized void onDraw(Canvas canvas, long presentationTimeUs) {
if (!started) {
start();
}
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
for (int i = 0; i < confettiList.size(); i++) {
Confetti confetti = confettiList.get(i);
if (confetti.y > (float) height / 2 || confetti.x <= 0 || confetti.x > width) {
confettiList.remove(confetti);
continue;
}
confetti.draw(canvas, paint);
confetti.update();
}
}
/** Starts the confetti. */
public void start() {
runnable = this::addConfetti;
handler.post(runnable);
started = true;
}
/** Stops the confetti. */
public void stop() {
checkStateNotNull(runnable);
handler.removeCallbacks(runnable);
confettiList.clear();
started = false;
runnable = null;
}
@Override
public void release() throws VideoFrameProcessingException {
super.release();
handler.post(this::stop);
}
private synchronized void addConfetti() {
for (int i = 0; i < 5; i++) {
confettiList.add(
new Confetti(
CONFETTI_TEXTS.get(Math.abs(random.nextInt()) % CONFETTI_TEXTS.size()),
random,
/* x= */ (float) width / 2,
/* y= */ EMITTER_POSITION_Y,
/* size= */ CONFETTI_BASE_SIZE + random.nextInt(CONFETTI_SIZE_VARIATION),
/* color= */ Color.HSVToColor(
new float[] {
/* hue= */ random.nextInt(360), /* saturation= */ 0.6f, /* value= */ 0.8f
})));
}
handler.postDelayed(this::addConfetti, /* delayMillis= */ 100);
}
private static final class Confetti {
private final String text;
private final float speedX;
private final float speedY;
private final int size;
private final int color;
private float x;
private float y;
public Confetti(String text, Random random, float x, float y, int size, int color) {
this.text = text;
this.x = x;
this.y = y;
this.size = size;
this.color = color;
speedX = 4 * (random.nextFloat() * 2 - 1); // Random speed in x direction
speedY = 4 * random.nextFloat(); // Random downward speed
}
/** Draws the {@code Confetti} on the {@link Canvas}. */
public void draw(Canvas canvas, Paint paint) {
canvas.save();
paint.setColor(color);
paint.setTextSize(size);
canvas.drawText(text, x, y, paint);
canvas.restore();
}
/** Updates the {@code Confetti}. */
public void update() {
x += speedX;
y += speedY;
}
}
}

View File

@ -114,6 +114,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final int OVERLAY_LOGO_AND_TIMER_INDEX = 10; public static final int OVERLAY_LOGO_AND_TIMER_INDEX = 10;
public static final int BITMAP_OVERLAY_INDEX = 11; public static final int BITMAP_OVERLAY_INDEX = 11;
public static final int TEXT_OVERLAY_INDEX = 12; public static final int TEXT_OVERLAY_INDEX = 12;
public static final int CLOCK_OVERLAY_INDEX = 13;
public static final int CONFETTI_OVERLAY_INDEX = 14;
// Audio effect selections. // Audio effect selections.
public static final int HIGH_PITCHED_INDEX = 0; public static final int HIGH_PITCHED_INDEX = 0;

View File

@ -648,6 +648,12 @@ public final class TransformerActivity extends AppCompatActivity {
TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings); TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings);
overlaysBuilder.add(textOverlay); overlaysBuilder.add(textOverlay);
} }
if (selectedEffects[ConfigurationActivity.CLOCK_OVERLAY_INDEX]) {
overlaysBuilder.add(new ClockOverlay());
}
if (selectedEffects[ConfigurationActivity.CONFETTI_OVERLAY_INDEX]) {
overlaysBuilder.add(new ConfettiOverlay());
}
ImmutableList<TextureOverlay> overlays = overlaysBuilder.build(); ImmutableList<TextureOverlay> overlays = overlaysBuilder.build();
return overlays.isEmpty() ? null : new OverlayEffect(overlays); return overlays.isEmpty() ? null : new OverlayEffect(overlays);

View File

@ -28,6 +28,8 @@
<item>Overlay logo and timer</item> <item>Overlay logo and timer</item>
<item>Custom Bitmap Overlay</item> <item>Custom Bitmap Overlay</item>
<item>Custom Text Overlay</item> <item>Custom Text Overlay</item>
<item>Clock Overlay</item>
<item>Confetti Overlay</item>
</string-array> </string-array>
<string-array name="audio_effects_names"> <string-array name="audio_effects_names">
<item>High pitched</item> <item>High pitched</item>

View File

@ -0,0 +1,98 @@
/*
* Copyright 2024 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 androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link TextOverlay} that is backed by a {@link Canvas}.
*
* <p>Use this class when the size of the drawing {@link Canvas} is known, or when drawing to the
* entire video size is desied.
*/
@UnstableApi
public abstract class CanvasOverlay extends BitmapOverlay {
private final boolean useInputFrameSize;
private @MonotonicNonNull Bitmap lastBitmap;
private @MonotonicNonNull Canvas lastCanvas;
private volatile int width;
private volatile int height;
/**
* Creates a new {@code CanvasOverlay}.
*
* @param useInputFrameSize Whether to create the {@link Canvas} to match the input frame size, if
* {@code false}, {@link #setCanvasSize(int, int)} must be set before the first invocation to
* {@link #onDraw}.
*/
public CanvasOverlay(boolean useInputFrameSize) {
this.useInputFrameSize = useInputFrameSize;
}
/**
* Perform custom drawing onto the {@link Canvas}.
*
* @param canvas The {@link Canvas} to draw onto.
* @param presentationTimeUs The presentation timestamp, in microseconds.
*/
public abstract void onDraw(Canvas canvas, long presentationTimeUs);
/**
* Sets the size of the {@link Canvas}.
*
* <p>The default canvas size will be of the same size as the video frame.
*
* <p>The size will be applied on the next invocation of {@link #onDraw}.
*/
public void setCanvasSize(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void configure(Size videoSize) {
super.configure(videoSize);
if (useInputFrameSize) {
setCanvasSize(videoSize.getWidth(), videoSize.getHeight());
}
}
@Override
public Bitmap getBitmap(long presentationTimeUs) {
if (lastBitmap == null || lastBitmap.getWidth() != width || lastBitmap.getHeight() != height) {
lastBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
lastCanvas = new Canvas(lastBitmap);
}
onDraw(checkNotNull(lastCanvas), presentationTimeUs);
return lastBitmap;
}
@Override
public void release() throws VideoFrameProcessingException {
super.release();
if (lastBitmap != null) {
lastBitmap.recycle();
}
}
}