mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add a CanvasOverlay for easier drawing, and an example in demo app
PiperOrigin-RevId: 687223353
This commit is contained in:
parent
08e6f30b68
commit
ba3d2b3fef
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user