Skip to content

Commit d8c1a14

Browse files
authored
1224 audio video switch example (#1228)
1 parent 0986d71 commit d8c1a14

6 files changed

Lines changed: 274 additions & 0 deletions

File tree

pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,7 @@ sealed interface NavigationRoutes {
8989

9090
@Serializable
9191
data object CastShowcase : NavigationRoutes
92+
93+
@Serializable
94+
data object AudioVideoSwitchShowcase : NavigationRoutes
9295
}

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ fun ShowcasesHome(navController: NavController) {
120120
stringResource(R.string.smooth_seeking_example) to NavigationRoutes.SmoothSeeking,
121121
stringResource(R.string.video_360) to NavigationRoutes.Video360,
122122
stringResource(R.string.showcase_countdown) to NavigationRoutes.CountdownShowcase,
123+
stringResource(R.string.showcase_video_audio_switch) to NavigationRoutes.AudioVideoSwitchShowcase
123124
)
124125

125126
DemoListHeaderView(

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ch.srgssr.pillarbox.demo.ui.showcases.layouts.ChapterShowcase
1717
import ch.srgssr.pillarbox.demo.ui.showcases.layouts.SimpleLayoutShowcase
1818
import ch.srgssr.pillarbox.demo.ui.showcases.layouts.StoryLayoutShowcase
1919
import ch.srgssr.pillarbox.demo.ui.showcases.layouts.thumbnail.ThumbnailView
20+
import ch.srgssr.pillarbox.demo.ui.showcases.misc.AudioVideoSwitchShowcase
2021
import ch.srgssr.pillarbox.demo.ui.showcases.misc.ContentNotYetAvailable
2122
import ch.srgssr.pillarbox.demo.ui.showcases.misc.MultiPlayerShowcase
2223
import ch.srgssr.pillarbox.demo.ui.showcases.misc.ResizablePlayerShowcase
@@ -87,6 +88,10 @@ fun NavGraphBuilder.showcasesNavGraph(navController: NavController) {
8788
composable<NavigationRoutes.CastShowcase>(DemoPageView("GoogleCastSample", Levels)) {
8889
CastShowcase()
8990
}
91+
92+
composable<NavigationRoutes.AudioVideoSwitchShowcase>(DemoPageView("VideoAudioSwitchLiveSample", Levels)) {
93+
AudioVideoSwitchShowcase()
94+
}
9095
}
9196

9297
private val Levels = listOf("app", "pillarbox", "showcase")
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.demo.ui.showcases.misc
6+
7+
import android.app.Application
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.setValue
11+
import androidx.lifecycle.AndroidViewModel
12+
import androidx.media3.common.MediaItem
13+
import ch.srgssr.pillarbox.core.business.SRGMediaItem
14+
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
15+
import ch.srgssr.pillarbox.player.PillarboxPlayer
16+
17+
/**
18+
* The showcase viewmodel
19+
*/
20+
class AudioVideoLiveSwitchViewModel(application: Application) : AndroidViewModel(application) {
21+
/**
22+
* The player.
23+
*/
24+
val player: PillarboxPlayer = PlayerModule.provideDefaultPlayer(application)
25+
26+
/**
27+
* The current selected [ContentType].
28+
*/
29+
var currentContentType by mutableStateOf(ContentType.Video)
30+
private set
31+
32+
/**
33+
* The current [LiveContent] to play.
34+
*/
35+
var currentContent: LiveContent? by mutableStateOf(null)
36+
37+
init {
38+
load(contents.first())
39+
player.prepare()
40+
player.play()
41+
}
42+
43+
override fun onCleared() {
44+
player.release()
45+
}
46+
47+
/**
48+
* Load the given [content].
49+
*/
50+
fun load(content: LiveContent) {
51+
if (content != currentContent) {
52+
player.setMediaItem(content.toMediaItem(currentContentType))
53+
currentContent = content
54+
}
55+
}
56+
57+
/**
58+
* Toggle [currentContentType].
59+
*/
60+
fun toggleContentType() {
61+
currentContentType = when (currentContentType) {
62+
ContentType.Video -> ContentType.Audio
63+
ContentType.Audio -> ContentType.Video
64+
}
65+
currentContent?.let {
66+
val mediaItem = it.toMediaItem(currentContentType)
67+
val startPositionMs = getStartPosition()
68+
player.setMediaItem(mediaItem, startPositionMs)
69+
}
70+
}
71+
72+
private fun getStartPosition(): Long {
73+
return player.currentPosition
74+
}
75+
76+
companion object {
77+
/**
78+
* The list of the [LiveContent].
79+
*/
80+
val contents = listOf(
81+
LiveContent(
82+
label = "SRF1",
83+
audioUrn = "urn:srf:audio:69e8ac16-4327-4af4-b873-fd5cd6e895a7",
84+
videoUrn = "urn:srf:video:5b90d1fb-477b-4d98-86a6-82921a4bb0ea"
85+
),
86+
LiveContent(
87+
label = "SRF3",
88+
audioUrn = "urn:srf:audio:dd0fa1ba-4ff6-4e1a-ab74-d7e49057d96f",
89+
videoUrn = "urn:srf:video:972b2dbd-3958-43b7-8c15-e92f56c8d734"
90+
),
91+
LiveContent(
92+
label = "SRF Musikwelle",
93+
audioUrn = "urn:srf:audio:a9c5c070-8899-46c7-ac27-f04f1be902fd",
94+
videoUrn = "urn:srf:video:973440d3-60a5-4ddf-ae83-2c77815a32a1"
95+
),
96+
LiveContent(
97+
label = "SRF Virus",
98+
audioUrn = "urn:srf:audio:66815fe2-9008-4853-80a5-f9caaffdf3a9",
99+
videoUrn = "urn:srf:video:2a60b590-8a28-4540-bce8-fc4e52b3b5d8"
100+
),
101+
LiveContent(label = "RTS Couleur3", audioUrn = "urn:rts:audio:3262363", videoUrn = "urn:rts:video:8841634"),
102+
)
103+
}
104+
105+
/**
106+
* The content type
107+
*/
108+
@Suppress("UndocumentedPublicProperty")
109+
enum class ContentType {
110+
Video,
111+
Audio,
112+
}
113+
114+
/**
115+
* The live content
116+
* @property label The label to display.
117+
* @property audioUrn The urn to play when the content type is Audio.
118+
* @property videoUrn The urn to play when the content type is Video.
119+
*/
120+
data class LiveContent(val label: String, val audioUrn: String, val videoUrn: String) {
121+
122+
internal fun getUrnToPlay(contentType: ContentType): String = when (contentType) {
123+
ContentType.Video -> videoUrn
124+
ContentType.Audio -> audioUrn
125+
}
126+
127+
internal fun toMediaItem(contentType: ContentType): MediaItem = SRGMediaItem(urn = getUrnToPlay(contentType))
128+
}
129+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.demo.ui.showcases.misc
6+
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Row
10+
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.aspectRatio
12+
import androidx.compose.foundation.layout.fillMaxSize
13+
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.layout.size
16+
import androidx.compose.foundation.text.input.TextFieldLineLimits
17+
import androidx.compose.foundation.text.input.rememberTextFieldState
18+
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
19+
import androidx.compose.material.icons.Icons
20+
import androidx.compose.material.icons.filled.Audiotrack
21+
import androidx.compose.material.icons.filled.Videocam
22+
import androidx.compose.material3.DropdownMenuItem
23+
import androidx.compose.material3.ExperimentalMaterial3Api
24+
import androidx.compose.material3.ExposedDropdownMenuAnchorType
25+
import androidx.compose.material3.ExposedDropdownMenuBox
26+
import androidx.compose.material3.ExposedDropdownMenuDefaults
27+
import androidx.compose.material3.Icon
28+
import androidx.compose.material3.MaterialTheme
29+
import androidx.compose.material3.OutlinedIconToggleButton
30+
import androidx.compose.material3.Text
31+
import androidx.compose.material3.TextField
32+
import androidx.compose.runtime.Composable
33+
import androidx.compose.runtime.getValue
34+
import androidx.compose.runtime.mutableStateOf
35+
import androidx.compose.runtime.remember
36+
import androidx.compose.runtime.setValue
37+
import androidx.compose.ui.Alignment
38+
import androidx.compose.ui.Modifier
39+
import androidx.lifecycle.viewmodel.compose.viewModel
40+
import ch.srgssr.pillarbox.demo.ui.player.PlayerView
41+
import ch.srgssr.pillarbox.demo.ui.theme.paddings
42+
43+
/**
44+
* A showcase demonstrating how switching audio / video live.
45+
*/
46+
@Composable
47+
fun AudioVideoSwitchShowcase(audioVideoLiveSwitchViewModel: AudioVideoLiveSwitchViewModel = viewModel()) {
48+
val player = audioVideoLiveSwitchViewModel.player
49+
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.medium)) {
50+
PlayerView(
51+
player = player,
52+
modifier = Modifier
53+
.fillMaxWidth()
54+
.aspectRatio(16 / 9f)
55+
)
56+
Row(
57+
modifier = Modifier.fillMaxWidth().padding(horizontal = MaterialTheme.paddings.baseline),
58+
verticalAlignment = Alignment.CenterVertically,
59+
horizontalArrangement = Arrangement.End
60+
) {
61+
ContentTypeToggleButton(
62+
currentContentType = audioVideoLiveSwitchViewModel.currentContentType,
63+
toggle = audioVideoLiveSwitchViewModel::toggleContentType
64+
)
65+
Spacer(modifier = Modifier.size(MaterialTheme.paddings.baseline))
66+
ContentSelector(
67+
contents = AudioVideoLiveSwitchViewModel.contents,
68+
currentContent = audioVideoLiveSwitchViewModel.currentContent,
69+
loadContent = audioVideoLiveSwitchViewModel::load,
70+
)
71+
}
72+
}
73+
}
74+
75+
@OptIn(ExperimentalMaterial3Api::class)
76+
@Composable
77+
private fun ContentSelector(
78+
contents: List<AudioVideoLiveSwitchViewModel.LiveContent>,
79+
currentContent: AudioVideoLiveSwitchViewModel.LiveContent?,
80+
loadContent: (AudioVideoLiveSwitchViewModel.LiveContent) -> Unit,
81+
modifier: Modifier = Modifier
82+
) {
83+
val textFieldState = rememberTextFieldState(currentContent?.label ?: "Select")
84+
var expanded by remember { mutableStateOf(false) }
85+
ExposedDropdownMenuBox(modifier = modifier, expanded = expanded, onExpandedChange = {
86+
expanded = it
87+
}) {
88+
TextField(
89+
// The `menuAnchor` modifier must be passed to the text field to handle
90+
// expanding/collapsing the menu on click. A read-only text field has
91+
// the anchor type `PrimaryNotEditable`.
92+
modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
93+
state = textFieldState,
94+
readOnly = true,
95+
lineLimits = TextFieldLineLimits.SingleLine,
96+
label = { Text("Channel") },
97+
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
98+
colors = ExposedDropdownMenuDefaults.textFieldColors(),
99+
)
100+
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
101+
contents.forEach { content ->
102+
DropdownMenuItem(
103+
text = { Text(content.label, style = MaterialTheme.typography.bodyLarge) },
104+
onClick = {
105+
textFieldState.setTextAndPlaceCursorAtEnd(content.label)
106+
expanded = false
107+
loadContent(content)
108+
},
109+
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
110+
)
111+
}
112+
}
113+
}
114+
}
115+
116+
@Composable
117+
private fun ContentTypeToggleButton(
118+
currentContentType: AudioVideoLiveSwitchViewModel.ContentType,
119+
toggle: () -> Unit,
120+
modifier: Modifier = Modifier,
121+
) {
122+
OutlinedIconToggleButton(
123+
modifier = modifier,
124+
checked = currentContentType == AudioVideoLiveSwitchViewModel.ContentType.Video,
125+
onCheckedChange = {
126+
toggle()
127+
}
128+
) {
129+
val icon = when (currentContentType) {
130+
AudioVideoLiveSwitchViewModel.ContentType.Video -> Icons.Default.Videocam
131+
AudioVideoLiveSwitchViewModel.ContentType.Audio -> Icons.Default.Audiotrack
132+
}
133+
Icon(icon, "Toggle video or audio")
134+
}
135+
}

pillarbox-demo/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@
5858
<string name="settings_github">GitHub</string>
5959
<string name="settings_github_source_code">Source code</string>
6060
<string name="settings_github_project">Project</string>
61+
<string name="showcase_video_audio_switch">Switch video/audio</string>
6162
</resources>

0 commit comments

Comments
 (0)