diff --git a/demos/effect/lint.xml b/demos/effect/lint.xml
new file mode 100644
index 0000000000..46a2afc3a1
--- /dev/null
+++ b/demos/effect/lint.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt b/demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt
new file mode 100644
index 0000000000..e978c3ae46
--- /dev/null
+++ b/demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.effect
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.os.Handler
+import androidx.media3.common.VideoFrameProcessingException
+import androidx.media3.common.util.Size
+import androidx.media3.common.util.Util
+import androidx.media3.effect.CanvasOverlay
+import kotlin.math.abs
+import kotlin.random.Random
+
+/** Mimics an emitter of confetti, dropping from the center of the frame. */
+internal class ConfettiOverlay : CanvasOverlay(/* useInputFrameSize= */ true) {
+
+ private val confettiList = mutableListOf()
+ private val paint = Paint()
+ private val handler = Handler(Util.getCurrentOrMainLooper())
+
+ private var addConfettiTask: (() -> Unit)? = null
+ private var width = 0f
+ private var height = 0f
+ private var started = false
+
+ override fun configure(videoSize: Size) {
+ super.configure(videoSize)
+ this.width = videoSize.width.toFloat()
+ this.height = videoSize.height.toFloat()
+ }
+
+ @Synchronized
+ override fun onDraw(canvas: Canvas, presentationTimeUs: Long) {
+ if (!started) {
+ start()
+ }
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
+
+ confettiList.removeAll { confetti ->
+ confetti.y > height / 2 || confetti.x <= 0 || confetti.x > width
+ }
+ for (confetti in confettiList) {
+ confetti.draw(canvas, paint)
+ confetti.update()
+ }
+ }
+
+ @Throws(VideoFrameProcessingException::class)
+ override fun release() {
+ super.release()
+ handler.post(this::stop)
+ }
+
+ /** Starts the confetti. */
+ fun start() {
+ addConfettiTask = this::addConfetti
+ handler.post(checkNotNull(addConfettiTask))
+ started = true
+ }
+
+ /** Stops the confetti. */
+ fun stop() {
+ handler.removeCallbacks(checkNotNull(addConfettiTask))
+ confettiList.clear()
+ started = false
+ addConfettiTask = null
+ }
+
+ @Synchronized
+ fun addConfetti() {
+ repeat(5) {
+ confettiList.add(
+ Confetti(
+ text = CONFETTI_TEXTS[abs(Random.nextInt()) % CONFETTI_TEXTS.size],
+ x = width / 2f,
+ y = EMITTER_POSITION_Y.toFloat(),
+ size = CONFETTI_BASE_SIZE + Random.nextInt(CONFETTI_SIZE_VARIATION),
+ color = Color.HSVToColor(floatArrayOf(Random.nextInt(360).toFloat(), 0.6f, 0.8f)),
+ )
+ )
+ }
+ handler.postDelayed(this::addConfetti, /* delayMillis= */ 100)
+ }
+
+ private class Confetti(
+ private val text: String,
+ private val size: Int,
+ private val color: Int,
+ var x: Float,
+ var y: Float,
+ ) {
+ private val speedX = 4 * (Random.nextFloat() * 2 - 1) // Random speed in x direction
+ private val speedY = 4 * Random.nextFloat() // Random speed in y direction
+ private val rotationSpeed = (Random.nextFloat() - 0.5f) * 4f // Random rotation speed
+
+ private var rotation = Random.nextFloat() * 360f
+
+ /** Draws the [Confetti] on the [Canvas]. */
+ fun draw(canvas: Canvas, paint: Paint) {
+ canvas.save()
+ paint.color = color
+ canvas.translate(x, y)
+ canvas.rotate(rotation)
+ paint.textSize = size.toFloat()
+ canvas.drawText(text, /* x= */ 0f, /* y= */ 0f, paint) // Only draw text
+ canvas.restore()
+ }
+
+ /** Updates the [Confetti]. */
+ fun update() {
+ x += speedX
+ y += speedY
+ rotation += rotationSpeed
+ }
+ }
+
+ private companion object {
+ val CONFETTI_TEXTS = listOf("❊", "✿", "❊", "✦︎", "♥︎", "☕︎")
+ const val EMITTER_POSITION_Y = -50
+ const val CONFETTI_BASE_SIZE = 30
+ const val CONFETTI_SIZE_VARIATION = 10
+ }
+}
diff --git a/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt b/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt
index 3beb442b5a..5ab2f12d01 100644
--- a/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt
+++ b/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt
@@ -68,8 +68,11 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util.SDK_INT
import androidx.media3.effect.Contrast
+import androidx.media3.effect.OverlayEffect
+import androidx.media3.effect.TextureOverlay
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
+import com.google.common.collect.ImmutableList
import kotlinx.coroutines.launch
class EffectActivity : ComponentActivity() {
@@ -279,7 +282,15 @@ class EffectActivity : ComponentActivity() {
onClick = {
val effectsList = mutableListOf()
- effectsList += Contrast(effectControlsState.contrastValue)
+ if (effectControlsState.contrastValue != 0f) {
+ effectsList += Contrast(effectControlsState.contrastValue)
+ }
+
+ val overlaysBuilder = ImmutableList.builder()
+ if (effectControlsState.confettiOverlayChecked) {
+ overlaysBuilder.add(ConfettiOverlay())
+ }
+ effectsList += OverlayEffect(overlaysBuilder.build())
onApplyEffectsClicked(effectsList)
effectControlsState = effectControlsState.copy(effectsChanged = false)
@@ -333,6 +344,17 @@ class EffectActivity : ComponentActivity() {
}
}
}
+ item {
+ EffectItem(
+ name = stringResource(R.string.confetti_overlay),
+ enabled = enabled,
+ onCheckedChange = { checked ->
+ onEffectControlsStateChange(
+ effectControlsState.copy(effectsChanged = true, confettiOverlayChecked = checked)
+ )
+ },
+ )
+ }
}
}
@@ -382,6 +404,7 @@ class EffectActivity : ComponentActivity() {
data class EffectControlsState(
val effectsChanged: Boolean = false,
val contrastValue: Float = 0f,
+ val confettiOverlayChecked: Boolean = false,
)
companion object {
diff --git a/demos/effect/src/main/res/values/strings.xml b/demos/effect/src/main/res/values/strings.xml
index 19e097ff49..d6977494a3 100644
--- a/demos/effect/src/main/res/values/strings.xml
+++ b/demos/effect/src/main/res/values/strings.xml
@@ -24,4 +24,5 @@
"File couldn't be opened. Please try again."
"Permission was not granted."
Contrast
+ Confetti Overlay