From 59dd3baa7e7ed1301adec1df90b4a14d7dbaa148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Thu, 17 Jul 2025 15:36:05 +0200 Subject: [PATCH 1/2] Add `SphericalGLSurfaceView` support to `PlayerSurface` This change allows `PlayerSurface` to render video using `SphericalGLSurfaceView` when specifying the `SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW` type. The compose demo has also been updated to include a 360 video example that utilizes this new surface type. --- .../media3/demo/compose/MainActivity.kt | 15 +++++++-- .../media3/demo/compose/data/videos.kt | 15 ++++++--- .../spherical/SphericalGLSurfaceView.java | 5 +-- libraries/ui_compose/build.gradle | 1 + .../media3/ui/compose/PlayerSurface.kt | 33 +++++++++++++++---- .../media3/ui/compose/PlayerSurfaceTest.kt | 13 ++++++++ 6 files changed, 66 insertions(+), 16 deletions(-) diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt index d61ac2ad4df..3022adf4d1d 100644 --- a/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -44,6 +45,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.listen import androidx.media3.demo.compose.buttons.ExtraControls import androidx.media3.demo.compose.buttons.MinimalControls import androidx.media3.demo.compose.data.videos @@ -102,7 +104,7 @@ fun ComposeDemoApp(modifier: Modifier = Modifier) { private fun initializePlayer(context: Context): Player = ExoPlayer.Builder(context).build().apply { - setMediaItems(videos.map(MediaItem::fromUri)) + setMediaItems(videos.keys.map(MediaItem::fromUri)) prepare() } @@ -110,11 +112,20 @@ private fun initializePlayer(context: Context): Player = private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) { var showControls by remember { mutableStateOf(true) } var currentContentScaleIndex by remember { mutableIntStateOf(0) } + var surfaceType by remember { mutableIntStateOf(SURFACE_TYPE_SURFACE_VIEW) } val contentScale = CONTENT_SCALES[currentContentScaleIndex].second val presentationState = rememberPresentationState(player) val scaledModifier = Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp) + LaunchedEffect(player) { + player.listen { + currentMediaItem?.localConfiguration?.let { + surfaceType = videos.getValue(it.uri.toString()) + } + } + } + // Only use MediaPlayerScreen's modifier once for the top level Composable Box(modifier) { // Always leave PlayerSurface to be part of the Compose tree because it will be initialised in @@ -122,7 +133,7 @@ private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) { // because the Player will not emit the relevant event, e.g. the first frame being ready. PlayerSurface( player = player, - surfaceType = SURFACE_TYPE_SURFACE_VIEW, + surfaceType = surfaceType, modifier = scaledModifier.noRippleClickable { showControls = !showControls }, ) diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt index ed42d15d5a6..58479115e12 100644 --- a/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt @@ -15,10 +15,15 @@ */ package androidx.media3.demo.compose.data +import androidx.media3.ui.compose.SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW +import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW + val videos = - listOf( - "https://html5demos.com/assets/dizzy.mp4", - "https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4", - "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm", - "https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4", + mapOf( + "https://html5demos.com/assets/dizzy.mp4" to SURFACE_TYPE_SURFACE_VIEW, + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4" to SURFACE_TYPE_SURFACE_VIEW, + "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" to SURFACE_TYPE_SURFACE_VIEW, + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4" to SURFACE_TYPE_SURFACE_VIEW, + // https://bitmovin.com/demos/vr-360/ + "https://cdn.bitmovin.com/content/assets/playhouse-vr/progressive.mp4" to SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW, ) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/spherical/SphericalGLSurfaceView.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/spherical/SphericalGLSurfaceView.java index ad268a0aaa3..71b452faef3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/spherical/SphericalGLSurfaceView.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/spherical/SphericalGLSurfaceView.java @@ -47,8 +47,9 @@ /** * Renders a GL scene in a non-VR Activity that is affected by phone orientation and touch input. * - *

The two input components are the TYPE_GAME_ROTATION_VECTOR Sensor and a TouchListener. The GL - * renderer combines these two inputs to render a scene with the appropriate camera orientation. + *

The two input components are the {@link Sensor#TYPE_GAME_ROTATION_VECTOR + * TYPE_GAME_ROTATION_VECTOR} Sensor and a TouchListener. The GL renderer combines these two inputs + * to render a scene with the appropriate camera orientation. * *

The primary complexity in this class is related to the various rotations. It is important to * apply the touch and sensor rotations in the correct order or the user's touch manipulations won't diff --git a/libraries/ui_compose/build.gradle b/libraries/ui_compose/build.gradle index db04d25018d..c68656bf317 100644 --- a/libraries/ui_compose/build.gradle +++ b/libraries/ui_compose/build.gradle @@ -49,6 +49,7 @@ android { dependencies { api project(modulePrefix + 'lib-common') api project(modulePrefix + 'lib-common-ktx') + api project(modulePrefix + 'lib-exoplayer') def composeBom = platform('androidx.compose:compose-bom:2024.12.01') api composeBom 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 b58769475bc..cab045a3f8f 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 @@ -31,14 +31,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** * Provides a dedicated drawing [android.view.Surface] for media playbacks using a [Player]. * - * The player's video output is displayed with either a [android.view.SurfaceView] or a - * [android.view.TextureView]. + * The player's video output is displayed with either a [android.view.SurfaceView], a + * [android.view.TextureView], or a + * [androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView]. * * [Player] takes care of attaching the rendered output to the [android.view.Surface] and clearing * it, when it is destroyed. @@ -71,6 +73,16 @@ fun PlayerSurface( setVideoView = Player::setVideoTextureView, clearVideoView = Player::clearVideoTextureView, ) + SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW -> + PlayerSurfaceInternal( + player, + modifier, + createView = ::SphericalGLSurfaceView, + setVideoView = { setVideoSurfaceView(it) }, + clearVideoView = { setVideoSurfaceView(null) }, + onReset = SphericalGLSurfaceView::onPause, + onUpdate = SphericalGLSurfaceView::onResume, + ) else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType") } } @@ -82,14 +94,19 @@ private fun PlayerSurfaceInternal( createView: (Context) -> T, setVideoView: Player.(T) -> Unit, clearVideoView: Player.(T) -> Unit, + onReset: (T) -> Unit = {}, + onUpdate: (T) -> Unit = {}, ) { var view by remember { mutableStateOf(null) } AndroidView( modifier = modifier, factory = { createView(it) }, - onReset = {}, - update = { view = it }, + onReset = onReset, + update = { + view = it + onUpdate(it) + }, ) view?.let { view -> @@ -130,16 +147,18 @@ private var View.attachedPlayer: Player? } /** - * The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or - * [SURFACE_TYPE_TEXTURE_VIEW]. + * The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW], + * [SURFACE_TYPE_TEXTURE_VIEW] or [SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW]. */ @UnstableApi @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER) -@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW) +@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW, SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW) annotation class SurfaceType /** Surface type to create [android.view.SurfaceView]. */ @UnstableApi const val SURFACE_TYPE_SURFACE_VIEW = 1 /** Surface type to create [android.view.TextureView]. */ @UnstableApi const val SURFACE_TYPE_TEXTURE_VIEW = 2 +/** Surface type to create [androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView]. */ +@UnstableApi const val SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3 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 index 6ba82aab72a..b1a682dfe91 100644 --- 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.ForwardingPlayer import androidx.media3.common.Player import androidx.media3.common.SimpleBasePlayer +import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView import androidx.media3.ui.compose.utils.TestPlayer import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -78,6 +79,18 @@ class PlayerSurfaceTest { assertThat(player.videoOutput).isInstanceOf(TextureView::class.java) } + @Test + fun playerSurface_withSphericalGlSurfaceViewType_setsSphericalGlSurfaceViewOnPlayer() { + val player = TestPlayer() + + composeTestRule.setContent { + PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW) + } + composeTestRule.waitForIdle() + + assertThat(player.videoOutput).isInstanceOf(SphericalGLSurfaceView::class.java) + } + @Test fun playerSurface_withoutSupportedCommand_doesNotSetSurfaceOnPlayer() { val player = TestPlayer() From daa4ae578e77efeabe32a86e197b63706e07bf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 6 Aug 2025 10:07:05 +0200 Subject: [PATCH 2/2] Remove dependency on `lib-exoplayer` from `lib-ui-compose` --- libraries/ui_compose/build.gradle | 2 +- libraries/ui_compose/proguard-rules.txt | 7 +++++ .../media3/ui/compose/PlayerSurface.kt | 28 ++++++++++++++----- 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 libraries/ui_compose/proguard-rules.txt diff --git a/libraries/ui_compose/build.gradle b/libraries/ui_compose/build.gradle index c68656bf317..d61ab137053 100644 --- a/libraries/ui_compose/build.gradle +++ b/libraries/ui_compose/build.gradle @@ -49,7 +49,6 @@ android { dependencies { api project(modulePrefix + 'lib-common') api project(modulePrefix + 'lib-common-ktx') - api project(modulePrefix + 'lib-exoplayer') def composeBom = platform('androidx.compose:compose-bom:2024.12.01') api composeBom @@ -60,6 +59,7 @@ dependencies { testImplementation 'androidx.compose.ui:ui-test-android:1.8.2' testImplementation 'androidx.compose.ui:ui-test' testImplementation 'androidx.compose.ui:ui-test-junit4' + testImplementation project(modulePrefix + 'lib-exoplayer') testImplementation project(modulePrefix + 'test-utils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/libraries/ui_compose/proguard-rules.txt b/libraries/ui_compose/proguard-rules.txt new file mode 100644 index 00000000000..c5d499780b4 --- /dev/null +++ b/libraries/ui_compose/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the UI Compose module. + +# Constructor method and classes accessed via reflection in PlayerSurface +-dontnote androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView +-keepclassmembers class androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView { + (android.content.Context); +} 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 cab045a3f8f..f3b5a4e21df 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 @@ -17,6 +17,7 @@ package androidx.media3.ui.compose import android.content.Context +import android.opengl.GLSurfaceView import android.view.SurfaceView import android.view.TextureView import android.view.View @@ -31,7 +32,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,8 +39,7 @@ import kotlinx.coroutines.withContext * Provides a dedicated drawing [android.view.Surface] for media playbacks using a [Player]. * * The player's video output is displayed with either a [android.view.SurfaceView], a - * [android.view.TextureView], or a - * [androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView]. + * [android.view.TextureView], or a `SphericalGLSurfaceView`. * * [Player] takes care of attaching the rendered output to the [android.view.Surface] and clearing * it, when it is destroyed. @@ -77,11 +76,26 @@ fun PlayerSurface( PlayerSurfaceInternal( player, modifier, - createView = ::SphericalGLSurfaceView, + createView = { + try { + // LINT.IfChange + val surfaceViewClassName = + "androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView" + val surfaceViewClass = Class.forName(surfaceViewClassName) + + surfaceViewClass.getConstructor(Context::class.java).newInstance(it) as GLSurfaceView + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + } catch (exception: ClassNotFoundException) { + throw IllegalStateException( + "SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW requires an ExoPlayer dependency", + exception + ) + } + }, setVideoView = { setVideoSurfaceView(it) }, clearVideoView = { setVideoSurfaceView(null) }, - onReset = SphericalGLSurfaceView::onPause, - onUpdate = SphericalGLSurfaceView::onResume, + onReset = GLSurfaceView::onPause, + onUpdate = GLSurfaceView::onResume, ) else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType") } @@ -160,5 +174,5 @@ annotation class SurfaceType @UnstableApi const val SURFACE_TYPE_SURFACE_VIEW = 1 /** Surface type to create [android.view.TextureView]. */ @UnstableApi const val SURFACE_TYPE_TEXTURE_VIEW = 2 -/** Surface type to create [androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView]. */ +/** Surface type to create `SphericalGLSurfaceView`. */ @UnstableApi const val SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3