Skip to content

Commit

Permalink
Improve the multi player showcase (#478)
Browse files Browse the repository at this point in the history
Co-authored-by: Joaquim Stähli <[email protected]>
  • Loading branch information
MGaetan89 and StaehliJ authored Apr 5, 2024
1 parent 99e2300 commit 86f7dce
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 63 deletions.
1 change: 1 addition & 0 deletions pillarbox-demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.media)
implementation(libs.androidx.media3.common)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.session)
Expand Down
8 changes: 5 additions & 3 deletions pillarbox-demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application
android:name=".DemoApplication"
android:allowBackup="true"
Expand All @@ -26,8 +28,8 @@

<activity
android:name=".MainActivity"
android:theme="@style/Theme.PillarboxDemo"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.PillarboxDemo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ import androidx.compose.ui.graphics.Color
import androidx.media3.common.Player
import ch.srgssr.pillarbox.ui.extension.currentMediaMetadataAsState

private val controlsBackgroundColor = Color.Black.copy(0.5f)

/**
* Player controls
*
* @param player The [Player] to interact with.
* @param modifier The modifier to be applied to the layout.
* @param backgroundColor The background color to apply behind the controls.
* @param interactionSource The interaction source of the slider.
* @param content The content to display under the slider.
* @receiver
Expand All @@ -35,14 +34,13 @@ private val controlsBackgroundColor = Color.Black.copy(0.5f)
fun PlayerControls(
player: Player,
modifier: Modifier = Modifier,
backgroundColor: Color = Color.Black.copy(0.5f),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable ColumnScope.() -> Unit,
) {
val mediaMetadata by player.currentMediaMetadataAsState()
Box(
modifier = modifier.then(
Modifier.background(color = controlsBackgroundColor)
),
modifier = modifier.background(color = backgroundColor),
contentAlignment = Alignment.Center
) {
Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,88 +5,66 @@
package ch.srgssr.pillarbox.demo.ui.showcases.misc

import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.Player
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.ui.player.PlayerView
import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerControls
import ch.srgssr.pillarbox.demo.ui.theme.paddings
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface

/**
* Demo of 2 player swapping view
* Demo displaying two players, that can be swapped.
* At any given moment, there's always only one player with sound active.
*/
@Composable
fun MultiPlayerShowcase() {
var swapLeftRight by remember {
mutableStateOf(false)
}
val context = LocalContext.current
val playerOne = remember {
PlayerModule.provideDefaultPlayer(context).apply {
repeatMode = Player.REPEAT_MODE_ONE
setMediaItem(DemoItem.LiveVideo.toMediaItem())
prepare()
}
}
val playerTwo = remember {
PlayerModule.provideDefaultPlayer(context).apply {
repeatMode = Player.REPEAT_MODE_ONE
setMediaItem(DemoItem.DvrVideo.toMediaItem())
prepare()
}
}
DisposableEffect(Unit) {
onDispose {
playerOne.release()
playerTwo.release()
}
}
LifecycleResumeEffect(Unit) {
playerOne.play()
playerTwo.play()
onPauseOrDispose {
playerOne.pause()
playerTwo.pause()
}
}
val multiPlayerViewModel = viewModel<MultiPlayerViewModel>()
val activePlayer by multiPlayerViewModel.activePlayer.collectAsState()
val playerOne by multiPlayerViewModel.playerOne.collectAsState()
val playerTwo by multiPlayerViewModel.playerTwo.collectAsState()

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { swapLeftRight = !swapLeftRight }) {
Button(onClick = multiPlayerViewModel::swapPlayers) {
Text(text = "Swap players")
}

val players = remember {
movableContentOf {
PlayerView(
modifier = Modifier
.weight(1.0f)
.padding(MaterialTheme.paddings.mini),
player = if (swapLeftRight) playerTwo else playerOne,
ActivablePlayer(
player = playerOne,
isActive = activePlayer == playerOne,
modifier = Modifier.weight(1f),
onClick = { multiPlayerViewModel.setActivePlayer(playerOne) },
)
PlayerView(
modifier = Modifier
.weight(1.0f)
.padding(MaterialTheme.paddings.mini),
player = if (swapLeftRight) playerOne else playerTwo,

ActivablePlayer(
player = playerTwo,
isActive = activePlayer == playerTwo,
modifier = Modifier.weight(1f),
onClick = { multiPlayerViewModel.setActivePlayer(playerTwo) },
)
}
}

if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Row(modifier = Modifier.fillMaxWidth()) {
players()
Expand All @@ -98,3 +76,37 @@ fun MultiPlayerShowcase() {
}
}
}

@Composable
private fun ActivablePlayer(
player: Player,
isActive: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
PlayerSurface(
modifier = modifier
.padding(MaterialTheme.paddings.mini)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
enabled = !isActive,
onClick = onClick,
),
player = player,
) {
val inactivePlayerOverlay = Modifier.drawWithContent {
drawContent()
drawRect(Color.LightGray.copy(alpha = 0.7f))
}

PlayerControls(
player = player,
modifier = Modifier
.fillMaxSize()
.then(if (isActive) Modifier else inactivePlayerOverlay),
backgroundColor = Color.Unspecified,
content = {},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.ui.showcases.misc

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.session.MediaSession
import androidx.media3.ui.PlayerNotificationManager
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.extension.disableAudioTrack
import ch.srgssr.pillarbox.player.notification.PillarboxMediaDescriptionAdapter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update

/**
* The [ViewModel][androidx.lifecycle.ViewModel] for the [MultiPlayerShowcase].
*
* @param application The running [Application].
*/
class MultiPlayerViewModel(application: Application) : AndroidViewModel(application) {
private val notificationManager = PlayerNotificationManager.Builder(application, NOTIFICATION_ID, CHANNEL_ID)
.setChannelNameResourceId(androidx.media3.session.R.string.default_notification_channel_name)
.setMediaDescriptionAdapter(PillarboxMediaDescriptionAdapter(null, application))
.build()
private val mediaSession: MediaSession

private val _playerOne = PlayerModule.provideDefaultPlayer(application).apply {
repeatMode = Player.REPEAT_MODE_ONE
setMediaItem(DemoItem.LiveVideo.toMediaItem())
prepare()
play()
}
private val _playerTwo = PlayerModule.provideDefaultPlayer(application).apply {
repeatMode = Player.REPEAT_MODE_ONE
setMediaItem(DemoItem.DvrVideo.toMediaItem())
prepare()
play()
}

private val _activePlayer = MutableStateFlow(_playerOne)
private val swapPlayers = MutableStateFlow(false)

/**
* The currently active player.
*/
val activePlayer: StateFlow<PillarboxPlayer> = _activePlayer

/**
* The first player to display.
*/
val playerOne = swapPlayers.map { swapPlayers ->
if (swapPlayers) _playerTwo else _playerOne
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), _playerOne)

/**
* The second play to display.
*/
val playerTwo = swapPlayers.map { swapPlayers ->
if (swapPlayers) _playerOne else _playerTwo
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), _playerTwo)

init {
mediaSession = MediaSession.Builder(application, _playerTwo)
.setId("MultiPlayerSession")
.build()
notificationManager.setMediaSessionToken(mediaSession.sessionCompatToken)
setActivePlayer(_playerOne)
}

/**
* Set the currently active player.
*
* @param activePlayer The new active player.
*/
fun setActivePlayer(activePlayer: PillarboxPlayer) {
val oldActivePlayer = mediaSession.player as PillarboxPlayer
_activePlayer.update { activePlayer }
mediaSession.player = activePlayer
notificationManager.setPlayer(activePlayer)

oldActivePlayer.disableAudioTrack()
oldActivePlayer.trackSelectionParameters = oldActivePlayer.trackSelectionParameters.buildUpon().setTrackTypeDisabled(
C.TRACK_TYPE_AUDIO,
true
).build()
oldActivePlayer.trackingEnabled = false
oldActivePlayer.setHandleAudioFocus(false)
oldActivePlayer.setHandleAudioBecomingNoisy(false)

activePlayer.trackSelectionParameters = activePlayer.trackSelectionParameters.buildUpon().setTrackTypeDisabled(
C.TRACK_TYPE_AUDIO,
false
).build()
activePlayer.trackingEnabled = true
activePlayer.setHandleAudioFocus(true)
activePlayer.setHandleAudioBecomingNoisy(true)
}

/**
* Swap the two players.
*/
fun swapPlayers() {
swapPlayers.update { !it }
}

override fun onCleared() {
notificationManager.setPlayer(null)
mediaSession.release()

_playerOne.release()
_playerTwo.release()
}

private companion object {
private const val NOTIFICATION_ID = 42
private const val CHANNEL_ID = "DemoChannel"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.session.MediaSession
import androidx.media3.ui.PlayerNotificationManager
import ch.srgssr.pillarbox.core.business.SRGMediaItemBuilder
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
Expand Down Expand Up @@ -38,6 +39,7 @@ class UpdatableMediaItemViewModel(application: Application) : AndroidViewModel(a
private val timer: Timer
private val baseTitle = "Update title"
private var counter = 0
private val mediaSession = MediaSession.Builder(application, player).build()

init {
player.prepare()
Expand All @@ -48,6 +50,7 @@ class UpdatableMediaItemViewModel(application: Application) : AndroidViewModel(a
.setMediaDescriptionAdapter(PillarboxMediaDescriptionAdapter(context = application, pendingIntent = null))
.build()
notificationManager.setPlayer(player)
notificationManager.setMediaSessionToken(mediaSession.sessionCompatToken)

timer = timer(name = "update-item", period = 3.seconds.inWholeMilliseconds) {
viewModelScope.launch(Dispatchers.Main) {
Expand Down Expand Up @@ -91,6 +94,7 @@ class UpdatableMediaItemViewModel(application: Application) : AndroidViewModel(a
super.onCleared()
timer.cancel()
notificationManager.setPlayer(null)
mediaSession.release()
player.release()
}

Expand Down
Loading

0 comments on commit 86f7dce

Please sign in to comment.