Skip to content

Add SphericalGLSurfaceView support to PlayerSurface #2619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -102,27 +104,36 @@ 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()
}

@Composable
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBD: this will be triggered on all the events, even when they are not relevant to localConfiguration. We are considering adding a player.listen(Player.Events, Player.()->Unit) overload that will let you specify the relevant events for the actionable-trailing-lambda. I'll update this comment once I push that.

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
// the process. If this composable is guarded by some condition, it might never become visible
// 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 },
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: leftover uri

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this comment to explain where the video comes from.
Should I remove it or clarify it (for example 360 video coming from https://bitmovin.com/demos/vr-360/)?

"https://cdn.bitmovin.com/content/assets/playhouse-vr/progressive.mp4" to SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW,
)
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
/**
* Renders a GL scene in a non-VR Activity that is affected by phone orientation and touch input.
*
* <p>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.
* <p>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.
*
* <p>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
Expand Down
1 change: 1 addition & 0 deletions libraries/ui_compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,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
}
Expand Down
7 changes: 7 additions & 0 deletions libraries/ui_compose/proguard-rules.txt
Original file line number Diff line number Diff line change
@@ -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 {
<init>(android.content.Context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,8 +38,8 @@ 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 `SphericalGLSurfaceView`.
*
* [Player] takes care of attaching the rendered output to the [android.view.Surface] and clearing
* it, when it is destroyed.
Expand Down Expand Up @@ -71,6 +72,31 @@ fun PlayerSurface(
setVideoView = Player::setVideoTextureView,
clearVideoView = Player::clearVideoTextureView,
)
SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW ->
PlayerSurfaceInternal(
player,
modifier,
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 = GLSurfaceView::onPause,
onUpdate = GLSurfaceView::onResume,
)
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
}
}
Expand All @@ -82,14 +108,19 @@ private fun <T : View> PlayerSurfaceInternal(
createView: (Context) -> T,
setVideoView: Player.(T) -> Unit,
clearVideoView: Player.(T) -> Unit,
onReset: (T) -> Unit = {},
onUpdate: (T) -> Unit = {},
) {
var view by remember { mutableStateOf<T?>(null) }

AndroidView(
modifier = modifier,
factory = { createView(it) },
onReset = {},
update = { view = it },
onReset = onReset,
update = {
view = it
onUpdate(it)
},
)

view?.let { view ->
Expand Down Expand Up @@ -130,16 +161,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 `SphericalGLSurfaceView`. */
@UnstableApi const val SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down