Skip to content

Commit 5a66407

Browse files
committed
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.
1 parent 9c5e046 commit 5a66407

File tree

6 files changed

+66
-16
lines changed

6 files changed

+66
-16
lines changed

demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding
3030
import androidx.compose.material3.Button
3131
import androidx.compose.material3.Text
3232
import androidx.compose.runtime.Composable
33+
import androidx.compose.runtime.LaunchedEffect
3334
import androidx.compose.runtime.getValue
3435
import androidx.compose.runtime.mutableIntStateOf
3536
import androidx.compose.runtime.mutableStateOf
@@ -44,6 +45,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
4445
import androidx.lifecycle.compose.LifecycleStartEffect
4546
import androidx.media3.common.MediaItem
4647
import androidx.media3.common.Player
48+
import androidx.media3.common.listen
4749
import androidx.media3.demo.compose.buttons.ExtraControls
4850
import androidx.media3.demo.compose.buttons.MinimalControls
4951
import androidx.media3.demo.compose.data.videos
@@ -102,27 +104,36 @@ fun ComposeDemoApp(modifier: Modifier = Modifier) {
102104

103105
private fun initializePlayer(context: Context): Player =
104106
ExoPlayer.Builder(context).build().apply {
105-
setMediaItems(videos.map(MediaItem::fromUri))
107+
setMediaItems(videos.keys.map(MediaItem::fromUri))
106108
prepare()
107109
}
108110

109111
@Composable
110112
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
111113
var showControls by remember { mutableStateOf(true) }
112114
var currentContentScaleIndex by remember { mutableIntStateOf(0) }
115+
var surfaceType by remember { mutableIntStateOf(SURFACE_TYPE_SURFACE_VIEW) }
113116
val contentScale = CONTENT_SCALES[currentContentScaleIndex].second
114117

115118
val presentationState = rememberPresentationState(player)
116119
val scaledModifier = Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
117120

121+
LaunchedEffect(player) {
122+
player.listen {
123+
currentMediaItem?.localConfiguration?.let {
124+
surfaceType = videos.getValue(it.uri.toString())
125+
}
126+
}
127+
}
128+
118129
// Only use MediaPlayerScreen's modifier once for the top level Composable
119130
Box(modifier) {
120131
// Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
121132
// the process. If this composable is guarded by some condition, it might never become visible
122133
// because the Player will not emit the relevant event, e.g. the first frame being ready.
123134
PlayerSurface(
124135
player = player,
125-
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
136+
surfaceType = surfaceType,
126137
modifier = scaledModifier.noRippleClickable { showControls = !showControls },
127138
)
128139

demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
*/
1616
package androidx.media3.demo.compose.data
1717

18+
import androidx.media3.ui.compose.SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW
19+
import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW
20+
1821
val videos =
19-
listOf(
20-
"https://html5demos.com/assets/dizzy.mp4",
21-
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4",
22-
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
23-
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4",
22+
mapOf(
23+
"https://html5demos.com/assets/dizzy.mp4" to SURFACE_TYPE_SURFACE_VIEW,
24+
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4" to SURFACE_TYPE_SURFACE_VIEW,
25+
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" to SURFACE_TYPE_SURFACE_VIEW,
26+
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4" to SURFACE_TYPE_SURFACE_VIEW,
27+
// https://bitmovin.com/demos/vr-360/
28+
"https://cdn.bitmovin.com/content/assets/playhouse-vr/progressive.mp4" to SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW,
2429
)

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/spherical/SphericalGLSurfaceView.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@
4747
/**
4848
* Renders a GL scene in a non-VR Activity that is affected by phone orientation and touch input.
4949
*
50-
* <p>The two input components are the TYPE_GAME_ROTATION_VECTOR Sensor and a TouchListener. The GL
51-
* renderer combines these two inputs to render a scene with the appropriate camera orientation.
50+
* <p>The two input components are the {@link Sensor#TYPE_GAME_ROTATION_VECTOR
51+
* TYPE_GAME_ROTATION_VECTOR} Sensor and a TouchListener. The GL renderer combines these two inputs
52+
* to render a scene with the appropriate camera orientation.
5253
*
5354
* <p>The primary complexity in this class is related to the various rotations. It is important to
5455
* apply the touch and sensor rotations in the correct order or the user's touch manipulations won't

libraries/ui_compose/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ android {
4949
dependencies {
5050
api project(modulePrefix + 'lib-common')
5151
api project(modulePrefix + 'lib-common-ktx')
52+
api project(modulePrefix + 'lib-exoplayer')
5253

5354
def composeBom = platform('androidx.compose:compose-bom:2024.12.01')
5455
api composeBom

libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ import androidx.compose.ui.Modifier
3131
import androidx.compose.ui.viewinterop.AndroidView
3232
import androidx.media3.common.Player
3333
import androidx.media3.common.util.UnstableApi
34+
import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView
3435
import kotlinx.coroutines.Dispatchers
3536
import kotlinx.coroutines.withContext
3637

3738
/**
3839
* Provides a dedicated drawing [android.view.Surface] for media playbacks using a [Player].
3940
*
40-
* The player's video output is displayed with either a [android.view.SurfaceView] or a
41-
* [android.view.TextureView].
41+
* The player's video output is displayed with either a [android.view.SurfaceView], a
42+
* [android.view.TextureView], or a
43+
* [androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView].
4244
*
4345
* [Player] takes care of attaching the rendered output to the [android.view.Surface] and clearing
4446
* it, when it is destroyed.
@@ -71,6 +73,16 @@ fun PlayerSurface(
7173
setVideoView = Player::setVideoTextureView,
7274
clearVideoView = Player::clearVideoTextureView,
7375
)
76+
SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW ->
77+
PlayerSurfaceInternal(
78+
player,
79+
modifier,
80+
createView = ::SphericalGLSurfaceView,
81+
setVideoView = { setVideoSurfaceView(it) },
82+
clearVideoView = { setVideoSurfaceView(null) },
83+
onReset = SphericalGLSurfaceView::onPause,
84+
onUpdate = SphericalGLSurfaceView::onResume,
85+
)
7486
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
7587
}
7688
}
@@ -82,14 +94,19 @@ private fun <T : View> PlayerSurfaceInternal(
8294
createView: (Context) -> T,
8395
setVideoView: Player.(T) -> Unit,
8496
clearVideoView: Player.(T) -> Unit,
97+
onReset: (T) -> Unit = {},
98+
onUpdate: (T) -> Unit = {},
8599
) {
86100
var view by remember { mutableStateOf<T?>(null) }
87101

88102
AndroidView(
89103
modifier = modifier,
90104
factory = { createView(it) },
91-
onReset = {},
92-
update = { view = it },
105+
onReset = onReset,
106+
update = {
107+
view = it
108+
onUpdate(it)
109+
},
93110
)
94111

95112
view?.let { view ->
@@ -130,16 +147,18 @@ private var View.attachedPlayer: Player?
130147
}
131148

132149
/**
133-
* The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or
134-
* [SURFACE_TYPE_TEXTURE_VIEW].
150+
* The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW],
151+
* [SURFACE_TYPE_TEXTURE_VIEW] or [SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW].
135152
*/
136153
@UnstableApi
137154
@Retention(AnnotationRetention.SOURCE)
138155
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER)
139-
@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW)
156+
@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW, SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW)
140157
annotation class SurfaceType
141158

142159
/** Surface type to create [android.view.SurfaceView]. */
143160
@UnstableApi const val SURFACE_TYPE_SURFACE_VIEW = 1
144161
/** Surface type to create [android.view.TextureView]. */
145162
@UnstableApi const val SURFACE_TYPE_TEXTURE_VIEW = 2
163+
/** Surface type to create [androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView]. */
164+
@UnstableApi const val SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3

libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
3535
import androidx.media3.common.ForwardingPlayer
3636
import androidx.media3.common.Player
3737
import androidx.media3.common.SimpleBasePlayer
38+
import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView
3839
import androidx.media3.ui.compose.utils.TestPlayer
3940
import androidx.test.ext.junit.runners.AndroidJUnit4
4041
import com.google.common.truth.Truth.assertThat
@@ -78,6 +79,18 @@ class PlayerSurfaceTest {
7879
assertThat(player.videoOutput).isInstanceOf(TextureView::class.java)
7980
}
8081

82+
@Test
83+
fun playerSurface_withSphericalGlSurfaceViewType_setsSphericalGlSurfaceViewOnPlayer() {
84+
val player = TestPlayer()
85+
86+
composeTestRule.setContent {
87+
PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW)
88+
}
89+
composeTestRule.waitForIdle()
90+
91+
assertThat(player.videoOutput).isInstanceOf(SphericalGLSurfaceView::class.java)
92+
}
93+
8194
@Test
8295
fun playerSurface_withoutSupportedCommand_doesNotSetSurfaceOnPlayer() {
8396
val player = TestPlayer()

0 commit comments

Comments
 (0)