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