From 0210eed63bfa33772b737a9624e8308e461052b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 22 Apr 2024 16:37:00 +0200 Subject: [PATCH] Remove quality selector and add video selector --- .../settings/PlayerSettingsViewModel.kt | 73 ++- .../ui/player/settings/SettingItemOptions.kt | 19 - .../ui/player/settings/TracksSettingItem.kt | 27 ++ .../src/main/res/values/strings.xml | 2 +- .../settings/PlaybackSettingsDrawer.kt | 185 ++++---- .../settings/PlaybackSettingsContent.kt | 67 +-- .../settings/SelectionSettingOptions.kt | 122 ----- .../player/settings/TrackSelectionSettings.kt | 192 ++++++++ .../player/tracks/PlayerExtensions.kt | 52 --- .../srgssr/pillarbox/player/tracks/Track.kt | 6 + .../pillarbox/player/tracks/VideoQuality.kt | 33 -- .../player/tracks/PlayerExtensionsTest.kt | 415 ------------------ 12 files changed, 346 insertions(+), 847 deletions(-) delete mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingItemOptions.kt create mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/TracksSettingItem.kt delete mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/SelectionSettingOptions.kt create mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/VideoQuality.kt delete mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensionsTest.kt diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/PlayerSettingsViewModel.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/PlayerSettingsViewModel.kt index 1656d472f..24145415a 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/PlayerSettingsViewModel.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/PlayerSettingsViewModel.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.media3.common.C import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.player.extension.displayName @@ -25,18 +26,17 @@ import ch.srgssr.pillarbox.player.getCurrentTracksAsFlow import ch.srgssr.pillarbox.player.getPlaybackSpeedAsFlow import ch.srgssr.pillarbox.player.getTrackSelectionParametersAsFlow import ch.srgssr.pillarbox.player.tracks.Track -import ch.srgssr.pillarbox.player.tracks.VideoQuality +import ch.srgssr.pillarbox.player.tracks.VideoTrack import ch.srgssr.pillarbox.player.tracks.audioTracks import ch.srgssr.pillarbox.player.tracks.disableAudioTrack import ch.srgssr.pillarbox.player.tracks.disableTextTrack import ch.srgssr.pillarbox.player.tracks.disableVideoTrack -import ch.srgssr.pillarbox.player.tracks.selectMaxVideoQuality import ch.srgssr.pillarbox.player.tracks.selectTrack import ch.srgssr.pillarbox.player.tracks.setAutoAudioTrack import ch.srgssr.pillarbox.player.tracks.setAutoTextTrack import ch.srgssr.pillarbox.player.tracks.setAutoVideoTrack import ch.srgssr.pillarbox.player.tracks.textTracks -import ch.srgssr.pillarbox.player.tracks.videoQualities +import ch.srgssr.pillarbox.player.tracks.videoTracks import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -67,9 +67,9 @@ class PlayerSettingsViewModel( tracks, trackSelectionParameters ) { tracks, trackSelectionParameters -> - SettingItemOptions( + TracksSettingItem( title = application.getString(R.string.subtitles), - items = tracks.textTracks, + tracks = tracks.textTracks, disabled = trackSelectionParameters.isTextTrackDisabled ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -81,23 +81,27 @@ class PlayerSettingsViewModel( tracks, trackSelectionParameters ) { tracks, trackSelectionParameters -> - SettingItemOptions( + TracksSettingItem( title = application.getString(R.string.audio_track), - items = tracks.audioTracks, + tracks = tracks.audioTracks, disabled = trackSelectionParameters.isAudioTrackDisabled ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) /** - * All the available video qualities for the current [player]. + * All the available video tracks for the current [player]. */ - val videoQualities = combine( + val videoTracks = combine( tracks, trackSelectionParameters, - ) { _, trackSelectionParameters -> - SettingItemOptions( - title = application.getString(R.string.quality), - items = player.videoQualities, + ) { tracks, trackSelectionParameters -> + TracksSettingItem( + title = application.getString(R.string.video_tracks), + tracks = tracks.videoTracks + .sortedWith( + compareByDescending { it.format.height } + .thenByDescending { it.format.bitrate } + ), disabled = trackSelectionParameters.isVideoTrackDisabled, ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -121,10 +125,10 @@ class PlayerSettingsViewModel( val settings = combine( subtitles, audioTracks, - videoQualities, + videoTracks, trackSelectionParameters, playbackSpeed, - ) { subtitles, audioTracks, videoQualities, trackSelectionParameters, playbackSpeed -> + ) { subtitles, audioTracks, videoTracks, trackSelectionParameters, playbackSpeed -> buildList { add( SettingItem( @@ -135,12 +139,12 @@ class PlayerSettingsViewModel( ) ) - if (subtitles != null && subtitles.items.isNotEmpty()) { + if (subtitles != null && subtitles.tracks.isNotEmpty()) { add( SettingItem( title = application.getString(R.string.subtitles), subtitle = getTracksSubtitle( - tracks = subtitles.items, + tracks = subtitles.tracks, disabled = trackSelectionParameters.isTextTrackDisabled, ), icon = Icons.Default.ClosedCaption, @@ -149,12 +153,12 @@ class PlayerSettingsViewModel( ) } - if (audioTracks != null && audioTracks.items.isNotEmpty()) { + if (audioTracks != null && audioTracks.tracks.isNotEmpty()) { add( SettingItem( title = application.getString(R.string.audio_track), subtitle = getTracksSubtitle( - tracks = audioTracks.items, + tracks = audioTracks.tracks, disabled = trackSelectionParameters.isAudioTrackDisabled, ), icon = Icons.Default.RecordVoiceOver, @@ -163,12 +167,12 @@ class PlayerSettingsViewModel( ) } - if (videoQualities != null && videoQualities.items.isNotEmpty()) { + if (videoTracks != null && videoTracks.tracks.isNotEmpty()) { add( SettingItem( - title = application.getString(R.string.quality), - subtitle = getVideoQualitiesSubtitle( - videoQualities = videoQualities.items, + title = application.getString(R.string.video_tracks), + subtitle = getTracksSubtitle( + tracks = videoTracks.tracks, disabled = trackSelectionParameters.isVideoTrackDisabled, ), icon = Icons.Default.Tune, @@ -188,15 +192,6 @@ class PlayerSettingsViewModel( player.selectTrack(track) } - /** - * Select the max video quality. - * - * @param videoQuality The max video quality. - */ - fun selectMaxVideoQuality(videoQuality: VideoQuality) { - player.selectMaxVideoQuality(videoQuality) - } - /** * Reset the subtitles. */ @@ -257,19 +252,7 @@ class PlayerSettingsViewModel( } else { tracks.filter { it.isSelected } .map { it.format.displayName } - .firstOrNull() - } - } - - private fun getVideoQualitiesSubtitle( - videoQualities: List, - disabled: Boolean, - ): String? { - return if (disabled) { - application.getString(R.string.disabled) - } else { - videoQualities.filter { it.isSelected } - .map { "${it.height}p" } + .filter { it != C.LANGUAGE_UNDETERMINED } .firstOrNull() } } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingItemOptions.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingItemOptions.kt deleted file mode 100644 index 725edc491..000000000 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/SettingItemOptions.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.shared.ui.player.settings - -/** - * The options for a specific setting item. - * - * @param T The type of option. - * @property title The title of the setting. - * @property items The list of possible items. - * @property disabled `true` if this kind of tracks is disabled, `false` otherwise. - */ -data class SettingItemOptions( - val title: String, - val items: List, - val disabled: Boolean, -) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/TracksSettingItem.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/TracksSettingItem.kt new file mode 100644 index 000000000..7ba446e9b --- /dev/null +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/settings/TracksSettingItem.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.shared.ui.player.settings + +import ch.srgssr.pillarbox.player.tracks.Track + +/** + * The setting for a specific kind a track (audio/text/video). + */ +data class TracksSettingItem( + /** + * The title of the setting. + */ + val title: String, + + /** + * The list of possible tracks. + */ + val tracks: List, + + /** + * `true` if this kind of tracks is disabled, `false` otherwise. + */ + val disabled: Boolean +) diff --git a/pillarbox-demo-shared/src/main/res/values/strings.xml b/pillarbox-demo-shared/src/main/res/values/strings.xml index d80d1f87e..e6d0c7283 100644 --- a/pillarbox-demo-shared/src/main/res/values/strings.xml +++ b/pillarbox-demo-shared/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ %1$d min Settings Audio track - Quality + Video tacks Subtitles Speed Normal diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt index 1bc5f42ca..f6fb41eea 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt @@ -11,10 +11,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.HearingDisabled import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState @@ -22,6 +24,7 @@ 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.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -52,11 +55,15 @@ import androidx.tv.material3.NavigationDrawerScope import androidx.tv.material3.Text import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.player.settings.PlayerSettingsViewModel -import ch.srgssr.pillarbox.demo.shared.ui.player.settings.SettingItemOptions import ch.srgssr.pillarbox.demo.shared.ui.player.settings.SettingsRoutes +import ch.srgssr.pillarbox.demo.shared.ui.player.settings.TracksSettingItem import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import ch.srgssr.pillarbox.player.extension.displayName import ch.srgssr.pillarbox.player.extension.hasAccessibilityRoles +import ch.srgssr.pillarbox.player.extension.isForced +import ch.srgssr.pillarbox.player.tracks.AudioTrack +import ch.srgssr.pillarbox.player.tracks.Track +import ch.srgssr.pillarbox.player.tracks.VideoTrack /** * Drawer used to display a player's settings. @@ -174,75 +181,20 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( tracksSetting = it, onResetClick = settingsViewModel::resetAudioTrack, onDisabledClick = settingsViewModel::disableAudioTrack, - itemContent = { item -> - NavigationDrawerItem( - selected = item.isSelected, - onClick = { settingsViewModel.selectTrack(item) }, - leadingContent = { - AnimatedVisibility(visible = item.isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - }, - content = { - val format = item.format - val label = buildString { - append(format.displayName) - - if (format.bitrate > Format.NO_VALUE) { - append(" @") - append(format.bitrate) - append(" bit/sec") - } - - if (format.hasAccessibilityRoles()) { - append(" (AD)") - } - } - - Text( - text = label, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - } - ) - }, + onTrackClick = settingsViewModel::selectTrack, ) } } composable(SettingsRoutes.VideoQuality.route) { - val videoQualities by settingsViewModel.videoQualities.collectAsState() + val videoTracks by settingsViewModel.videoTracks.collectAsState() - videoQualities?.let { + videoTracks?.let { TracksSetting( tracksSetting = it, onResetClick = settingsViewModel::resetVideoTrack, onDisabledClick = settingsViewModel::disableVideoTrack, - itemContent = { item -> - NavigationDrawerItem( - selected = item.isSelected, - onClick = { settingsViewModel.selectMaxVideoQuality(item) }, - leadingContent = { - AnimatedVisibility(visible = item.isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - }, - content = { - Text( - text = "${item.height}p", - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - } - ) - }, + onTrackClick = settingsViewModel::selectTrack, ) } } @@ -255,42 +207,7 @@ private fun NavigationDrawerScope.NavigationDrawerNavHost( tracksSetting = it, onResetClick = settingsViewModel::resetSubtitles, onDisabledClick = settingsViewModel::disableSubtitles, - itemContent = { item -> - NavigationDrawerItem( - selected = item.isSelected, - onClick = { settingsViewModel.selectTrack(item) }, - leadingContent = { - AnimatedVisibility(visible = item.isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - }, - content = { - val format = item.format - val label = buildString { - append(format.displayName) - - if (format.bitrate > Format.NO_VALUE) { - append(" @") - append(format.bitrate) - append(" bit/sec") - } - - if (format.hasAccessibilityRoles()) { - append(" (AD)") - } - } - - Text( - text = label, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - } - ) - }, + onTrackClick = settingsViewModel::selectTrack, ) } } @@ -360,12 +277,12 @@ private fun NavigationDrawerScope.GenericSetting( @Composable @OptIn(ExperimentalTvMaterial3Api::class) -private fun NavigationDrawerScope.TracksSetting( - tracksSetting: SettingItemOptions, +private fun NavigationDrawerScope.TracksSetting( + tracksSetting: TracksSettingItem, modifier: Modifier = Modifier, onResetClick: () -> Unit, onDisabledClick: () -> Unit, - itemContent: @Composable (item: T) -> Unit, + onTrackClick: (track: Track) -> Unit, ) { Column( modifier = modifier @@ -418,8 +335,74 @@ private fun NavigationDrawerScope.TracksSetting( ) } - items(tracksSetting.items) { item -> - itemContent(item) + items(tracksSetting.tracks) { track -> + val format = track.format + NavigationDrawerItem( + selected = track.isSelected, + enabled = track.isSupported && !format.isForced(), + onClick = { onTrackClick(track) }, + leadingContent = { + AnimatedVisibility(visible = track.isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + }, + content = { + when (track) { + is AudioTrack -> { + val text = buildString { + append(format.displayName) + + if (format.bitrate > Format.NO_VALUE) { + append(" @%1\$.2f Mbps".format(format.bitrate / 1_000_000f)) + } + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = text) + if (format.hasAccessibilityRoles()) { + Icon( + imageVector = Icons.Filled.HearingDisabled, + contentDescription = "AD", + modifier = Modifier.padding(start = MaterialTheme.paddings.small), + ) + } + } + } + + is VideoTrack -> { + val text = buildString { + append(format.width) + append("x") + append(format.height) + + if (format.bitrate > Format.NO_VALUE) { + append(" @%1\$.2f Mbps".format(format.bitrate / 1_000_000f)) + } + } + + Text(text = text) + } + + else -> { + if (format.hasAccessibilityRoles()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = format.displayName) + Icon( + imageVector = Icons.Default.HearingDisabled, + contentDescription = "Hearing disabled", + modifier = Modifier.padding(start = MaterialTheme.paddings.small), + ) + } + } else { + Text(text = format.displayName) + } + } + } + } + ) } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt index aaad6f4ae..f5a1d30c6 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt @@ -7,7 +7,6 @@ package ch.srgssr.pillarbox.demo.ui.player.settings import android.app.Application import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Icon @@ -22,7 +21,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.media3.common.Format import androidx.media3.common.Player import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -30,8 +28,6 @@ import androidx.navigation.compose.rememberNavController import ch.srgssr.pillarbox.demo.shared.ui.player.settings.PlayerSettingsViewModel import ch.srgssr.pillarbox.demo.shared.ui.player.settings.SettingItem import ch.srgssr.pillarbox.demo.shared.ui.player.settings.SettingsRoutes -import ch.srgssr.pillarbox.player.extension.displayName -import ch.srgssr.pillarbox.player.extension.hasAccessibilityRoles /** * Playback settings content @@ -92,25 +88,11 @@ fun PlaybackSettingsContent(player: Player) { ) { val subtitles by settingsViewModel.subtitles.collectAsState() subtitles?.let { - SelectionSettingOptions( + TrackSelectionSettings( tracksSetting = it, onResetClick = settingsViewModel::resetSubtitles, onDisabledClick = settingsViewModel::disableSubtitles, - itemContent = { item -> - SettingsOption( - modifier = Modifier.fillMaxWidth(), - selected = item.isSelected, - onClick = { settingsViewModel.selectTrack(item) }, - content = { - val format = item.format - if (format.hasAccessibilityRoles()) { - Text(text = format.displayName + " (AD)") - } else { - Text(text = format.displayName) - } - }, - ) - }, + onTrackClick = settingsViewModel::selectTrack, ) } } @@ -126,35 +108,11 @@ fun PlaybackSettingsContent(player: Player) { ) { val audioTracks by settingsViewModel.audioTracks.collectAsState() audioTracks?.let { - SelectionSettingOptions( + TrackSelectionSettings( tracksSetting = it, onResetClick = settingsViewModel::resetAudioTrack, onDisabledClick = settingsViewModel::disableAudioTrack, - itemContent = { item -> - SettingsOption( - modifier = Modifier.fillMaxWidth(), - selected = item.isSelected, - onClick = { settingsViewModel.selectTrack(item) }, - content = { - val format = item.format - val text = buildString { - append(format.displayName) - - if (format.bitrate > Format.NO_VALUE) { - append(" @") - append(format.bitrate) - append(" bit/sec") - } - - if (format.hasAccessibilityRoles()) { - append(" (AD)") - } - } - - Text(text = text) - }, - ) - }, + onTrackClick = settingsViewModel::selectTrack, ) } } @@ -168,22 +126,13 @@ fun PlaybackSettingsContent(player: Player) { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Up) } ) { - val videoQualities by settingsViewModel.videoQualities.collectAsState() - videoQualities?.let { - SelectionSettingOptions( + val videoTracks by settingsViewModel.videoTracks.collectAsState() + videoTracks?.let { + TrackSelectionSettings( tracksSetting = it, onResetClick = settingsViewModel::resetVideoTrack, onDisabledClick = settingsViewModel::disableVideoTrack, - itemContent = { item -> - SettingsOption( - modifier = Modifier.fillMaxWidth(), - selected = item.isSelected, - onClick = { settingsViewModel.selectMaxVideoQuality(item) }, - content = { - Text(text = "${item.height}p") - }, - ) - }, + onTrackClick = settingsViewModel::selectTrack, ) } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/SelectionSettingOptions.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/SelectionSettingOptions.kt deleted file mode 100644 index 97a85dc9c..000000000 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/SelectionSettingOptions.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.demo.ui.player.settings - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ListItem -import androidx.compose.material3.Text -import androidx.compose.material3.minimumInteractiveComponentSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.media3.common.C -import androidx.media3.common.Format -import androidx.media3.common.TrackGroup -import androidx.media3.common.Tracks -import ch.srgssr.pillarbox.demo.shared.R -import ch.srgssr.pillarbox.demo.shared.ui.player.settings.SettingItemOptions -import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme -import ch.srgssr.pillarbox.player.extension.displayName -import ch.srgssr.pillarbox.player.tracks.TextTrack - -/** - * Selection setting options. - * - * @param T The type of option. - * @param tracksSetting List of tracks. - * @param modifier The [Modifier] to apply to this screen. - * @param onResetClick The action to perform when clicking on the reset button. - * @param onDisabledClick The action to perform when clicking on the disable button. - * @param itemContent The content to display for the provided [item][T]. - */ -@Composable -fun SelectionSettingOptions( - tracksSetting: SettingItemOptions, - modifier: Modifier = Modifier, - onResetClick: () -> Unit, - onDisabledClick: () -> Unit, - itemContent: @Composable (item: T) -> Unit, -) { - val itemModifier = Modifier.fillMaxWidth() - LazyColumn(modifier = modifier) { - item { - ListItem( - modifier = itemModifier - .minimumInteractiveComponentSize() - .clickable { onResetClick() }, - headlineContent = { - Text( - text = stringResource(R.string.reset_to_default) - ) - } - ) - HorizontalDivider() - } - item { - SettingsOption( - modifier = itemModifier, - selected = tracksSetting.disabled, - onClick = onDisabledClick, - content = { - Text(text = stringResource(R.string.disabled)) - } - ) - HorizontalDivider() - } - items(tracksSetting.items) { item -> - itemContent(item) - } - item { - HorizontalDivider() - } - } -} - -@Preview -@Composable -private fun SelectionSettingOptionsPreview() { - // Track are group by language. - val textTrackFr1 = Format.Builder() - .setLabel("FR1") - .setId("subtitle:0") - .setLanguage("fr") - .build() - val textTrackEn1 = Format.Builder() - .setLabel("EN1") - .setId("subtitle:1") - .setLanguage("en") - .build() - val dummyListTrack = listOf( - TextTrack( - group = Tracks.Group(TrackGroup("fr", textTrackFr1), false, intArrayOf(C.FORMAT_HANDLED), booleanArrayOf(true)), - groupIndex = 0, - trackIndexInGroup = 0, - ), - TextTrack( - group = Tracks.Group(TrackGroup("en", textTrackEn1), false, intArrayOf(C.FORMAT_HANDLED), booleanArrayOf(false)), - groupIndex = 0, - trackIndexInGroup = 0, - ), - ) - PillarboxTheme { - SelectionSettingOptions( - tracksSetting = SettingItemOptions( - title = stringResource(R.string.subtitles), - items = dummyListTrack, - disabled = false - ), - itemContent = { item -> - Text(text = item.format.displayName) - }, - onResetClick = {}, - onDisabledClick = {}, - ) - } -} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt new file mode 100644 index 000000000..c6521be8d --- /dev/null +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.ui.player.settings + +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.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HearingDisabled +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.media3.common.C +import androidx.media3.common.Format +import androidx.media3.common.TrackGroup +import androidx.media3.common.Tracks +import ch.srgssr.pillarbox.demo.shared.R +import ch.srgssr.pillarbox.demo.shared.ui.player.settings.TracksSettingItem +import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme +import ch.srgssr.pillarbox.demo.ui.theme.paddings +import ch.srgssr.pillarbox.player.extension.displayName +import ch.srgssr.pillarbox.player.extension.hasAccessibilityRoles +import ch.srgssr.pillarbox.player.extension.isForced +import ch.srgssr.pillarbox.player.tracks.AudioTrack +import ch.srgssr.pillarbox.player.tracks.Track +import ch.srgssr.pillarbox.player.tracks.VideoTrack + +/** + * Track selection settings + * + * @param tracksSetting List of tracks. + * @param modifier The [Modifier] to apply to this screen. + * @param onResetClick The action to perform when clicking on the reset button. + * @param onDisabledClick The action to perform when clicking on the disable button. + * @param onTrackClick The action to perform when clicking on a track. + */ +@Composable +fun TrackSelectionSettings( + tracksSetting: TracksSettingItem, + modifier: Modifier = Modifier, + onResetClick: () -> Unit, + onDisabledClick: () -> Unit, + onTrackClick: (track: Track) -> Unit +) { + val itemModifier = Modifier.fillMaxWidth() + LazyColumn(modifier = modifier) { + item { + ListItem( + modifier = itemModifier + .minimumInteractiveComponentSize() + .clickable { onResetClick() }, + headlineContent = { + Text( + text = stringResource(R.string.reset_to_default) + ) + } + ) + HorizontalDivider() + } + item { + SettingsOption( + modifier = itemModifier, + selected = tracksSetting.disabled, + onClick = onDisabledClick, + content = { + Text(text = stringResource(R.string.disabled)) + } + ) + HorizontalDivider() + } + items(tracksSetting.tracks) { track -> + val format = track.format + SettingsOption( + modifier = itemModifier, + selected = track.isSelected, + enabled = track.isSupported && !format.isForced(), + onClick = { + onTrackClick(track) + }, + content = { + when (track) { + is AudioTrack -> { + val text = buildString { + append(format.displayName) + + if (format.bitrate > Format.NO_VALUE) { + append(" @%1\$.2f Mbps".format(format.bitrate / 1_000_000f)) + } + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = text) + if (format.hasAccessibilityRoles()) { + Icon( + imageVector = Icons.Filled.HearingDisabled, + contentDescription = "AD", + modifier = Modifier.padding(start = MaterialTheme.paddings.small), + ) + } + } + } + + is VideoTrack -> { + val text = buildString { + append(format.width) + append("x") + append(format.height) + + if (format.bitrate > Format.NO_VALUE) { + append(" @%1\$.2f Mbps".format(format.bitrate / 1_000_000f)) + } + } + + Text(text = text) + } + + else -> { + if (format.hasAccessibilityRoles()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = format.displayName) + Icon( + imageVector = Icons.Default.HearingDisabled, + contentDescription = "Hearing disabled", + modifier = Modifier.padding(start = MaterialTheme.paddings.small), + ) + } + } else { + Text(text = format.displayName) + } + } + } + } + ) + } + item { + HorizontalDivider() + } + } +} + +@Preview +@Composable +private fun TextTrackSelectionPreview() { + // Track are group by language. + val textTrackFr1 = Format.Builder() + .setLabel("FR1") + .setId("subtitle:0") + .setLanguage("fr") + .build() + val textTrackEn1 = Format.Builder() + .setLabel("EN1") + .setId("subtitle:1") + .setLanguage("en") + .build() + val dummyListTrack = listOfNotNull( + Track( + group = Tracks.Group(TrackGroup("fr", textTrackFr1), false, intArrayOf(C.FORMAT_HANDLED), booleanArrayOf(true)), + groupIndex = 0, + trackIndexInGroup = 0, + ), + Track( + group = Tracks.Group(TrackGroup("en", textTrackEn1), false, intArrayOf(C.FORMAT_HANDLED), booleanArrayOf(false)), + groupIndex = 0, + trackIndexInGroup = 0, + ), + ) + PillarboxTheme { + TrackSelectionSettings( + tracksSetting = TracksSettingItem( + title = stringResource(R.string.subtitles), + tracks = dummyListTrack, + disabled = false + ), + onResetClick = {}, + onDisabledClick = {}, + onTrackClick = {}, + ) + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensions.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensions.kt index 18617b6d3..b5deddf87 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensions.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensions.kt @@ -5,7 +5,6 @@ package ch.srgssr.pillarbox.player.tracks import android.content.Context -import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import ch.srgssr.pillarbox.player.extension.defaultAudioTrack @@ -17,47 +16,8 @@ import ch.srgssr.pillarbox.player.extension.disableVideoTrack import ch.srgssr.pillarbox.player.extension.enableAudioTrack import ch.srgssr.pillarbox.player.extension.enableTextTrack import ch.srgssr.pillarbox.player.extension.enableVideoTrack -import ch.srgssr.pillarbox.player.extension.isVideoTrackDisabled import ch.srgssr.pillarbox.player.extension.setTrackOverride -/** - * All the supported video qualities for the currently played [MediaItem][androidx.media3.common.MediaItem]. - */ -val Player.videoQualities: List - get() { - val isVideoTrackDisabled = trackSelectionParameters.isVideoTrackDisabled - val filteredVideoTracks = currentTracks.videoTracks - .filter { isVideoTrackDisabled || it.group.isSelected } - .distinctBy { it.format.height } - .sortedByDescending { it.format.height } - .filter { it.format.height > 0 && it.format.width > 0 } - - val preferredFormat = if (isVideoTrackDisabled) { - null - } else { - val maxVideoHeight = trackSelectionParameters.maxVideoHeight.takeIf { it != Int.MAX_VALUE } - val maxVideoWidth = trackSelectionParameters.maxVideoWidth.takeIf { it != Int.MAX_VALUE } - - filteredVideoTracks - .filter { it.isSelected } - .firstOrNull { videoTrack -> - val format = videoTrack.format - val expectedHeight = maxVideoHeight ?: format.height - val expectedWidth = maxVideoWidth ?: format.width - - format.height <= expectedHeight && format.width <= expectedWidth - } - ?.format - } - - return filteredVideoTracks.map { videoTrack -> - VideoQuality( - format = videoTrack.format, - isSelected = videoTrack.format == preferredFormat, - ) - } - } - /** * Select the provided [track]. * @@ -69,18 +29,6 @@ fun Player.selectTrack(track: Track) { setTrackOverride(TrackSelectionOverride(trackGroup, track.trackIndexInGroup)) } -/** - * Select the max video quality that this [Player] should try to play. - * - * @param videoQuality The max video quality. - */ -fun Player.selectMaxVideoQuality(videoQuality: VideoQuality) { - trackSelectionParameters = trackSelectionParameters.buildUpon() - .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false) - .setMaxVideoSize(videoQuality.width, videoQuality.height) - .build() -} - /** * Enable the audio track. */ diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/Track.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/Track.kt index 7017bee6a..6f00baa3b 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/Track.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/Track.kt @@ -34,6 +34,12 @@ sealed class Track( val isSelected: Boolean get() = group.isTrackSelected(trackIndexInGroup) + /** + * `true` if this [Track] is supported, `false` otherwise. + */ + val isSupported: Boolean + get() = group.isTrackSupported(trackIndexInGroup) + companion object { /** * Converts the track at index [trackIndexInGroup] from the provided [group] into a [Track]. diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/VideoQuality.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/VideoQuality.kt deleted file mode 100644 index 2d9f6f3e2..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracks/VideoQuality.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracks - -import androidx.annotation.Px -import androidx.media3.common.Format - -/** - * Represent a video quality. - * - * @param format The [Format] containing information about this video quality. - * @property isSelected `true` if this video quality is selected, `false` otherwise. - */ -data class VideoQuality( - private val format: Format, - val isSelected: Boolean, -) { - /** - * The height of this video quality, in pixels. - */ - @get:Px - val height: Int - get() = format.height - - /** - * The width of this video quality, in pixels. - */ - @get:Px - val width: Int - get() = format.width -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensionsTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensionsTest.kt deleted file mode 100644 index c8e02b907..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracks/PlayerExtensionsTest.kt +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracks - -import android.content.Context -import androidx.media3.common.C -import androidx.media3.common.Format -import androidx.media3.common.MimeTypes -import androidx.media3.common.TrackGroup -import androidx.media3.common.TrackSelectionParameters -import androidx.media3.common.Tracks -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.PillarboxExoPlayer -import io.mockk.every -import io.mockk.mockk -import org.junit.runner.RunWith -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@RunWith(AndroidJUnit4::class) -class PlayerExtensionsTest { - private val context = ApplicationProvider.getApplicationContext() - private val singleVideoTrackGroup = Tracks( - listOf( - // Video tracks - Tracks.Group( - TrackGroup( - Format.Builder() - .setId("1_V_video_1") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(2001694) - .setCodecs("avc1.4D401F") - .setWidth(960) - .setHeight(540) - .build(), - Format.Builder() - .setId("1_V_video_2") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(500390) - .setCodecs("avc1.4D401F") - .setWidth(484) - .setHeight(272) - .build(), - Format.Builder() - .setId("1_V_video_3") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(6003201) - .setCodecs("avc1.640029") - .setWidth(1920) - .setHeight(1080) - .build(), - Format.Builder() - .setId("1_V_video_4") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(1201016) - .setCodecs("avc1.4D401F") - .setWidth(640) - .setHeight(360) - .build(), - Format.Builder() - .setId("1_V_video_5") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(3502299) - .setCodecs("avc1.4D401F") - .setWidth(1280) - .setHeight(720) - .build(), - // Duplicated format with id=1_V_video_5 - Format.Builder() - .setId("1_V_video_6") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(3502299) - .setCodecs("avc1.4D401F") - .setWidth(1280) - .setHeight(720) - .build(), - // TrickPlay format - Format.Builder() - .setId("1_V_video_7") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setPeakBitrate(23000) - .setCodecs("avc1.42C00D") - .setWidth(224) - .setHeight(100) - .setRoleFlags(C.ROLE_FLAG_TRICK_PLAY) - .build(), - // Track without resolution - Format.Builder() - .setId("1_V_video_8") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setPeakBitrate(23000) - .setCodecs("avc1.42C00D") - .setRoleFlags(C.ROLE_FLAG_TRICK_PLAY) - .build(), - ), - true, - IntArray(8) { C.FORMAT_HANDLED }, - BooleanArray(8) { true }, - ), - // Audio tracks - Tracks.Group( - TrackGroup( - Format.Builder() - .setId("5_A_audio_fra_1") - .setSampleMimeType(MimeTypes.AUDIO_AAC) - .setContainerMimeType(MimeTypes.AUDIO_MP4) - .setPeakBitrate(128000) - .setCodecs("mp4a.40.2") - .setSampleRate(48000) - .setLanguage("fr") - .build(), - ), - true, - intArrayOf(C.FORMAT_HANDLED), - booleanArrayOf(true), - ), - // Text tracks - Tracks.Group( - TrackGroup( - Format.Builder() - .setId("fr") - .setSampleMimeType(MimeTypes.TEXT_VTT) - .setContainerMimeType(MimeTypes.TEXT_VTT) - .setPeakBitrate(0) - .setLanguage("text-fra-sdh") - .build(), - ), - true, - intArrayOf(C.FORMAT_HANDLED), - booleanArrayOf(false), - ), - // Unsupported track type - Tracks.Group( - TrackGroup( - Format.Builder().setSampleMimeType(MimeTypes.IMAGE_WEBP).build(), - ), - true, - intArrayOf(C.FORMAT_HANDLED), - booleanArrayOf(true), - ), - ) - ) - private val doubleVideoTrackGroup = Tracks( - listOf( - Tracks.Group( - TrackGroup( - Format.Builder() - .setId("video_eng=401000") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(401000) - .setCodecs("avc1.42C00D") - .setWidth(224) - .setHeight(100) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - Format.Builder() - .setId("video_eng=751000") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(751000) - .setCodecs("avc1.42C016") - .setWidth(448) - .setHeight(200) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - Format.Builder() - .setId("video_eng=1001000") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(1001000) - .setCodecs("avc1.4D401F") - .setWidth(784) - .setHeight(350) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - Format.Builder() - .setId("video_eng=1501000") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(1501000) - .setCodecs("avc1.640028") - .setWidth(1680) - .setHeight(750) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - Format.Builder() - .setId("video_eng=2200000") - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(2200000) - .setCodecs("avc1.640028") - .setWidth(1680) - .setHeight(750) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - ), - true, - intArrayOf(C.FORMAT_HANDLED, C.FORMAT_HANDLED, C.FORMAT_HANDLED, C.FORMAT_HANDLED, C.FORMAT_HANDLED), - booleanArrayOf(true, true, true, true, true), - ), - Tracks.Group( - TrackGroup( - Format.Builder() - .setId("video_eng_1=902000") - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(902000) - .setCodecs("hvc1.1.6.L150.90") - .setWidth(1680) - .setHeight(750) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - Format.Builder() - .setId("video_eng_1=1161000") - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(1161000) - .setCodecs("hvc1.1.6.L150.90") - .setWidth(2576) - .setHeight(1150) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - Format.Builder() - .setId("video_eng_1=1583000") - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setContainerMimeType(MimeTypes.VIDEO_MP4) - .setPeakBitrate(1583000) - .setCodecs("hvc1.1.6.L150.90") - .setWidth(3360) - .setHeight(1500) - .setFrameRate(24f) - .setLanguage("en") - .setRoleFlags(C.ROLE_FLAG_MAIN) - .build(), - ), - true, - intArrayOf(C.FORMAT_HANDLED, C.FORMAT_HANDLED, C.FORMAT_HANDLED), - booleanArrayOf(false, false, false), - ), - ) - ) - - private lateinit var player: PillarboxExoPlayer - - @BeforeTest - fun setUp() { - player = mockk() - } - - @Test - fun `video qualities, no tracks, no preferred video size`() { - every { player.currentTracks } returns Tracks.EMPTY - every { player.trackSelectionParameters } returns TrackSelectionParameters.getDefaults(context) - - val videoQualities = player.videoQualities - - assertTrue(videoQualities.isEmpty()) - } - - @Test - fun `video qualities, no tracks, with preferred video size`() { - every { player.currentTracks } returns Tracks.EMPTY - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setMaxVideoSize(1280, 720) - .build() - - val videoQualities = player.videoQualities - - assertTrue(videoQualities.isEmpty()) - } - - @Test - fun `video qualities, with tracks, disabled video track`() { - every { player.currentTracks } returns singleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) - .build() - - val videoQualities = player.videoQualities - - assertEquals(5, videoQualities.size) - assertEquals(listOf(1920, 1280, 960, 640, 484), videoQualities.map { it.width }) - assertEquals(listOf(1080, 720, 540, 360, 272), videoQualities.map { it.height }) - assertEquals(listOf(false, false, false, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with tracks, no preferred video size`() { - every { player.currentTracks } returns singleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.getDefaults(context) - - val videoQualities = player.videoQualities - - assertEquals(5, videoQualities.size) - assertEquals(listOf(1920, 1280, 960, 640, 484), videoQualities.map { it.width }) - assertEquals(listOf(1080, 720, 540, 360, 272), videoQualities.map { it.height }) - assertEquals(listOf(true, false, false, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with tracks, with matched preferred video size`() { - every { player.currentTracks } returns singleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setMaxVideoSize(1280, 720) - .build() - - val videoQualities = player.videoQualities - - assertEquals(5, videoQualities.size) - assertEquals(listOf(1920, 1280, 960, 640, 484), videoQualities.map { it.width }) - assertEquals(listOf(1080, 720, 540, 360, 272), videoQualities.map { it.height }) - assertEquals(listOf(false, true, false, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with tracks, with unmatched preferred video size`() { - every { player.currentTracks } returns singleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setMaxVideoSize(1024, 600) - .build() - - val videoQualities = player.videoQualities - - assertEquals(5, videoQualities.size) - assertEquals(listOf(1920, 1280, 960, 640, 484), videoQualities.map { it.width }) - assertEquals(listOf(1080, 720, 540, 360, 272), videoQualities.map { it.height }) - assertEquals(listOf(false, false, true, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with two video groups, disabled video track`() { - every { player.currentTracks } returns doubleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) - .build() - - val videoQualities = player.videoQualities - - assertEquals(6, videoQualities.size) - assertEquals(listOf(3360, 2576, 1680, 784, 448, 224), videoQualities.map { it.width }) - assertEquals(listOf(1500, 1150, 750, 350, 200, 100), videoQualities.map { it.height }) - assertEquals(listOf(false, false, false, false, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with two video groups, no preferred video size`() { - every { player.currentTracks } returns doubleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.getDefaults(context) - - val videoQualities = player.videoQualities - - assertEquals(4, videoQualities.size) - assertEquals(listOf(1680, 784, 448, 224), videoQualities.map { it.width }) - assertEquals(listOf(750, 350, 200, 100), videoQualities.map { it.height }) - assertEquals(listOf(true, false, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with two video groups, with matched preferred video size`() { - every { player.currentTracks } returns doubleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setMaxVideoSize(2576, 1150) - .build() - - val videoQualities = player.videoQualities - - assertEquals(4, videoQualities.size) - assertEquals(listOf(1680, 784, 448, 224), videoQualities.map { it.width }) - assertEquals(listOf(750, 350, 200, 100), videoQualities.map { it.height }) - assertEquals(listOf(true, false, false, false), videoQualities.map { it.isSelected }) - } - - @Test - fun `video qualities, with two video groups, with unmatched preferred video size`() { - every { player.currentTracks } returns doubleVideoTrackGroup - every { player.trackSelectionParameters } returns TrackSelectionParameters.Builder(context) - .setMaxVideoSize(1024, 600) - .build() - - val videoQualities = player.videoQualities - - assertEquals(4, videoQualities.size) - assertEquals(listOf(1680, 784, 448, 224), videoQualities.map { it.width }) - assertEquals(listOf(750, 350, 200, 100), videoQualities.map { it.height }) - assertEquals(listOf(false, true, false, false), videoQualities.map { it.isSelected }) - } -}