diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bc413b90e6..58b27e2e6e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,8 @@ * IMA extension: * Session: * UI: + * Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and + `CompositionPlayer`. * Downloads: * Add partial download support for progressive streams. Apps can prepare a progressive stream with `DownloadHelper`, and request a diff --git a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt index 02246b11d7..a89b8c97b5 100644 --- a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt +++ b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt @@ -16,24 +16,22 @@ package androidx.media3.ui.compose -import android.view.Surface +import android.view.SurfaceView +import android.view.TextureView import androidx.annotation.IntDef -import androidx.compose.foundation.AndroidEmbeddedExternalSurface -import androidx.compose.foundation.AndroidExternalSurface -import androidx.compose.foundation.AndroidExternalSurfaceScope import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi /** * Provides a dedicated drawing [Surface] for media playbacks using a [Player]. * - * The player's video output is displayed with either a - * [android.view.SurfaceView]/[AndroidExternalSurface] or a - * [android.view.TextureView]/[AndroidEmbeddedExternalSurface]. + * The player's video output is displayed with either a [android.view.SurfaceView] or a + * [android.view.TextureView]. * * [Player] takes care of attaching the rendered output to the [Surface] and clearing it, when it is * destroyed. @@ -52,32 +50,36 @@ fun PlayerSurface( // Player might change between compositions, // we need long-lived surface-related lambdas to always use the latest value val currentPlayer by rememberUpdatedState(player) - val onSurfaceCreated: (Surface) -> Unit = { surface -> - if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) - currentPlayer.setVideoSurface(surface) - } - val onSurfaceDestroyed: () -> Unit = { - if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) - currentPlayer.clearVideoSurface() - } - val onSurfaceInitialized: AndroidExternalSurfaceScope.() -> Unit = { - onSurface { surface, _, _ -> - onSurfaceCreated(surface) - surface.onDestroyed { onSurfaceDestroyed() } - } - } when (surfaceType) { SURFACE_TYPE_SURFACE_VIEW -> - AndroidExternalSurface(modifier = modifier, onInit = onSurfaceInitialized) + AndroidView( + factory = { + SurfaceView(it).apply { + if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) + currentPlayer.setVideoSurfaceView(this) + } + }, + onReset = {}, + modifier = modifier, + ) SURFACE_TYPE_TEXTURE_VIEW -> - AndroidEmbeddedExternalSurface(modifier = modifier, onInit = onSurfaceInitialized) + AndroidView( + factory = { + TextureView(it).apply { + if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) + currentPlayer.setVideoTextureView(this) + } + }, + onReset = {}, + modifier = modifier, + ) else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType") } } /** - * The type of surface view used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or + * The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or * [SURFACE_TYPE_TEXTURE_VIEW]. */ @UnstableApi @@ -86,7 +88,7 @@ fun PlayerSurface( @IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW) annotation class SurfaceType -/** Surface type equivalent to [android.view.SurfaceView]. */ +/** Surface type to create [android.view.SurfaceView]. */ @UnstableApi const val SURFACE_TYPE_SURFACE_VIEW = 1 -/** Surface type equivalent to [android.view.TextureView]. */ +/** Surface type to create [android.view.TextureView]. */ @UnstableApi const val SURFACE_TYPE_TEXTURE_VIEW = 2 diff --git a/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt b/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt new file mode 100644 index 0000000000..81ed7a4c83 --- /dev/null +++ b/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2025 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 + * + * https://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.ui.compose + +import android.view.SurfaceView +import android.view.TextureView +import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.media3.common.Player +import androidx.media3.ui.compose.utils.TestPlayer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit test for [PlayerSurface]. */ +@RunWith(AndroidJUnit4::class) +class PlayerSurfaceTest { + + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun playerSurface_withSurfaceViewType_setsSurfaceViewOnPlayer() { + val player = TestPlayer() + + composeTestRule.setContent { + PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW) + } + composeTestRule.waitForIdle() + + assertThat(player.videoOutput).isInstanceOf(SurfaceView::class.java) + } + + @Test + fun playerSurface_withTextureViewType_setsTextureViewOnPlayer() { + val player = TestPlayer() + + composeTestRule.setContent { + PlayerSurface(player = player, surfaceType = SURFACE_TYPE_TEXTURE_VIEW) + } + composeTestRule.waitForIdle() + + assertThat(player.videoOutput).isInstanceOf(TextureView::class.java) + } + + @Test + fun playerSurface_withoutSupportedCommand_doesNotSetSurfaceOnPlayer() { + val player = TestPlayer() + player.removeCommands(Player.COMMAND_SET_VIDEO_SURFACE) + + composeTestRule.setContent { + PlayerSurface(player = player, surfaceType = SURFACE_TYPE_TEXTURE_VIEW) + } + composeTestRule.waitForIdle() + + assertThat(player.videoOutput).isNull() + } + + @Test + fun playerSurface_withUpdateSurfaceType_setsNewSurfaceOnPlayer() { + val player = TestPlayer() + + lateinit var surfaceType: MutableIntState + composeTestRule.setContent { + surfaceType = remember { mutableIntStateOf(SURFACE_TYPE_TEXTURE_VIEW) } + PlayerSurface(player = player, surfaceType = surfaceType.intValue) + } + composeTestRule.waitForIdle() + surfaceType.intValue = SURFACE_TYPE_SURFACE_VIEW + composeTestRule.waitForIdle() + + assertThat(player.videoOutput).isInstanceOf(SurfaceView::class.java) + } +} diff --git a/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/utils/TestPlayer.kt b/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/utils/TestPlayer.kt index 208421ae3d..ef6e63e0ee 100644 --- a/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/utils/TestPlayer.kt +++ b/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/utils/TestPlayer.kt @@ -41,6 +41,9 @@ internal class TestPlayer : SimpleBasePlayer(Looper.myLooper()!!) { .setPlayWhenReady(true, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) .build() + var videoOutput: Any? = null + private set + override fun getState(): State { return state } @@ -99,6 +102,18 @@ internal class TestPlayer : SimpleBasePlayer(Looper.myLooper()!!) { return Futures.immediateVoidFuture() } + override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> { + this.videoOutput = videoOutput + return Futures.immediateVoidFuture() + } + + override fun handleClearVideoOutput(videoOutput: Any?): ListenableFuture<*> { + if (videoOutput == null || videoOutput == this.videoOutput) { + this.videoOutput = null + } + return Futures.immediateVoidFuture() + } + fun setPlaybackState(playbackState: @Player.State Int) { state = state.buildUpon().setPlaybackState(playbackState).build() invalidateState()