Skip to content
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
4 changes: 4 additions & 0 deletions demos/compose/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

<uses-sdk/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

<application
android:allowBackup="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.media3.common.MediaItem
import androidx.media3.common.util.Util
import androidx.media3.demo.compose.layout.MainScreen
import androidx.media3.demo.compose.layout.SampleChooserScreen
import androidx.media3.demo.compose.viewmodel.ComposeDemoViewModel
Expand All @@ -45,12 +47,27 @@ import androidx.navigation.compose.rememberNavController

class MainActivity : ComponentActivity() {
private val sharedViewModel: ComposeDemoViewModel by viewModels()
private val mediaFolderPath = "/sdcard/Download/"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestMediaPermission()
enableEdgeToEdge()
setContent { ComposeDemoApp(modifier = Modifier.fillMaxSize(), viewModel = sharedViewModel) }
}

private fun requestMediaPermission() {
while (true) {
val permissionRequested = Util.maybeRequestReadStoragePermission(
this, MediaItem.fromUri(mediaFolderPath)
)
if (permissionRequested) {
Thread.sleep(5000)
} else {
break
}
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package androidx.media3.demo.compose.layout

import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
Expand Down Expand Up @@ -53,14 +54,19 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.demo.compose.buttons.LabeledProgressSlider
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.compose.material3.buttons.trackselection.TrackSelectionDialogButton
import androidx.media3.ui.compose.material3.Player
import androidx.media3.ui.compose.material3.buttons.PlaybackSpeedToggleButton
import androidx.media3.ui.compose.material3.buttons.RepeatButton
import androidx.media3.ui.compose.material3.buttons.ShuffleButton
import androidx.media3.ui.compose.material3.indicator.PositionAndDurationText

private const val TAG = "MainScreen"

@Composable
fun MainScreen(mediaItems: List<MediaItem>, modifier: Modifier = Modifier) {
Log.d(TAG, "MainScreen: mediaItems=$mediaItems")

val context = LocalContext.current
var player by remember { mutableStateOf<Player?>(null) }

Expand Down Expand Up @@ -163,6 +169,7 @@ private fun BottomControlsWithLabeledProgress(
) {
PositionAndDurationText(player, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.weight(1f))
TrackSelectionDialogButton(player)
PlaybackSpeedToggleButton(
player,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.primary),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package androidx.media3.demo.compose.layout

import android.content.Context
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -50,68 +51,101 @@ import androidx.media3.demo.compose.data.loadDurationsForMediaItems
import androidx.media3.demo.compose.data.loadPlaylistHolderGroups
import kotlinx.coroutines.launch

private const val TAG = "SampleChooserScreen"

@Composable
fun SampleChooserScreen(
onPlaylistClick: suspend (List<MediaItem>) -> Unit,
modifier: Modifier = Modifier,
context: Context = LocalContext.current,
onPlaylistClick: suspend (List<MediaItem>) -> Unit,
modifier: Modifier = Modifier,
context: Context = LocalContext.current,
) {
var playlistGroups by remember { mutableStateOf<List<PlaylistGroup>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
val coroutineScope = rememberCoroutineScope()
var playlistGroups by remember { mutableStateOf<List<PlaylistGroup>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
val coroutineScope = rememberCoroutineScope()

LaunchedEffect(context) {
playlistGroups = context.loadPlaylistHolderGroups()
isLoading = false
}
LaunchedEffect(context) {
playlistGroups = context.loadPlaylistHolderGroups()
isLoading = false
}

Box(modifier = modifier.background(MaterialTheme.colorScheme.background).fillMaxSize()) {
if (isLoading) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Text(
"Loading samples...",
modifier = Modifier.padding(top = 16.dp),
color = MaterialTheme.colorScheme.primary,
)
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
playlistGroups.forEach { group ->
item {
Text(
text = group.title,
fontWeight = FontWeight.Bold,
modifier =
Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
items(group.playlists) { playlist ->
ListItem(
headlineContent = { Text(playlist.name) },
modifier =
Modifier.clickable(enabled = !isLoading) {
coroutineScope.launch {
isLoading = true
onPlaylistClick(loadDurationsForMediaItems(context, playlist.mediaItems))
}
},
colors =
ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
headlineColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
}
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
) {
if (isLoading) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Text(
"Loading samples...",
modifier = Modifier.padding(top = 16.dp),
color = MaterialTheme.colorScheme.primary,
)
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Text(
text = "Testing",
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp)
)
}
item {
ListItem(
headlineContent = { Text("Video (Resolution tracks)") },
modifier = Modifier.clickable {
coroutineScope.launch {
onPlaylistClick(listOf(MediaItem.fromUri("file:///sdcard/Download/output_multi_track.mp4")))
}
},
)
}
item {
ListItem(
headlineContent = { Text("Audio (Language tracks)") },
modifier = Modifier.clickable {
coroutineScope.launch {
onPlaylistClick(listOf(MediaItem.fromUri("file:///sdcard/Download/output_multi_audio.mp4")))
}
},
)
}
// playlistGroups.forEach { group ->
// item {
// Text(
// text = group.title,
// fontWeight = FontWeight.Bold,
// modifier =
// Modifier.fillMaxWidth()
// .background(MaterialTheme.colorScheme.primaryContainer)
// .padding(16.dp),
// color = MaterialTheme.colorScheme.onPrimaryContainer,
// )
// }
// items(group.playlists) { playlist ->
// ListItem(
// headlineContent = { Text(playlist.name) },
// modifier =
// Modifier.clickable(enabled = !isLoading) {
// coroutineScope.launch {
// isLoading = true
// onPlaylistClick(loadDurationsForMediaItems(context, playlist.mediaItems))
// }
// },
// colors =
// ListItemDefaults.colors(
// containerColor = MaterialTheme.colorScheme.surfaceVariant,
// headlineColor = MaterialTheme.colorScheme.onSurfaceVariant,
// ),
// )
// }
// }
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.media3.ui.compose.buttons

import androidx.compose.runtime.Composable
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.state.TrackSelectionState
import androidx.media3.ui.compose.state.rememberTrackSelectionState

/**
* A state container for track selection.
*
* This composable exposes the [TrackSelectionState] to its [content] lambda,
* allowing developers to build custom track selection UIs using player tracks and parameters
* for a specific [trackType].
*
* @param player The [Player] to control.
* @param trackType The track type to manage (e.g. [C.TRACK_TYPE_VIDEO]).
* @param content The composable content to be displayed, which has access to the [TrackSelectionState].
*/
@UnstableApi
@Composable
fun TrackSelectionButton(
player: Player?,
trackType: @C.TrackType Int,
content: @Composable TrackSelectionState.() -> Unit,
) {
rememberTrackSelectionState(player, trackType).content()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.media3.ui.compose.buttons

import androidx.compose.runtime.Composable
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.state.TrackSelectionParametersState
import androidx.media3.ui.compose.state.rememberTrackSelectionParametersState

/**
* A state container for track selection parameters.
*
* This composable exposes the [TrackSelectionParametersState] to its [content] lambda,
* allowing developers to build custom track selection UIs using player tracks and parameters.
*
* @param player The [Player] to control.
* @param content The composable content to be displayed, which has access to the [TrackSelectionParametersState].
*/
@UnstableApi
@Composable
fun TrackSelectionParametersButton(
player: Player?,
content: @Composable TrackSelectionParametersState.() -> Unit,
) {
rememberTrackSelectionParametersState(player).content()
}
Loading