diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ClockOverlay.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ClockOverlay.java new file mode 100644 index 0000000000..292eebb2d5 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ClockOverlay.java @@ -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(); + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfettiOverlay.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfettiOverlay.java new file mode 100644 index 0000000000..c4bf75a453 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfettiOverlay.java @@ -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 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 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; + } + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index 148f3be4e2..049d6b48d5 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -114,6 +114,8 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final int OVERLAY_LOGO_AND_TIMER_INDEX = 10; public static final int BITMAP_OVERLAY_INDEX = 11; 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. public static final int HIGH_PITCHED_INDEX = 0; diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index 95f2f94849..b8382e6ff8 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -648,6 +648,12 @@ public final class TransformerActivity extends AppCompatActivity { TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings); overlaysBuilder.add(textOverlay); } + if (selectedEffects[ConfigurationActivity.CLOCK_OVERLAY_INDEX]) { + overlaysBuilder.add(new ClockOverlay()); + } + if (selectedEffects[ConfigurationActivity.CONFETTI_OVERLAY_INDEX]) { + overlaysBuilder.add(new ConfettiOverlay()); + } ImmutableList overlays = overlaysBuilder.build(); return overlays.isEmpty() ? null : new OverlayEffect(overlays); diff --git a/demos/transformer/src/main/res/values/arrays.xml b/demos/transformer/src/main/res/values/arrays.xml index 21e9cbc852..1d4f16c239 100644 --- a/demos/transformer/src/main/res/values/arrays.xml +++ b/demos/transformer/src/main/res/values/arrays.xml @@ -28,6 +28,8 @@ Overlay logo and timer Custom Bitmap Overlay Custom Text Overlay + Clock Overlay + Confetti Overlay High pitched diff --git a/libraries/effect/src/main/java/androidx/media3/effect/CanvasOverlay.java b/libraries/effect/src/main/java/androidx/media3/effect/CanvasOverlay.java new file mode 100644 index 0000000000..2c76c1cf6b --- /dev/null +++ b/libraries/effect/src/main/java/androidx/media3/effect/CanvasOverlay.java @@ -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}. + * + *

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}. + * + *

The default canvas size will be of the same size as the video frame. + * + *

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(); + } + } +}