diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000000..dc3b8952ae6
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/demos/compose/src/main/AndroidManifest.xml b/demos/compose/src/main/AndroidManifest.xml
index 6194835b2cc..955fba6e16f 100644
--- a/demos/compose/src/main/AndroidManifest.xml
+++ b/demos/compose/src/main/AndroidManifest.xml
@@ -19,6 +19,10 @@
+
+
+
+
, modifier: Modifier = Modifier) {
+ Log.d(TAG, "MainScreen: mediaItems=$mediaItems")
+
val context = LocalContext.current
var player by remember { mutableStateOf(null) }
@@ -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),
diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/layout/SampleChooserScreen.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/layout/SampleChooserScreen.kt
index e6efd02b14d..4b9acfe235b 100644
--- a/demos/compose/src/main/java/androidx/media3/demo/compose/layout/SampleChooserScreen.kt
+++ b/demos/compose/src/main/java/androidx/media3/demo/compose/layout/SampleChooserScreen.kt
@@ -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
@@ -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) -> Unit,
- modifier: Modifier = Modifier,
- context: Context = LocalContext.current,
+ onPlaylistClick: suspend (List) -> Unit,
+ modifier: Modifier = Modifier,
+ context: Context = LocalContext.current,
) {
- var playlistGroups by remember { mutableStateOf>(emptyList()) }
- var isLoading by remember { mutableStateOf(true) }
- val coroutineScope = rememberCoroutineScope()
+ var playlistGroups by remember { mutableStateOf>(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,
+// ),
+// )
+// }
+// }
+ }
}
- }
}
- }
}
diff --git a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/buttons/TrackSelectionButton.kt b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/buttons/TrackSelectionButton.kt
new file mode 100644
index 00000000000..c7c88895cc5
--- /dev/null
+++ b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/buttons/TrackSelectionButton.kt
@@ -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()
+}
diff --git a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/buttons/TrackSelectionParametersButton.kt b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/buttons/TrackSelectionParametersButton.kt
new file mode 100644
index 00000000000..c6d43f64061
--- /dev/null
+++ b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/buttons/TrackSelectionParametersButton.kt
@@ -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()
+}
diff --git a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/state/TrackSelectionParametersState.kt b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/state/TrackSelectionParametersState.kt
new file mode 100644
index 00000000000..cc1127bb672
--- /dev/null
+++ b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/state/TrackSelectionParametersState.kt
@@ -0,0 +1,68 @@
+package androidx.media3.ui.compose.state
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.media3.common.Player
+import androidx.media3.common.TrackSelectionParameters
+import androidx.media3.common.Tracks
+import androidx.media3.common.util.UnstableApi
+
+/**
+ * Remembers the value of [TrackSelectionParametersState] created based on the passed [Player] and
+ * launch a coroutine to listen to [Player's][Player] changes. If the [Player] instance changes between
+ * compositions, produce and remember a new value.
+ */
+@UnstableApi
+@Composable
+fun rememberTrackSelectionParametersState(player: Player?): TrackSelectionParametersState {
+ val trackSelectionState = remember(player) { TrackSelectionParametersState(player) }
+ LaunchedEffect(player) { trackSelectionState.observe() }
+ return trackSelectionState
+}
+
+/**
+ * State that holds the current [Tracks] and [TrackSelectionParameters] of a [Player].
+ *
+ * This state is suitable for advanced use cases where direct access to the player's track
+ * information and parameters is needed, without any additional UI abstraction.
+ *
+ * @property[tracks] The current [Tracks] available in the player.
+ * @property[trackSelectionParameters] The current [TrackSelectionParameters] acting on the player.
+ */
+@UnstableApi
+class TrackSelectionParametersState(private val player: Player?) {
+
+ var tracks: Tracks by mutableStateOf(player?.currentTracks ?: Tracks.EMPTY)
+ private set
+
+ var trackSelectionParameters: TrackSelectionParameters by mutableStateOf(
+ player?.trackSelectionParameters ?: TrackSelectionParameters.DEFAULT
+ )
+ private set
+
+ private val playerStateObserver = player?.observeState(
+ Player.EVENT_TRACKS_CHANGED,
+ Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
+ ) {
+ tracks = player.currentTracks
+ trackSelectionParameters = player.trackSelectionParameters
+ }
+
+ /**
+ * Applies new track selection parameters to the player.
+ */
+ fun updateTrackSelectionParameters(params: TrackSelectionParameters) {
+ player?.trackSelectionParameters = params
+ }
+
+ /**
+ * Subscribes to updates from [Player.Events] and listens to
+ * [Player.EVENT_TRACKS_CHANGED] and [Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED]
+ * in order to determine whether the state properties should be updated.
+ */
+ suspend fun observe(): Nothing? = playerStateObserver?.observe()
+}
diff --git a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/state/TrackSelectionState.kt b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/state/TrackSelectionState.kt
new file mode 100644
index 00000000000..3d2e5d69277
--- /dev/null
+++ b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/state/TrackSelectionState.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.state
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.media3.common.C
+import androidx.media3.common.Format
+import androidx.media3.common.Player
+import androidx.media3.common.TrackSelectionOverride
+import androidx.media3.common.util.UnstableApi
+
+@UnstableApi
+enum class OnOffState {
+ OFF, ON_DEFAULT, ON_ALWAYS
+}
+
+@UnstableApi
+data class TrackSelectionOption(
+ val isSupported: Boolean,
+ val isSelected: Boolean,
+ val groupIndex: Int,
+ val trackIndex: Int,
+ val format: Format
+)
+
+@UnstableApi
+@Composable
+fun rememberTrackSelectionState(
+ player: Player?, trackType: @C.TrackType Int
+): TrackSelectionState {
+ val parametersState = rememberTrackSelectionParametersState(player)
+ return remember(parametersState, trackType) {
+ TrackSelectionState(parametersState, trackType)
+ }
+}
+
+@UnstableApi
+class TrackSelectionState(
+ private val parametersState: TrackSelectionParametersState, val trackType: @C.TrackType Int
+) {
+
+ /**
+ * Whether track selection is possible for the given [trackType].
+ *
+ * This is true if there is at least one track group of the [trackType] available.
+ */
+ val isEnabled: Boolean by derivedStateOf {
+ parametersState.tracks.groups.any { it.type == trackType }
+ }
+
+ /**
+ * The current [OnOffState] determining the selection logic for the [trackType].
+ *
+ * - [OnOffState.OFF]: The track type is explicitly disabled.
+ * - [OnOffState.ON_DEFAULT]: The track type is enabled and will use the player's default adaptive selection.
+ * - [OnOffState.ON_ALWAYS]: The track type is enabled and a specific track override is applied.
+ */
+ val onOffState: OnOffState by derivedStateOf {
+ val params = parametersState.trackSelectionParameters
+ if (params.disabledTrackTypes.contains(trackType)) {
+ OnOffState.OFF
+ } else {
+ // Check if we have an override for this type.
+ // We check if any of the overrides apply to a TrackGroup of our trackType.
+ val hasOverride = params.overrides.keys.any { group ->
+ parametersState.tracks.groups.any { it.type == trackType && it.mediaTrackGroup == group }
+ }
+ if (hasOverride) OnOffState.ON_ALWAYS else OnOffState.ON_DEFAULT
+ }
+ }
+
+ /**
+ * The currently selected [TrackSelectionOption], or `null` if the selection is automatic.
+ *
+ * This option is derived from the current [OnOffState] and the active [TrackSelectionOverride].
+ */
+ val selectedOption: TrackSelectionOption? by derivedStateOf {
+ if (onOffState != OnOffState.ON_ALWAYS) {
+ null
+ } else {
+ val params = parametersState.trackSelectionParameters
+ val checkGroups = parametersState.tracks.groups
+ // Find the override that caused ON_ALWAYS
+ val override = params.overrides.entries.firstOrNull { (group, _) ->
+ checkGroups.any { it.type == trackType && it.mediaTrackGroup == group }
+ }?.value
+
+ if (override != null && override.trackIndices.isNotEmpty()) {
+ // Assuming single track selection for this simple state holder
+ val selectedTrackIndex = override.trackIndices[0]
+ val selectedGroup = override.mediaTrackGroup
+ selectionOptions.firstOrNull { option ->
+ checkGroups[option.groupIndex].mediaTrackGroup == selectedGroup &&
+ option.trackIndex == selectedTrackIndex
+ }
+ } else {
+ null
+ }
+ }
+ }
+
+ /**
+ * The list of available [TrackSelectionOption]s for the [trackType].
+ *
+ * This list includes all tracks from all track groups of the [trackType].
+ */
+ val selectionOptions: List by derivedStateOf {
+ val list = mutableListOf()
+ val groups = parametersState.tracks.groups
+ for (i in 0 until groups.size) {
+ val group = groups[i]
+ if (group.type == trackType) {
+ for (j in 0 until group.length) {
+ list.add(
+ TrackSelectionOption(
+ isSupported = group.isTrackSupported(j),
+ isSelected = group.isTrackSelected(j),
+ groupIndex = i,
+ trackIndex = j,
+ format = group.getTrackFormat(j)
+ )
+ )
+ }
+ }
+ }
+ list
+ }
+
+ fun setOnOff(state: OnOffState) {
+ val builder = parametersState.trackSelectionParameters.buildUpon()
+ when (state) {
+ OnOffState.OFF -> builder.setTrackTypeDisabled(trackType, true)
+ OnOffState.ON_DEFAULT -> {
+ builder.setTrackTypeDisabled(trackType, false)
+ builder.clearOverridesOfType(trackType)
+ }
+ OnOffState.ON_ALWAYS -> {
+ builder.setTrackTypeDisabled(trackType, false)
+ }
+ }
+ parametersState.updateTrackSelectionParameters(builder.build())
+ }
+
+ fun selectOption(option: TrackSelectionOption?) {
+ val builder = parametersState.trackSelectionParameters.buildUpon()
+ builder.setTrackTypeDisabled(trackType, false) // Ensure enabled
+
+ if (option == null) {
+ // "Auto" -> Clear overrides
+ builder.clearOverridesOfType(trackType)
+ } else {
+ val group = parametersState.tracks.groups[option.groupIndex]
+ val override = TrackSelectionOverride(group.mediaTrackGroup, option.trackIndex)
+ builder.setOverrideForType(override)
+ }
+ parametersState.updateTrackSelectionParameters(builder.build())
+ }
+}
diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/AudioLanguageTrackSelectionDialog.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/AudioLanguageTrackSelectionDialog.kt
new file mode 100644
index 00000000000..7f98e84de9c
--- /dev/null
+++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/AudioLanguageTrackSelectionDialog.kt
@@ -0,0 +1,58 @@
+package androidx.media3.ui.compose.material3.buttons.trackselection
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.media3.common.C
+import androidx.media3.common.TrackSelectionOverride
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.compose.material3.R
+import androidx.media3.ui.compose.state.TrackSelectionParametersState
+
+
+@UnstableApi
+@Composable
+fun AudioLanguageDialog(
+ state: TrackSelectionParametersState,
+ onDismissRequest: () -> Unit,
+ onOptionSelected: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(R.string.track_selection_title_audio)) },
+ text = {
+ LazyColumn {
+ val groups = state.tracks.groups
+ for (i in 0 until groups.size) {
+ val group = groups[i]
+ if (group.type == C.TRACK_TYPE_AUDIO) {
+ items(group.length) { trackIndex ->
+ val format = group.getTrackFormat(trackIndex)
+ val isSelected = group.isTrackSelected(trackIndex)
+ // Basic language display
+ val trackLabel = getAudioTrackLabel(format)
+
+ TrackSelectionOption(
+ text = trackLabel,
+ selected = isSelected,
+ onClick = {
+ val override = TrackSelectionOverride(group.mediaTrackGroup, trackIndex)
+ val newParams = state.trackSelectionParameters
+ .buildUpon()
+ .setOverrideForType(override)
+ .build()
+ state.updateTrackSelectionParameters(newParams)
+ onOptionSelected()
+ }
+ )
+ }
+ }
+ }
+ }
+ },
+ confirmButton = {}
+ )
+}
diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackLabelProvider.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackLabelProvider.kt
new file mode 100644
index 00000000000..52f9e14380a
--- /dev/null
+++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackLabelProvider.kt
@@ -0,0 +1,52 @@
+package androidx.media3.ui.compose.material3.buttons.trackselection
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.media3.common.C
+import androidx.media3.common.Format
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.compose.material3.R
+import java.util.Locale
+
+/**
+ * Returns a localized name for an audio track, including its language and bitrate.
+ *
+ * @param format The [Format] of the audio track.
+ * @return A string representation of the track name (e.g. "English • 128 kbps").
+ */
+@UnstableApi
+@Composable
+internal fun getAudioTrackLabel(format: Format): String {
+ val language = format.language
+ val locale = if (language == null || language == C.LANGUAGE_UNDETERMINED) {
+ null
+ } else {
+ Locale.forLanguageTag(language)
+ }
+ val languageName = locale?.displayName ?: stringResource(R.string.track_selection_unknown)
+ val label = format.label ?: languageName
+ return buildString {
+ append(label)
+ if (format.bitrate != Format.NO_VALUE) {
+ append(" • ${format.bitrate / 1000} kbps")
+ }
+ }
+}
+
+/**
+ * Returns a localized label for a video track, including its resolution.
+ *
+ * @param format The [Format] of the video track.
+ * @return A string representation of the track label (e.g. "1920 × 1080").
+ */
+@UnstableApi
+@Composable
+internal fun getVideoTrackLabel(format: Format): String {
+ val width = format.width
+ val height = format.height
+ return if (width == Format.NO_VALUE || height == Format.NO_VALUE) {
+ stringResource(R.string.track_selection_unknown)
+ } else {
+ "$width × $height"
+ }
+}
diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackSelectionDialogButton.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackSelectionDialogButton.kt
new file mode 100644
index 00000000000..3d5cf390be6
--- /dev/null
+++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackSelectionDialogButton.kt
@@ -0,0 +1,195 @@
+package androidx.media3.ui.compose.material3.buttons.trackselection
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+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.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.media3.common.C
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.compose.material3.R
+import androidx.media3.ui.compose.material3.buttons.ClickableIconButton
+import androidx.media3.ui.compose.state.OnOffState
+import androidx.media3.ui.compose.state.TrackSelectionParametersState
+import androidx.media3.ui.compose.state.TrackSelectionState
+import androidx.media3.ui.compose.state.rememberTrackSelectionParametersState
+import androidx.media3.ui.compose.state.rememberTrackSelectionState
+
+/**
+ * A Material3 [IconButton][androidx.compose.material3.IconButton] that opens a settings dialog
+ * for track selection (Video Resolution and Audio Language).
+ *
+ * @param player The [Player] to control.
+ * @param modifier The [Modifier] to be applied to the button.
+ * @param tint Tint to be applied to the icon.
+ */
+@UnstableApi
+@Composable
+fun TrackSelectionDialogButton(
+ player: Player?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current,
+) {
+ var showMainDialog by remember { mutableStateOf(false) }
+ var showVideoDialog by remember { mutableStateOf(false) }
+ var showAudioDialog by remember { mutableStateOf(false) }
+
+ val videoState = rememberTrackSelectionState(player, C.TRACK_TYPE_VIDEO)
+ val audioState = rememberTrackSelectionParametersState(player)
+
+ ClickableIconButton(
+ modifier = modifier,
+ enabled = true,
+ icon = painterResource(R.drawable.media3_icon_settings),
+ contentDescription = "Settings",
+ tint = tint,
+ onClick = { showMainDialog = true },
+ )
+
+ if (showMainDialog) {
+ AlertDialog(
+ onDismissRequest = { showMainDialog = false },
+ title = { Text(stringResource(R.string.track_selection_settings_title)) },
+ text = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ TrackSelectionDialogRow(
+ title = stringResource(R.string.track_selection_quality_title),
+ subtitle = getVideoLabel(videoState),
+ icon = painterResource(R.drawable.media3_icon_settings),
+ onClick = {
+ showMainDialog = false
+ showVideoDialog = true
+ }
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ TrackSelectionDialogRow(
+ title = stringResource(R.string.track_selection_audio_title),
+ subtitle = getAudioLabel(audioState),
+ icon = painterResource(R.drawable.media3_icon_play),
+ onClick = {
+ showMainDialog = false
+ showAudioDialog = true
+ }
+ )
+ }
+ },
+ confirmButton = {}
+ )
+ }
+
+ if (showVideoDialog) {
+ VideoResolutionDialog(
+ state = videoState,
+ onDismissRequest = {
+ showVideoDialog = false
+ showMainDialog = true
+ },
+ onOptionSelected = {
+ showVideoDialog = false
+ showMainDialog = false
+ }
+ )
+ }
+
+ if (showAudioDialog) {
+ AudioLanguageDialog(
+ state = audioState,
+ onDismissRequest = {
+ showAudioDialog = false
+ showMainDialog = true
+ },
+ onOptionSelected = {
+ showAudioDialog = false
+ showMainDialog = false
+ }
+ )
+ }
+}
+
+@Composable
+private fun TrackSelectionDialogRow(
+ title: String,
+ subtitle: String,
+ icon: Painter,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(vertical = 12.dp, horizontal = 16.dp), // Added horizontal padding for standard dialog look
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = icon,
+ contentDescription = null,
+ modifier = Modifier.width(24.dp).height(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = title,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.weight(1f)
+ )
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
+
+@UnstableApi
+@Composable
+private fun getVideoLabel(state: TrackSelectionState): String {
+ return when (state.onOffState) {
+ OnOffState.OFF -> stringResource(R.string.track_selection_off)
+ OnOffState.ON_DEFAULT -> if (state.selectedOption == null) stringResource(R.string.track_selection_auto) else getVideoTrackLabel(state.selectedOption!!.format)
+ OnOffState.ON_ALWAYS -> getVideoTrackLabel(state.selectedOption!!.format)
+ }
+}
+
+@UnstableApi
+@Composable
+private fun getAudioLabel(state: TrackSelectionParametersState): String {
+ // Iterate to find selected audio track
+ val groups = state.tracks.groups
+ for (i in 0 until groups.size) {
+ val group = groups[i]
+ if (group.type == C.TRACK_TYPE_AUDIO) {
+ for (trackIndex in 0 until group.length) {
+ if (group.isTrackSelected(trackIndex)) {
+ val format = group.getTrackFormat(trackIndex)
+ return getAudioTrackLabel(format)
+ }
+ }
+ }
+ }
+ return stringResource(R.string.track_selection_default)
+}
diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackSelectionOption.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackSelectionOption.kt
new file mode 100644
index 00000000000..b23192eb5a7
--- /dev/null
+++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/TrackSelectionOption.kt
@@ -0,0 +1,50 @@
+package androidx.media3.ui.compose.material3.buttons.trackselection
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.media3.common.util.UnstableApi
+
+/**
+ * A radio button option for track selection dialogs.
+ *
+ * @param text The text to display for the option.
+ * @param selected Whether the option is currently selected.
+ * @param onClick Callback to be invoked when the option is clicked.
+ * @param modifier The [Modifier] to be applied to the option.
+ */
+@UnstableApi
+@Composable
+internal fun TrackSelectionOption(
+ text: String,
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(vertical = 8.dp, horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selected,
+ onClick = null
+ )
+ Text(
+ text = text,
+ modifier = Modifier.padding(start = 16.dp),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+}
diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/VideoResolutionTrackSelectionDialog.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/VideoResolutionTrackSelectionDialog.kt
new file mode 100644
index 00000000000..cc6b4ffd148
--- /dev/null
+++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/buttons/trackselection/VideoResolutionTrackSelectionDialog.kt
@@ -0,0 +1,66 @@
+package androidx.media3.ui.compose.material3.buttons.trackselection
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.compose.material3.R
+import androidx.media3.ui.compose.state.OnOffState
+import androidx.media3.ui.compose.state.TrackSelectionState
+
+
+@UnstableApi
+@Composable
+fun VideoResolutionDialog(
+ state: TrackSelectionState,
+ onDismissRequest: () -> Unit,
+ onOptionSelected: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(stringResource(R.string.track_selection_title_video)) },
+ text = {
+ LazyColumn {
+ items(state.selectionOptions.size) { index ->
+ val option = state.selectionOptions[index]
+ val trackLabel = getVideoTrackLabel(option.format)
+
+ TrackSelectionOption(
+ text = trackLabel,
+ selected = state.selectedOption == option,
+ onClick = {
+ state.selectOption(option)
+ onOptionSelected()
+ }
+ )
+ }
+
+ item {
+ TrackSelectionOption(
+ text = stringResource(R.string.track_selection_auto),
+ selected = state.selectedOption == null && state.onOffState == OnOffState.ON_DEFAULT,
+ onClick = {
+ state.selectOption(null)
+ onOptionSelected()
+ }
+ )
+ }
+
+ item {
+ TrackSelectionOption(
+ text = stringResource(R.string.track_selection_off),
+ selected = state.onOffState == OnOffState.OFF,
+ onClick = {
+ state.setOnOff(OnOffState.OFF)
+ onOptionSelected()
+ }
+ )
+ }
+ }
+ },
+ confirmButton = {}
+ )
+}
diff --git a/libraries/ui_compose_material3/src/main/res/drawable/media3_icon_settings.xml b/libraries/ui_compose_material3/src/main/res/drawable/media3_icon_settings.xml
new file mode 100644
index 00000000000..8ade9488024
--- /dev/null
+++ b/libraries/ui_compose_material3/src/main/res/drawable/media3_icon_settings.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/libraries/ui_compose_material3/src/main/res/values/strings.xml b/libraries/ui_compose_material3/src/main/res/values/strings.xml
index 83d7eda116d..ae8d846485a 100644
--- a/libraries/ui_compose_material3/src/main/res/values/strings.xml
+++ b/libraries/ui_compose_material3/src/main/res/values/strings.xml
@@ -55,4 +55,17 @@
Decrease speed by %.2f
%.2fx
+
+
+ Settings
+ Settings
+ Quality
+ Audio
+
+ Off
+ Auto
+ Unknown
+ Default
+ Video Resolution
+ Audio Language