Skip to content

Commit

Permalink
Add surface type to PlayerSurface (#742)
Browse files Browse the repository at this point in the history
Co-authored-by: Gaëtan Muller <[email protected]>
  • Loading branch information
StaehliJ and MGaetan89 authored Oct 9, 2024
1 parent eef59f2 commit 6967415
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import ch.srgssr.pillarbox.demo.ui.theme.paddings
import ch.srgssr.pillarbox.ui.ScaleMode
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
import ch.srgssr.pillarbox.ui.widget.player.SurfaceType

/**
* Optimized story trying to reproduce story-like TikTok or Instagram.
Expand Down Expand Up @@ -78,6 +79,9 @@ fun OptimizedStory(storyViewModel: StoryViewModel = viewModel()) {
PlayerSurface(
modifier = Modifier.fillMaxHeight(),
scaleMode = ScaleMode.Crop,
// Using Texture instead of Surface because on Android API 34 animations are not working well due to the hack
// See PlayerSurfaceView in AndroidPlayerSurfaceView
surfaceType = SurfaceType.Texture,
player = player,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.media3.common.Player
import ch.srgssr.pillarbox.core.business.SRGMediaItemBuilder
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.ui.widget.player.SphericalSurface
import ch.srgssr.pillarbox.ui.ScaleMode
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
import ch.srgssr.pillarbox.ui.widget.player.SurfaceType

/**
* Showcase how to display a spherical surface to play 360° video.
Expand Down Expand Up @@ -43,5 +45,10 @@ fun SphericalSurfaceShowcase() {
}
}

SphericalSurface(player = player, modifier = Modifier.fillMaxSize())
PlayerSurface(
player = player,
modifier = Modifier.fillMaxSize(),
surfaceType = SurfaceType.Spherical,
scaleMode = ScaleMode.Fill,
)
}
28 changes: 21 additions & 7 deletions pillarbox-ui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ More information can be found on the [top level README](../docs/README.md)

```kotlin
@Composable
fun SimplePlayer(player: Player){
fun SimplePlayer(player: Player) {
Box(modifier = Modifier) {
PlayerSurface(player = player)
}
Expand All @@ -34,7 +34,7 @@ fun SimplePlayer(player: Player){

### Create a simple player with controls and subtitles

In this example we are drawing controls and subtitles on top of the player surface. To add subtitles use `ExoPlayerSubtitleView` and for controls
In this example, we are drawing controls and subtitles on top of the player surface. To add subtitles use `ExoPlayerSubtitleView` and for controls
you can use the Exoplayer version, `ExoPlayerControlView`.

```kotlin
Expand All @@ -52,7 +52,8 @@ fun MyPlayer(player: Player) {
modifier = Modifier,
player = player,
scaleMode = ScaleMode.Fit,
defaultAspectRatio = defaultAspectRatio
defaultAspectRatio = defaultAspectRatio,
surfaceType = SurfaceType.Surface, // By default
)
ExoPlayerControlView(modifier = Modifier.matchParentSize(), player = player)
ExoPlayerSubtitleView(modifier = Modifier.matchParentSize(), player = player)
Expand All @@ -68,6 +69,19 @@ In this example we use `ScaleMode.Fit` to fit the content to the parent containe
- `ScaleMode.Fill` : Fill player content to the parent container.
- `ScaleMode.Crop` : Crop player content inside the parent container and keep aspect ratio. Content outside the parent container will be clipped.

### Surface types

`PlayerSurface` allows choosing between multiple types of surface:

- `SurfaceType.Surface` (default): the player is linked to a `SurfaceView`. This option is the most optimized version, and supports playing any
content including DRM protected content.
- `SurfaceType.Texture`: the player is linked to a `TextureView`. This option may be interesting when dealing with animation, and the `Surface`
option doesn't work as expected.
- `SurfaceType.Spherical`: the player is linked to a `SphericalGLSurfaceView`. This surface type is suited when playing 360° video content.

> [!NOTE]
> The last two surface types are not suited when playing DRM protected content.
### Listen to player states

To listen to player states _Pillarbox_ provides some extensions `PlayerCallbackFlow.kt` and some Compose extensions `ComposablePlayer.kt`.
Expand All @@ -93,13 +107,13 @@ fun MyPlayerView(player: Player) {
// Displays current position periodically
val currentPosition = player.currentPositionAsState()
Text(text = "Position = $currentPosition ms", modifier = Modifier.align(Alignment.TopStart))

val duration = player.durationAsState()
Text(text = "Duration = $duration ms", modifier = Modifier.align(Alignment.TopEnd))

val isPlaying = player.isPlayingAsState()
Button(modifier = Modifier = Modififer.align(Alignement.Center), onClick = { togglePlayingBack() }){
Text(text = if(isPlaying) "Pause" else "Play")
Button(modifier = Modififer.align(Alignement.Center), onClick = { togglePlayingBack() }) {
Text(text = if (isPlaying) "Pause" else "Play")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.ui.widget.player

import android.content.Context
import android.graphics.Canvas
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.SurfaceControl
import android.view.SurfaceView
import android.window.SurfaceSyncGroup
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.Player

/**
* Render the [player] content on a [SurfaceView].
*
* @param player The player to render on the SurfaceView.
* @param modifier The modifier to be applied to the layout.
*/
@Composable
internal fun AndroidPlayerSurfaceView(player: Player, modifier: Modifier = Modifier) {
AndroidView(
/*
* On some devices (Pixel 2 XL Android 11),
* the "black" background of the SurfaceView shows outside its bound.
*/
modifier = modifier.clipToBounds(),
factory = { context ->
PlayerSurfaceView(context)
},
update = { view ->
view.player = player
},
onRelease = { view ->
view.player = null
},
onReset = { view ->
// onReset is called before `update` when the composable is reused with a different context.
view.player = null
}
)
}

/**
* Player surface view
*/
private class PlayerSurfaceView(context: Context) : SurfaceView(context) {
private val playerListener = PlayerListener()
private val surfaceSyncGroupV34 = when {
isInEditMode -> null
needSurfaceSyncWorkaround() -> SurfaceSyncGroupCompatV34()
else -> null
}

/**
* Player if null is passed just clear surface
*/
var player: Player? = null
set(value) {
if (field != value) {
field?.clearVideoSurfaceView(this)
field?.removeListener(playerListener)
value?.setVideoSurfaceView(this)
value?.addListener(playerListener)
}
field = value
}

override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)

if (needSurfaceSyncWorkaround()) {
surfaceSyncGroupV34?.maybeMarkSyncReadyAndClear()
}
}

// Workaround for a surface sync issue on API 34: https://github.com/androidx/media/issues/1237
// Imported from https://github.com/androidx/media/commit/30cb76269a67e09f6e1662ea9ead6aac70667028
private fun needSurfaceSyncWorkaround(): Boolean {
return Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE
}

private inner class PlayerListener : Player.Listener {
private val mainLooperHandler = Handler(Looper.getMainLooper())

override fun onSurfaceSizeChanged(width: Int, height: Int) {
if (needSurfaceSyncWorkaround()) {
surfaceSyncGroupV34?.postRegister(mainLooperHandler, this@PlayerSurfaceView)
}
}
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private class SurfaceSyncGroupCompatV34 {
private var surfaceSyncGroup: SurfaceSyncGroup? = null

fun postRegister(
mainLooperHandler: Handler,
surfaceView: SurfaceView,
) {
mainLooperHandler.post {
// The SurfaceView isn't attached to a window, so don't apply the workaround.
val rootSurfaceControl = surfaceView.getRootSurfaceControl() ?: return@post

surfaceSyncGroup = SurfaceSyncGroup("exo-sync-b-334901521")
surfaceSyncGroup?.add(rootSurfaceControl) {}
surfaceView.invalidate()
rootSurfaceControl.applyTransactionOnDraw(SurfaceControl.Transaction())
}
}

fun maybeMarkSyncReadyAndClear() {
surfaceSyncGroup?.markSyncReady()
surfaceSyncGroup = null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.ui.widget.player

import android.content.Context
import android.view.TextureView
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.Player

/**
* Render the [player] content on a [TextureView]. Does not work with DRM content!
*
* @param player The player to render on the TextureView.
* @param modifier The modifier to be applied to the layout.
*/
@Composable
internal fun AndroidPlayerTextureView(player: Player, modifier: Modifier = Modifier) {
AndroidView(
/*
* On some devices (Pixel 2 XL Android 11),
* the "black" background of the SurfaceView shows outside its bound.
*/
modifier = modifier.clipToBounds(),
factory = { context ->
PlayerTextureView(context)
},
update = { view ->
view.player = player
},
onRelease = { view ->
view.player = null
},
onReset = { view ->
// onReset is called before `update` when the composable is reused with a different context.
view.player = null
}
)
}

private class PlayerTextureView(context: Context) : TextureView(context) {
/**
* Player if null is passed just clear surface
*/
var player: Player? = null
set(value) {
if (field != value) {
field?.clearVideoTextureView(this)
value?.setVideoTextureView(this)
}
field = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView
* @param modifier The modifier to be applied to the layout.
*/
@Composable
fun SphericalSurface(player: Player, modifier: Modifier = Modifier) {
internal fun AndroidSphericalSurfaceView(player: Player, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context ->
Expand Down
Loading

0 comments on commit 6967415

Please sign in to comment.