diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 05dd6d27..68653272 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.WholphinApplication import com.github.damontecres.wholphin.services.UpdateChecker +import com.github.damontecres.wholphin.ui.formatMaxBitrate import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.ConditionalPreferences import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup @@ -345,7 +346,7 @@ sealed interface AppPreference { const val MEGA_BIT = 1024 * 1024L const val DEFAULT_BITRATE = 100 * MEGA_BIT - private val bitrateValues = + val bitrateValues = listOf( 500 * 1024L, 750 * 1024L, @@ -378,11 +379,7 @@ sealed interface AppPreference { summarizer = { value -> if (value != null) { val v = bitrateValues.getOrNull(value.toInt()) ?: DEFAULT_BITRATE - if (v < MEGA_BIT) { - "${v / 1024} kbps" - } else { - "${v / MEGA_BIT} Mbps" - } + formatMaxBitrate(v) } else { null } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt index 4293c422..c7cbaf44 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.WholphinApplication +import com.github.damontecres.wholphin.preferences.AppPreference.Companion.MEGA_BIT import com.github.damontecres.wholphin.ui.setup.ServerVersionSupported import org.jellyfin.sdk.model.ServerVersion import org.jellyfin.sdk.model.api.BaseItemDto @@ -208,6 +209,13 @@ fun formatBytes( fun formatBitrate(bitrate: Int) = formatBytes(bitrate.toLong(), byteRateSuffixes, 1000) +fun formatMaxBitrate(bitrate: Long) = + if (bitrate < MEGA_BIT) { + "${bitrate / 1024} kbps" + } else { + "${bitrate / MEGA_BIT} Mbps" + } + @get:StringRes val MediaSegmentType.stringRes: Int get() = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt index 491498cb..3165246f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt @@ -42,8 +42,10 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.TrackIndex +import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent +import com.github.damontecres.wholphin.ui.formatMaxBitrate import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialog import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialogItem @@ -60,6 +62,7 @@ enum class PlaybackDialogType { PLAYBACK_SPEED, VIDEO_SCALE, SUBTITLE_DELAY, + BITRATE, } data class PlaybackSettings( @@ -73,6 +76,7 @@ data class PlaybackSettings( val subtitleDelay: Duration, val hasSubtitleDownloadPermission: Boolean, val playbackSpeedEnabled: Boolean, + val maxBitrate: Long, ) /** @@ -162,6 +166,13 @@ fun PlaybackDialog( ), ) } + add( + BottomDialogItem( + data = PlaybackDialogType.BITRATE, + headline = stringResource(R.string.max_bitrate), + supporting = remember(settings.maxBitrate) { formatMaxBitrate(settings.maxBitrate) }, + ), + ) add( BottomDialogItem( data = PlaybackDialogType.DEBUG, @@ -271,6 +282,29 @@ fun PlaybackDialog( } } } + + PlaybackDialogType.BITRATE -> { + val choices = + AppPreference.bitrateValues.map { bitrate -> + BottomDialogItem( + data = bitrate, + headline = formatMaxBitrate(bitrate), + supporting = null, + ) + } + BottomDialog( + choices = choices, + currentChoice = choices.firstOrNull { it.data == settings.maxBitrate }, + onDismissRequest = { + onControllerInteraction.invoke() + onDismissRequest.invoke() + }, + onSelectChoice = { _, choice -> + onPlaybackActionClick.invoke(PlaybackAction.ChangeBitrate(choice.data)) + }, + gravity = leftGravity, + ) + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index 483083f2..86d64651 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -322,6 +322,10 @@ fun PlaybackPageContent( player.seekToPrevious() } } + + is PlaybackAction.ChangeBitrate -> { + viewModel.changeMaxBitrate(it.maxBitrate) + } } } @@ -752,6 +756,7 @@ fun PlaybackPageContent( // TODO Passing through audio prevents changing playback speed // See https://github.com/damontecres/Wholphin/issues/164 playbackSpeedEnabled = playerBackend == PlayerBackend.MPV || state.currentPlayback?.audioDecoder != null, + maxBitrate = state.maxBitrate, ), onDismissRequest = { playbackDialog = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt index b42b7151..cca3a0d0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackState.kt @@ -24,6 +24,7 @@ data class PlaybackState( val nextUp: BaseItem? = null, val playlistIndex: Int = 0, val playlist: Playlist = Playlist(emptyList()), + val maxBitrate: Long = -1L, ) { val hasNext: Boolean get() = (playlistIndex + 1) < playlist.items.size val hasPrevious: Boolean get() = playlistIndex > 0 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index f89f3af1..e51ccfbb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -292,8 +292,16 @@ class PlaybackViewModel */ private suspend fun init() { musicService.stop() - _state.update { it.copy(nextUp = null) } this.preferences = userPreferencesService.getCurrent() + val maxBitrate = + preferences.appPreferences.playbackPreferences.maxBitrate + .takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE + _state.update { + it.copy( + nextUp = null, + maxBitrate = maxBitrate, + ) + } if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) { addCloseable { refreshRateService.resetRefreshRate() } } @@ -628,6 +636,7 @@ class PlaybackViewModel positionMs: Long = 0, enableDirectPlay: Boolean = !this.forceTranscoding, enableDirectStream: Boolean = !this.forceTranscoding, + maxBitrate: Long = this.state.value.maxBitrate, ): Unit = withContext(WholphinDispatchers.IO) { val itemId = item.id @@ -638,7 +647,8 @@ class PlaybackViewModel state.value.currentPlayback?.let { currentPlayback -> if (currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY && - enableDirectPlay + enableDirectPlay && + state.value.maxBitrate == maxBitrate ) { val wasSuccessful = changeStreamsDirectPlay( @@ -651,18 +661,17 @@ class PlaybackViewModel } Timber.i( - "changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s", + "changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s, maxBitrate=%s", itemId, audioIndex, subtitleIndex, enableDirectPlay, enableDirectStream, positionMs, + maxBitrate, ) + _state.update { it.copy(maxBitrate = maxBitrate) } - val maxBitrate = - preferences.appPreferences.playbackPreferences.maxBitrate - .takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE val response by api.mediaInfoApi .getPostedPlaybackInfo( @@ -1021,6 +1030,26 @@ class PlaybackViewModel } } + fun changeMaxBitrate(newMaxBitrate: Long) { + viewModelScope.launchIO { + val currentPlayback = state.value.currentPlayback + if (currentPlayback != null) { + Timber.d("Changing bitrate to %s", newMaxBitrate) + changeStreams( + item = currentItem.item, + sourceId = currentPlayback.mediaSourceInfo.id, + audioIndex = currentPlayback.audioIndex, + subtitleIndex = currentPlayback.subtitleIndex, + positionMs = onMain { player.currentPosition }, + enableDirectPlay = true, + maxBitrate = newMaxBitrate, + ) + } else { + Timber.w("Trying to change bitrate, but currentPlayback is null") + } + } + } + internal suspend fun saveTrackSelection( trackIndex: Int, type: MediaStreamType, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt index ba77d8b7..45fcdd4c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.shape.RoundedCornerShape @@ -122,6 +123,10 @@ sealed interface PlaybackAction { data object Previous : PlaybackAction data object Next : PlaybackAction + + data class ChangeBitrate( + val maxBitrate: Long, + ) : PlaybackAction } /** @@ -561,12 +566,19 @@ fun BottomDialog( gravity: Int, currentChoice: BottomDialogItem? = null, ) { + val initialIndex = + remember(choices, currentChoice) { choices.indexOfFirstOrNull { it == currentChoice } } ?: 0 + val listState = + rememberLazyListState( + initialFirstVisibleItemIndex = initialIndex, + ) val focusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } } + val bringIntoViewRequesters = + remember(choices.size) { List(choices.size) { BringIntoViewRequester() } } if (currentChoice != null) { LaunchedEffect(Unit) { - choices.indexOfFirstOrNull { it == currentChoice }?.let { - focusRequesters.getOrNull(it)?.tryRequestFocus() - } + bringIntoViewRequesters.getOrNull(initialIndex)?.bringIntoView() + focusRequesters.getOrNull(initialIndex)?.tryRequestFocus() } } // TODO enforcing a width ends up ignore the gravity @@ -591,6 +603,7 @@ fun BottomDialog( ), ) { LazyColumn( + state = listState, modifier = Modifier .fillMaxWidth() @@ -624,7 +637,10 @@ fun BottomDialog( } }, interactionSource = interactionSource, - modifier = Modifier.focusRequester(focusRequesters[index]), + modifier = + Modifier + .bringIntoViewRequester(bringIntoViewRequesters[index]) + .focusRequester(focusRequesters[index]), ) } }