diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 75c7fbf7..f1091839 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -88,7 +88,7 @@ import java.util.UUID data class DialogParams( val fromLongClick: Boolean, val title: String, - val items: List, + val items: List, ) sealed interface DialogItemEntry @@ -675,8 +675,9 @@ fun chooseStream( type: MediaStreamType, preferredSubtitleLanguage: String?, onClick: (Int) -> Unit, -): DialogParams = - DialogParams( +): DialogParams { + val filteredStreams = streams.filter { it.type == type } + return DialogParams( fromLongClick = false, title = resources.getString(R.string.choose_stream, resources.getString(resourceFor(type))), items = @@ -709,10 +710,12 @@ fun chooseStream( onClick = { onClick.invoke(TrackIndex.ONLY_FORCED) }, ), ) + if (filteredStreams.isNotEmpty()) { + add(DialogItemDivider) + } } addAll( - streams - .filter { it.type == type } + filteredStreams .let { if (type == MediaStreamType.SUBTITLE && preferredSubtitleLanguage.isNotNullOrBlank()) { it.sortedByDescending { it.language != null && it.language == preferredSubtitleLanguage } @@ -728,7 +731,9 @@ fun chooseStream( }, headlineContent = { Text( - text = simpleStream.streamTitle ?: simpleStream.displayTitle, + text = + simpleStream.streamTitle + ?: simpleStream.displayTitle, ) }, supportingContent = { @@ -740,3 +745,4 @@ fun chooseStream( ) }, ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt index 40b5e89b..380901a1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt @@ -4,7 +4,10 @@ import androidx.core.net.toUri import androidx.media3.common.MediaMetadata import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter +import com.github.damontecres.wholphin.ui.isNotNullOrBlank import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.MediaSourceInfo +import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.TrickplayInfo import org.jellyfin.sdk.model.extensions.ticks @@ -26,6 +29,40 @@ data class CurrentMediaInfo( } } +/** + * Gets a sorted list of subtitle streams in a [MediaSourceInfo] + */ +internal fun PlaybackViewModel.getSubtitleStreams(mediaSource: MediaSourceInfo): List { + val subtitleLanguagePreference = + serverRepository.currentUserDto + ?.configuration + ?.subtitleLanguagePreference + return mediaSource.mediaStreams + ?.filter { it.type == MediaStreamType.SUBTITLE } + .let { + if (subtitleLanguagePreference.isNotNullOrBlank()) { + it?.sortedByDescending { it.language != null && subtitleLanguagePreference == it.language } + } else { + it + } + }?.map { + // TODO should use a string provider instead + SimpleMediaStream.from(context.resources, it, true) + }.orEmpty() +} + +/** + * Gets a list of audio streams in a [MediaSourceInfo] + */ +internal fun PlaybackViewModel.getAudioStreams(mediaSource: MediaSourceInfo): List = + mediaSource.mediaStreams + ?.filter { it.type == MediaStreamType.AUDIO } + ?.map { + SimpleMediaStream.from(context.resources, it, true) + } +// ?.sortedWith(compareBy { it.language }.thenByDescending { it.channels }) + .orEmpty() + fun BaseItem.toMediaMetadata(imageUrl: String?): MediaMetadata = MediaMetadata .Builder() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt index b0f51e56..54663af6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt @@ -1,11 +1,14 @@ package com.github.damontecres.wholphin.ui.playback import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.util.TrackSupport import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.PlayMethod import org.jellyfin.sdk.model.api.TranscodingInfo +import org.jellyfin.sdk.model.serializer.toUUIDOrNull +import java.util.UUID import kotlin.time.Duration /** @@ -25,4 +28,12 @@ data class CurrentPlayback( val audioDecoder: String? = null, val transcodeInfo: TranscodingInfo? = null, val subtitleDelay: Duration = Duration.ZERO, -) + val audioIndex: Int = TrackIndex.UNSPECIFIED, + val subtitleIndex: Int = TrackIndex.UNSPECIFIED, +) { + val audioIndexEnabled = audioIndex >= 0 + val subtitleIndexEnabled = subtitleIndex >= 0 + + val itemId: UUID get() = item.id + val sourceId: UUID? = mediaSourceInfo.id?.toUUIDOrNull() +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt index c9845191..2ec02410 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt @@ -1,21 +1,15 @@ package com.github.damontecres.wholphin.ui.playback import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Star -import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -25,11 +19,9 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp @@ -37,15 +29,13 @@ import androidx.tv.material3.Icon import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.abbreviateNumber -import com.github.damontecres.wholphin.ui.components.DialogItem -import com.github.damontecres.wholphin.ui.components.DialogItemDivider import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.ErrorMessage -import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus import org.jellyfin.sdk.model.api.RemoteSubtitleInfo @@ -55,53 +45,41 @@ fun DownloadSubtitlesContent( language: String, onSearch: (String) -> Unit, onClickDownload: (RemoteSubtitleInfo) -> Unit, - onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ) { when (val s = state) { SubtitleSearchStatus.Inactive -> {} SubtitleSearchStatus.Searching -> { - Wrapper { - Text( - text = stringResource(R.string.searching), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - } + Text( + text = stringResource(R.string.searching), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = modifier, + ) } SubtitleSearchStatus.Downloading -> { - Wrapper { - Text( - text = stringResource(R.string.downloading), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - } + Text( + text = stringResource(R.string.downloading), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = modifier, + ) } is SubtitleSearchStatus.Error -> { - Wrapper { ErrorMessage(null, s.ex, modifier) } + ErrorMessage(null, s.ex, modifier) } is SubtitleSearchStatus.Success -> { - val dialogItems = convertRemoteSubtitles(s.options, onClickDownload) val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } - val elevatedContainerColor = - MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = - modifier - .graphicsLayer { - this.clip = true - this.shape = RoundedCornerShape(28.0.dp) - }.drawBehind { drawRect(color = elevatedContainerColor) } - .padding(PaddingValues(24.dp)), + modifier = modifier, ) { Text( text = stringResource(R.string.search_and_download_subtitles), @@ -128,51 +106,35 @@ fun DownloadSubtitlesContent( onSearch(lang) }, ), - // onKeyboardAction = { -// onSearch(lang.text.toString()) -// }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Search, ), ) } - if (dialogItems.isEmpty()) { + if (s.options.isEmpty()) { Text( text = stringResource(R.string.no_subtitles_found), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, ) } else { + val focusRequesters = + remember(s.options.size) { List(s.options.size) { FocusRequester() } } + LaunchedEffect(Unit) { + focusRequesters.firstOrNull()?.tryRequestFocus() + } LazyColumn( modifier = Modifier, ) { - itemsIndexed(dialogItems) { index, item -> - when (item) { - is DialogItemDivider -> { - HorizontalDivider(Modifier.height(16.dp)) - } - - is DialogItem -> { - ListItem( - selected = false, - enabled = item.enabled, - onClick = { - item.onClick.invoke() - }, - headlineContent = item.headlineContent, - overlineContent = item.overlineContent, - supportingContent = item.supportingContent, - leadingContent = item.leadingContent, - trailingContent = item.trailingContent, - modifier = - Modifier.ifElse( - index == 0, - Modifier.focusRequester(focusRequester), - ), - ) - } - } + itemsIndexed(s.options) { index, item -> + SubtitleInfo( + subtitle = item, + onClick = onClickDownload, + modifier = + Modifier + .focusRequester(focusRequesters[index]), + ) } } } @@ -182,54 +144,56 @@ fun DownloadSubtitlesContent( } @Composable -private fun Wrapper(content: @Composable BoxScope.() -> Unit) { - val elevatedContainerColor = - MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) - Box( - modifier = - Modifier - .graphicsLayer { - this.clip = true - this.shape = RoundedCornerShape(28.0.dp) - }.drawBehind { drawRect(color = elevatedContainerColor) } - .padding(PaddingValues(24.dp)), - content = content, - ) -} - -fun convertRemoteSubtitles( - options: List, +fun SubtitleInfo( + subtitle: RemoteSubtitleInfo, onClick: (RemoteSubtitleInfo) -> Unit, -) = options.map { op -> - DialogItem( - onClick = { onClick.invoke(op) }, + modifier: Modifier = Modifier, +) { + ListItem( + selected = false, + onClick = { onClick.invoke(subtitle) }, headlineContent = { Text( - text = op.name ?: "", + text = subtitle.name ?: "", ) }, + overlineContent = + if (subtitle.isHashMatch == true) { + { + Text(stringResource(R.string.hash_matches)) + } + } else { + null + }, supportingContent = { - val strings = - buildList { - op.providerName?.let(::add) - op.threeLetterIsoLanguageName?.let(::add) - if (op.forced == true) { - add(stringResource(R.string.forced_track)) - } - add( - pluralStringResource( - R.plurals.downloads, - op.downloadCount ?: 0, - abbreviateNumber(op.downloadCount ?: 0), - ), - ) + val resources = LocalResources.current + val details = + remember(resources, subtitle) { + val strings = + buildList { + subtitle.providerName?.let(::add) + if (subtitle.forced == true) { + add(resources.getString(R.string.forced_track)) + } + if (subtitle.hearingImpaired == true) { + add(resources.getString(R.string.subtitles_hearing_impaired)) + } + add( + resources.getQuantityString( + R.plurals.downloads, + subtitle.downloadCount ?: 0, + abbreviateNumber(subtitle.downloadCount ?: 0), + ), + ) + } + strings.joinToString(" - ") } Text( - text = strings.joinToString(" - "), + text = details, ) }, trailingContent = { - op.communityRating?.let { rating -> + subtitle.communityRating?.let { rating -> Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, @@ -245,5 +209,43 @@ fun convertRemoteSubtitles( } } }, + modifier = modifier, ) } + +@PreviewTvSpec +@Composable +fun SubtitleInfoPreview() { + WholphinTheme { + Column(Modifier.width(400.dp)) { + SubtitleInfo( + onClick = {}, + subtitle = + RemoteSubtitleInfo( + name = "Filename.mkv", + providerName = "OpenSubs", + threeLetterIsoLanguageName = "eng", + forced = true, + hearingImpaired = false, + isHashMatch = true, + communityRating = 7.5f, + downloadCount = 10_500, + ), + ) + SubtitleInfo( + onClick = {}, + subtitle = + RemoteSubtitleInfo( + name = "Filename.mkv", + providerName = "OpenSubs", + threeLetterIsoLanguageName = "eng", + forced = false, + hearingImpaired = true, + isHashMatch = false, + communityRating = 7.5f, + downloadCount = 10_500, + ), + ) + } + } +} 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 c3bff78c..491498cb 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 @@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.playback import android.view.Gravity import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -11,15 +12,21 @@ 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.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalView @@ -42,6 +49,7 @@ import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialog import com.github.damontecres.wholphin.ui.playback.overlay.BottomDialogItem import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackAction import com.github.damontecres.wholphin.ui.tryRequestFocus +import kotlinx.coroutines.launch import kotlin.time.Duration enum class PlaybackDialogType { @@ -276,6 +284,24 @@ fun SubtitleChoiceBottomDialog( hasDownloadPermission: Boolean, currentChoice: Int? = null, ) { + val numberBefore = 2 + val initialChoice = + remember(choices) { + choices.indexOfFirstOrNull { it.index == currentChoice } + } + val scope = rememberCoroutineScope() + val listState = + rememberLazyListState( + initialFirstVisibleItemIndex = initialChoice?.plus(numberBefore) ?: 0, + ) + val firstFocusRequester = remember { FocusRequester() } + val lastFocusRequester = remember { FocusRequester() } + val choiceFocusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } } + LaunchedEffect(Unit) { + val focusRequester = + initialChoice?.let { choiceFocusRequesters.getOrNull(it) } ?: firstFocusRequester + focusRequester.tryRequestFocus() + } // TODO enforcing a width ends up ignore the gravity Dialog( onDismissRequest = onDismissRequest, @@ -298,6 +324,7 @@ fun SubtitleChoiceBottomDialog( ), ) { LazyColumn( + state = listState, modifier = Modifier .fillMaxWidth() @@ -307,8 +334,11 @@ fun SubtitleChoiceBottomDialog( horizontalAlignment = Alignment.CenterHorizontally, ) { item { + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() ListItem( selected = currentChoice == TrackIndex.DISABLED, + interactionSource = interactionSource, onClick = { onSelectChoice(TrackIndex.DISABLED) }, @@ -321,6 +351,19 @@ fun SubtitleChoiceBottomDialog( ) }, supportingContent = {}, + modifier = + Modifier + .focusRequester(firstFocusRequester) + .onKeyEvent { + if (focused && isUp(it) && it.type == KeyEventType.KeyDown) { + scope.launch { + listState.animateScrollToItem(Int.MAX_VALUE) + lastFocusRequester.tryRequestFocus() + } + return@onKeyEvent true + } + false + }, ) } item { @@ -339,6 +382,9 @@ fun SubtitleChoiceBottomDialog( }, supportingContent = {}, ) + if (choices.isNotEmpty()) { + HorizontalDivider() + } } itemsIndexed(choices) { index, choice -> val interactionSource = remember { MutableInteractionSource() } @@ -359,10 +405,13 @@ fun SubtitleChoiceBottomDialog( if (choice.streamTitle != null) Text(choice.displayTitle) }, interactionSource = interactionSource, + modifier = Modifier.focusRequester(choiceFocusRequesters[index]), ) } item { HorizontalDivider() + val interactionSource = remember { MutableInteractionSource() } + val focused by interactionSource.collectIsFocusedAsState() ListItem( selected = false, enabled = hasDownloadPermission, @@ -374,6 +423,20 @@ fun SubtitleChoiceBottomDialog( ) }, supportingContent = {}, + interactionSource = interactionSource, + modifier = + Modifier + .focusRequester(lastFocusRequester) + .onKeyEvent { + if (focused && isDown(it) && it.type == KeyEventType.KeyDown) { + scope.launch { + listState.animateScrollToItem(0) + firstFocusRequester.tryRequestFocus() + } + return@onKeyEvent true + } + false + }, ) } } 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 6d22cd10..62eebd04 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 @@ -53,7 +53,6 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.core.view.children import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -75,6 +74,7 @@ import com.github.damontecres.wholphin.preferences.skipBackOnResume import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.LocalImageUrlService +import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.nav.Destination @@ -379,7 +379,13 @@ fun PlaybackPageContent( } } - if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L) { + val controlsVisible = + remember(controllerViewState.controlsVisible, playbackDialog, subtitleSearchState) { + controllerViewState.controlsVisible || + playbackDialog != null || + subtitleSearchState.status != SubtitleSearchStatus.Inactive + } + if (!controlsVisible && skipIndicatorDuration == 0L) { PauseIndicator( player = player, modifier = @@ -444,7 +450,7 @@ fun PlaybackPageContent( val subtitleVisible = skipIndicatorDuration == 0L && - state.currentItemPlayback?.subtitleIndexEnabled == true && + state.currentPlayback?.subtitleIndexEnabled == true && !presentationState.coverSurface AndroidView( @@ -629,28 +635,29 @@ fun PlaybackPageContent( } viewModel.cancelSubtitleSearch() } - Dialog( + BasicDialog( onDismissRequest = onDismissRequest, properties = DialogProperties( usePlatformDefaultWidth = false, ), ) { - DownloadSubtitlesContent( - state = subtitleSearchState.status, - language = subtitleSearchState.language, - onSearch = { lang -> - viewModel.searchForSubtitles(lang) - }, - onClickDownload = { - viewModel.downloadAndSwitchSubtitles(it.id, wasPlaying) - }, - onDismissRequest = onDismissRequest, - modifier = - Modifier - .widthIn(max = 640.dp) - .heightIn(max = 400.dp), - ) + Box(modifier = Modifier.padding(24.dp)) { + DownloadSubtitlesContent( + state = subtitleSearchState.status, + language = subtitleSearchState.language, + onSearch = { lang -> + viewModel.searchForSubtitles(lang) + }, + onClickDownload = { + viewModel.downloadAndSwitchSubtitles(it.id, wasPlaying) + }, + modifier = + Modifier + .widthIn(max = 640.dp) + .heightIn(max = 400.dp), + ) + } } } @@ -660,9 +667,9 @@ fun PlaybackPageContent( settings = PlaybackSettings( showDebugInfo = showDebugInfo, - audioIndex = state.currentItemPlayback?.audioIndex, + audioIndex = state.currentPlayback?.audioIndex, audioStreams = state.currentMediaInfo.audioStreams, - subtitleIndex = state.currentItemPlayback?.subtitleIndex, + subtitleIndex = state.currentPlayback?.subtitleIndex, subtitleStreams = state.currentMediaInfo.subtitleStreams, playbackSpeed = playbackSpeed, contentScale = contentScale, 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 2dc01d36..da55b98b 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 @@ -31,7 +31,6 @@ import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter -import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.TrackIndex @@ -65,7 +64,6 @@ import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings. import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekForward import com.github.damontecres.wholphin.ui.showToast -import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.PlaybackItemState @@ -142,7 +140,7 @@ class PlaybackViewModel private val playlistCreator: PlaylistCreator, private val itemPlaybackDao: ItemPlaybackDao, internal val serverRepository: ServerRepository, - private val itemPlaybackRepository: ItemPlaybackRepository, + internal val itemPlaybackRepository: ItemPlaybackRepository, private val playerFactory: PlayerFactory, private val datePlayedService: DatePlayedService, private val deviceInfo: DeviceInfo, @@ -405,7 +403,7 @@ class PlaybackViewModel } } - private fun updateCurrentPlayback(block: (CurrentPlayback?) -> CurrentPlayback?) { + internal fun updateCurrentPlayback(block: (CurrentPlayback?) -> CurrentPlayback?) { _state.update { it.copy(currentPlayback = block.invoke(it.currentPlayback)) } @@ -494,32 +492,9 @@ class PlaybackViewModel // Create the correct player for the media createPlayer(videoStream?.hdr == true, videoStream?.is4k == true) - val subtitleLanguagePreference = - serverRepository.currentUserDto - ?.configuration - ?.subtitleLanguagePreference - val subtitleStreams = - mediaSource.mediaStreams - ?.filter { it.type == MediaStreamType.SUBTITLE } - .let { - if (subtitleLanguagePreference.isNotNullOrBlank()) { - it?.sortedByDescending { it.language != null && subtitleLanguagePreference == it.language } - } else { - it - } - }?.map { - // TODO should use a string provider instead - SimpleMediaStream.from(context.resources, it, true) - }.orEmpty() - val audioStreams = - mediaSource.mediaStreams - ?.filter { it.type == MediaStreamType.AUDIO } - ?.map { - SimpleMediaStream.from(context.resources, it, true) - } -// ?.sortedWith(compareBy { it.language }.thenByDescending { it.channels }) - .orEmpty() + val subtitleStreams = getSubtitleStreams(mediaSource) + val audioStreams = getAudioStreams(mediaSource) val audioStream = streamChoiceService .chooseAudioStream( @@ -544,15 +519,6 @@ class PlaybackViewModel Timber.d("Selected mediaSource=${mediaSource.id}, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex") - val itemPlaybackToUse = - itemPlayback ?: ItemPlayback( - rowId = -1, - userId = -1, - itemId = base.id, - sourceId = if (!isLiveTv) mediaSource.id?.toUUIDOrNull() else null, - audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, - subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED, - ) val trickPlayInfo = item.data.trickplay ?.get(mediaSource.id) @@ -572,7 +538,10 @@ class PlaybackViewModel val chapters = Chapter.fromDto(base) _state.update { - it.copy(currentItemPlayback = itemPlaybackToUse) + it.copy( + currentItemPlayback = itemPlayback, + currentPlayback = null, + ) } updateCurrentMedia { CurrentMediaInfo( @@ -586,12 +555,11 @@ class PlaybackViewModel } withContext(WholphinDispatchers.Main) { changeStreams( - item, - itemPlaybackToUse, - audioIndex, - subtitleIndex, - if (positionMs > 0) positionMs else C.TIME_UNSET, - itemPlayback != null, // If it was passed in, then it was not queried from the database + item = item, + audioIndex = audioIndex, + subtitleIndex = subtitleIndex, + positionMs = if (positionMs > 0) positionMs else C.TIME_UNSET, + sourceId = mediaSource.id, enableDirectPlay = !forceTranscoding, enableDirectStream = !forceTranscoding, ) @@ -608,38 +576,34 @@ class PlaybackViewModel @OptIn(UnstableApi::class) internal suspend fun changeStreams( item: BaseItem, - currentItemPlayback: ItemPlayback = state.value.currentItemPlayback!!, + sourceId: String?, audioIndex: Int?, subtitleIndex: Int?, positionMs: Long = 0, - userInitiated: Boolean, enableDirectPlay: Boolean = !this.forceTranscoding, enableDirectStream: Boolean = !this.forceTranscoding, ): Unit = withContext(WholphinDispatchers.IO) { val itemId = item.id - val currentPlayback = state.value.currentPlayback - if (currentPlayback != null && - currentPlayback.item.id == item.id && - currentPlayback.playMethod == PlayMethod.DIRECT_PLAY && - enableDirectPlay - ) { - val wasSuccessful = - changeStreamsDirectPlay( - currentPlayback = currentPlayback, - currentItemPlayback = currentItemPlayback, - audioIndex = audioIndex, - subtitleIndex = subtitleIndex, - userInitiated = userInitiated, - ) - if (wasSuccessful) return@withContext + state.value.currentPlayback?.let { currentPlayback -> + if (currentPlayback.item.id == item.id && + currentPlayback.playMethod == PlayMethod.DIRECT_PLAY && + enableDirectPlay + ) { + val wasSuccessful = + changeStreamsDirectPlay( + currentPlayback = currentPlayback, + audioIndex = audioIndex, + subtitleIndex = subtitleIndex, + ) + if (wasSuccessful) return@withContext + } } Timber.i( - "changeStreams (%s): userInitiated=%s, audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s", + "changeStreams (%s): audioIndex=%s, subtitleIndex=%s, enableDirectPlay=%s, enableDirectStream=%s, positionMs=%s", itemId, - userInitiated, audioIndex, subtitleIndex, enableDirectPlay, @@ -668,7 +632,7 @@ class PlaybackViewModel maxAudioChannels = null, audioStreamIndex = audioIndex, subtitleStreamIndex = subtitleIndex, - mediaSourceId = currentItemPlayback.sourceId?.toServerString(), + mediaSourceId = sourceId, alwaysBurnInSubtitleWhenTranscoding = false, maxStreamingBitrate = maxBitrate.toInt(), enableDirectPlay = enableDirectPlay, @@ -680,6 +644,7 @@ class PlaybackViewModel ), ) if (response.errorCode != null) { + Timber.e("Error in PostedPlaybackInfo: %s", response.errorCode) _state.update { it.copy(loading = LoadingState.Error(response.errorCode?.serialName)) } return@withContext } @@ -782,6 +747,8 @@ class PlaybackViewModel playSessionId = response.playSessionId, liveStreamId = source.liveStreamId, mediaSourceInfo = source, + audioIndex = audioIndex ?: TrackIndex.DISABLED, + subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED, ) preferences.appPreferences.playbackPreferences.let { prefs -> @@ -802,7 +769,7 @@ class PlaybackViewModel player.removeListener(it) } - val playbackItemState = PlaybackItemState(playback, currentItemPlayback) + val playbackItemState = PlaybackItemState(playback) val activityListener = TrackActivityPlaybackListener( api = api, @@ -843,15 +810,17 @@ class PlaybackViewModel result.trackSelectionParameters } else { // Fall back to transcoding - Timber.w("Failed to select tracks, falling back to transcoding") + Timber.w( + "Failed to select tracks, falling back to transcoding: %s", + result, + ) viewModelScope.launchIO { changeStreams( item = item, - currentItemPlayback = currentItemPlayback, + sourceId = sourceId, audioIndex = audioIndex, subtitleIndex = subtitleIndex, positionMs = positionMs, - userInitiated = userInitiated, enableDirectPlay = false, enableDirectStream = true, ) @@ -874,10 +843,8 @@ class PlaybackViewModel @OptIn(UnstableApi::class) private suspend fun changeStreamsDirectPlay( currentPlayback: CurrentPlayback, - currentItemPlayback: ItemPlayback, audioIndex: Int?, subtitleIndex: Int?, - userInitiated: Boolean, ): Boolean = withContext(WholphinDispatchers.IO) { Timber.v("changeStreams direct play") @@ -912,29 +879,7 @@ class PlaybackViewModel } if (result.bothSelected) { onMain { player.trackSelectionParameters = result.trackSelectionParameters } - // TODO lots of duplicate code in this block Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex") - val itemPlayback = - currentItemPlayback.copy( - sourceId = source.id?.toUUIDOrNull(), - audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, - // Preserve special constants (ONLY_FORCED, DISABLED) instead of resolved index - subtitleIndex = - if (currentItemPlayback.subtitleIndex < 0) { - currentItemPlayback.subtitleIndex - } else { - subtitleIndex ?: TrackIndex.DISABLED - }, - ) - if (userInitiated) { - viewModelScope.launchIO { - Timber.v("Saving user initiated item playback: %s", itemPlayback) - val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback) - _state.update { it.copy(currentItemPlayback = updated) } - } - } else { - _state.update { it.copy(currentItemPlayback = itemPlayback) } - } _state.update { it.copy( currentPlayback = @@ -954,78 +899,92 @@ class PlaybackViewModel fun changeAudioStream(index: Int) { viewModelScope.launchIO { - Timber.d("Changing audio track to %s", index) - val itemPlayback = - itemPlaybackRepository.saveTrackSelection( - item = currentItem.item, - itemPlayback = state.value.currentItemPlayback!!, - trackIndex = index, - type = MediaStreamType.AUDIO, - ) - _state.update { it.copy(currentItemPlayback = itemPlayback) } - - // Resolve ONLY_FORCED to actual track based on new audio language - val source = state.value.currentPlayback?.mediaSourceInfo - val resolvedSubtitleIndex = - if (source != null) { - streamChoiceService.resolveSubtitleIndex( - source = source, - audioStreamIndex = index, - seriesId = currentItem.item.data.seriesId, - subtitleIndex = itemPlayback.subtitleIndex, - prefs = preferences, + val currentPlayback = state.value.currentPlayback + if (currentPlayback != null) { + Timber.d("Changing audio track to %s", index) + saveTrackSelection(index, MediaStreamType.AUDIO) + _state.update { + it.copy( + currentPlayback = it.currentPlayback?.copy(audioIndex = index), ) - } else { - itemPlayback.subtitleIndex.takeIf { it >= 0 } } - changeStreams( - currentItem.item, - itemPlayback, - index, - resolvedSubtitleIndex, - onMain { player.currentPosition }, - true, - ) + // Resolve ONLY_FORCED to actual track based on new audio language + val resolvedSubtitleIndex = + streamChoiceService.resolveSubtitleIndex( + source = currentPlayback.mediaSourceInfo, + audioStreamIndex = index, + seriesId = currentItem.item.data.seriesId, + subtitleIndex = currentPlayback.subtitleIndex, + prefs = preferences, + ) + + changeStreams( + item = currentItem.item, + sourceId = currentPlayback.mediaSourceInfo.id, + audioIndex = index, + subtitleIndex = resolvedSubtitleIndex, + positionMs = onMain { player.currentPosition }, + enableDirectPlay = true, + ) + } else { + Timber.w("Trying to change audio, but currentPlayback is null") + } } } fun changeSubtitleStream(index: Int): Job = viewModelScope.launchIO { - Timber.d("Changing subtitle track to %s", index) - val itemPlayback = - itemPlaybackRepository.saveTrackSelection( - item = currentItem.item, - itemPlayback = state.value.currentItemPlayback!!, - trackIndex = index, - type = MediaStreamType.SUBTITLE, - ) - _state.update { it.copy(currentItemPlayback = itemPlayback) } + val currentPlayback = state.value.currentPlayback + if (currentPlayback != null) { + Timber.d("Changing subtitle track to %s", index) + saveTrackSelection(index, MediaStreamType.SUBTITLE) + _state.update { + it.copy( + currentPlayback = it.currentPlayback?.copy(subtitleIndex = index), + ) + } - // Resolve ONLY_FORCED to actual track index for playback - val source = state.value.currentPlayback?.mediaSourceInfo - val resolvedIndex = - if (source != null) { + // Resolve ONLY_FORCED to actual track index for playback + val resolvedIndex = streamChoiceService.resolveSubtitleIndex( - source = source, - audioStreamIndex = itemPlayback.audioIndex, + source = currentPlayback.mediaSourceInfo, + audioStreamIndex = currentPlayback.audioIndex, seriesId = currentItem.item.data.seriesId, subtitleIndex = index, prefs = preferences, ) - } else { - index.takeIf { it >= 0 } - } - changeStreams( - currentItem.item, - itemPlayback, - itemPlayback.audioIndex, - resolvedIndex, - onMain { player.currentPosition }, - true, + changeStreams( + item = currentItem.item, + sourceId = currentPlayback.mediaSourceInfo.id, + audioIndex = currentPlayback.audioIndex, + subtitleIndex = resolvedIndex, + positionMs = onMain { player.currentPosition }, + enableDirectPlay = true, + ) + } else { + Timber.w("Trying to change subtitle, but currentPlayback is null") + } + } + + internal suspend fun saveTrackSelection( + trackIndex: Int, + type: MediaStreamType, + ) { + val itemPlayback = + itemPlaybackRepository.saveTrackSelection( + item = currentItem.item, + itemPlayback = state.value.currentItemPlayback, + trackIndex = trackIndex, + type = type, + ) + _state.update { + it.copy( + currentItemPlayback = itemPlayback, ) } + } private suspend fun prefetchTrickplay( duration: Duration, @@ -1050,7 +1009,11 @@ class PlaybackViewModel fun getTrickplayUrl( index: Int, trickPlayInfo: TrickplayInfo? = state.value.currentMediaInfo.trickPlayInfo, - mediaSourceId: UUID? = state.value.currentItemPlayback?.sourceId, + mediaSourceId: UUID? = + state.value.currentPlayback + ?.mediaSourceInfo + ?.id + ?.toUUIDOrNull(), ): String? = trickPlayInfo?.let { val itemId = currentItem.id @@ -1388,14 +1351,16 @@ class PlaybackViewModel PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> { Timber.w("Playback error during ${it.playMethod}, falling back to transcoding") - val currentItemPlayback = state.value.currentItemPlayback!! + val currentPlayback = state.value.currentPlayback + if (currentPlayback == null) { + Timber.w("Playback error, currentPlayback is null") + } changeStreams( - currentItem.item, - currentItemPlayback, - currentItemPlayback.audioIndex, - currentItemPlayback.subtitleIndex, - player.currentPosition, - false, + item = currentItem.item, + sourceId = currentPlayback?.mediaSourceInfo?.id, + audioIndex = currentPlayback?.audioIndex, + subtitleIndex = currentPlayback?.subtitleIndex, + positionMs = player.currentPosition, enableDirectPlay = false, enableDirectStream = false, ) @@ -1475,7 +1440,7 @@ class PlaybackViewModel * Atomically update [currentMediaInfo] */ internal suspend fun updateCurrentMedia(block: (CurrentMediaInfo) -> CurrentMediaInfo) = - withContext(WholphinDispatchers.IO) { + withContext(WholphinDispatchers.Default) { _state.update { it.copy(currentMediaInfo = block.invoke(it.currentMediaInfo)) } @@ -1598,16 +1563,16 @@ class PlaybackViewModel } suspend fun loadSubtitleDelay() { - state.value.currentItemPlayback?.let { + state.value.currentPlayback?.let { if (it.subtitleIndexEnabled) { val result = - itemPlaybackRepository.getTrackModifications(it.itemId, it.subtitleIndex) + itemPlaybackRepository.getTrackModifications(it.item.id, it.subtitleIndex) if (result != null) { Timber.v( "Loading subtitle delay %s for track=%s, itemId=%s", result.delayMs, it.subtitleIndex, - it.itemId, + it.item.id, ) updateCurrentPlayback { it?.copy(subtitleDelay = result.delayMs.milliseconds) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt index e9e809ae..c08759c0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt @@ -21,7 +21,9 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.RemoteSubtitleInfo +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber +import kotlin.time.Duration.Companion.milliseconds sealed interface SubtitleSearchStatus { data object Inactive : SubtitleSearchStatus @@ -62,16 +64,17 @@ fun PlaybackViewModel.searchForSubtitles(language: String? = null) { } viewModelScope.launchIO { try { - state.value.currentItemPlayback?.itemId?.let { - Timber.v("Searching for remote subtitles for %s", it) + state.value.currentPlayback?.itemId?.let { itemId -> + Timber.v("Searching for remote subtitles for %s", itemId) val results = api.subtitleApi .searchRemoteSubtitles( - itemId = it, + itemId = itemId, language = language, ).content .sortedWith( - compareByDescending { it.communityRating } + compareByDescending { it.isHashMatch } + .thenByDescending { it.communityRating } .thenByDescending { it.downloadCount }, ) subtitleSearchState.update { it.copy(status = SubtitleSearchStatus.Success(results)) } @@ -104,21 +107,19 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( subtitleSearchState.update { it.copy(status = SubtitleSearchStatus.Downloading) } viewModelScope.launchIO { try { - state.value.currentItemPlayback?.let { + state.value.currentPlayback?.let { currentPlayback -> Timber.v( "Downloading remote subtitles for itemId=%s, sourceId=%s: %s", - it.itemId, - it.sourceId, + currentPlayback.itemId, + currentPlayback.sourceId, subtitleId, ) api.subtitleApi.downloadRemoteSubtitles( - itemId = it.sourceId ?: it.itemId, + itemId = currentPlayback.sourceId ?: currentPlayback.itemId, subtitleId = subtitleId, ) - val currentSource = state.value.currentPlayback?.mediaSourceInfo val currentSubtitleStreams = - currentSource - ?.mediaStreams + currentPlayback.mediaSourceInfo.mediaStreams ?.filter { it.type == MediaStreamType.SUBTITLE } .orEmpty() val externalPaths = currentSubtitleStreams.map { it.path } @@ -131,14 +132,16 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( // The server triggers a refresh in the background, so query periodically for the item until its updated while (maxAttempts > 0 && subtitleCount == newCount) { maxAttempts-- - delay(1500) - val base = BaseItem(api.userLibraryApi.getItem(itemId = it.itemId).content) + delay(1500.milliseconds) + val base = + BaseItem(api.userLibraryApi.getItem(itemId = currentPlayback.itemId).content) currentItem = when (currentItem) { is PlaylistItem.Intro -> PlaylistItem.Intro(base) is PlaylistItem.Media -> PlaylistItem.Media(base) } - mediaSource = streamChoiceService.chooseSource(currentItem.item.data, it) + mediaSource = + base.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == currentPlayback.sourceId } if (mediaSource == null) { // This shouldn't happen, but just in case showToast( @@ -169,28 +172,43 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( stream.isExternal && stream.path !in externalPaths } if (newStream != null) { - var audioIndex = it?.audioIndex - if (audioIndex != null && audioIndex != TrackIndex.UNSPECIFIED) { - // User has picked a specific audio track - // Since, now adding a new external subtitle track, need to adjust the audio index as well + var audioIndex = currentPlayback.audioIndex + if (audioIndex != TrackIndex.UNSPECIFIED && audioIndex >= newStream.index) { + // User has previously picked a specific audio track + // If the new external subtitle track was added before the audio track, need to adjust the audio index as well Timber.v("New external subtitle, audioIndex=$audioIndex, adding 1") audioIndex += 1 + state.value.currentItemPlayback?.let { currentItemPlayback -> + if (currentItemPlayback.audioIndex != TrackIndex.UNSPECIFIED) { + Timber.d("User has a previously saved audio index that needs to be updated") + saveTrackSelection(audioIndex, MediaStreamType.AUDIO) + } + } } + saveTrackSelection(newStream.index, MediaStreamType.SUBTITLE) updateCurrentMedia { it.copy( - subtitleStreams = - subtitlesStreams.map { - SimpleMediaStream.from(context.resources, it, true) - }, + sourceId = mediaSource.id, + audioStreams = getAudioStreams(mediaSource), + subtitleStreams = getSubtitleStreams(mediaSource), ) } + updateCurrentPlayback { + it?.copy( + item = currentItem.item, + mediaSourceInfo = mediaSource, + audioIndex = audioIndex, + subtitleIndex = newStream.index, + ) + } + this@downloadAndSwitchSubtitles.changeStreams( - currentItem.item, - it, - audioIndex, - newStream.index, - onMain { player.currentPosition }, - true, + item = currentItem.item, + sourceId = currentPlayback.mediaSourceInfo.id, + audioIndex = audioIndex, + subtitleIndex = newStream.index, + positionMs = onMain { player.currentPosition }, + enableDirectPlay = true, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt index 6e11013b..2ab4c2f7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt @@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.util import androidx.annotation.OptIn import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.playback.CurrentPlayback import kotlinx.coroutines.CoroutineScope @@ -177,12 +176,11 @@ data class PlaybackItemState( ) { constructor( playback: CurrentPlayback, - itemPlayback: ItemPlayback, ) : this( - itemId = itemPlayback.itemId, + itemId = playback.item.id, playMethod = playback.playMethod, - audioStreamIndex = itemPlayback.audioIndex.takeIf { itemPlayback.audioIndexEnabled }, - subtitleStreamIndex = itemPlayback.subtitleIndex.takeIf { itemPlayback.subtitleIndexEnabled }, + audioStreamIndex = playback.audioIndex.takeIf { it >= 0 }, + subtitleStreamIndex = playback.subtitleIndex.takeIf { it >= 0 }, playSessionId = playback.playSessionId, liveStreamId = playback.liveStreamId, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb3d89af..a9bbb1e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -872,4 +872,6 @@ More user profile settings Override your user profile settings from the server Any language + Hash matches + SDH