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 4d89eeae..fc2c6615 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 @@ -84,6 +84,8 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.api.MediaSegmentType +import org.jellyfin.sdk.model.api.MediaSourceInfo +import org.jellyfin.sdk.model.api.MediaStream import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.PlayMethod import org.jellyfin.sdk.model.api.PlaybackInfoDto @@ -423,6 +425,56 @@ class PlaybackViewModel ) = withContext(Dispatchers.IO) { val itemId = item.id + val currentPlayback = this@PlaybackViewModel.currentPlayback.value + if (currentPlayback != null && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) { + // If direct playing, can try to switch tracks without playback restarting + // Except for external subtitles + // TODO there's probably no reason why we can't add external subtitles? + + val source = currentPlayback.mediaSourceInfo + val externalSubtitleCount = source.externalSubtitlesCount + val externalSubtitle = source.findExternalSubtitle(subtitleIndex) + + val result = + withContext(Dispatchers.Main) { + applyTrackSelections( + player, + true, + audioIndex, + subtitleIndex, + externalSubtitleCount, + externalSubtitle != null, + ) + } + if (result.bothSelected) { + Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex") + val itemPlayback = + currentItemPlayback.copy( + sourceId = source.id?.toUUIDOrNull(), + audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, + subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED, + ) + if (userInitiated) { + viewModelScope.launchIO { + Timber.v("Saving user initiated item playback: %s", itemPlayback) + val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback) + withContext(Dispatchers.Main) { + this@PlaybackViewModel.currentItemPlayback.value = updated + } + } + } + withContext(Dispatchers.Main) { + this@PlaybackViewModel.currentPlayback.value = + currentPlayback.copy( + tracks = checkForSupport(player.currentTracks), + ) + this@PlaybackViewModel.currentItemPlayback.value = itemPlayback + } + + return@withContext + } + } + // TODO // if (currentItemPlayback.let { // it.itemId == itemId && @@ -505,28 +557,25 @@ class PlaybackViewModel val decision = StreamDecision(itemId, transcodeType, mediaUrl) Timber.v("Playback decision: $decision") - val externalSubtitleCount = - source.mediaStreams - ?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0 + val externalSubtitleCount = source.externalSubtitlesCount val externalSubtitle = - source.mediaStreams - ?.firstOrNull { it.type == MediaStreamType.SUBTITLE && it.isExternal && it.index == subtitleIndex } - ?.let { - it.deliveryUrl?.let { deliveryUrl -> - var flags = 0 - if (it.isForced) flags = flags.or(C.SELECTION_FLAG_FORCED) - if (it.isDefault) flags = flags.or(C.SELECTION_FLAG_DEFAULT) - MediaItem.SubtitleConfiguration - .Builder( - api.createUrl(deliveryUrl).toUri(), - ).setId("e:${it.index}") - .setMimeType(subtitleMimeTypes[it.codec]) - .setLanguage(it.language) - .setSelectionFlags(flags) - .build() - } + source.findExternalSubtitle(subtitleIndex)?.let { + it.deliveryUrl?.let { deliveryUrl -> + var flags = 0 + if (it.isForced) flags = flags.or(C.SELECTION_FLAG_FORCED) + if (it.isDefault) flags = flags.or(C.SELECTION_FLAG_DEFAULT) + MediaItem.SubtitleConfiguration + .Builder( + api.createUrl(deliveryUrl).toUri(), + ).setId("e:${it.index}") + .setMimeType(subtitleMimeTypes[it.codec]) + .setLanguage(it.language) + .setSelectionFlags(flags) + .build() } + } + Timber.v("externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle") val mediaItem = @@ -544,6 +593,7 @@ class PlaybackViewModel playMethod = transcodeType, playSessionId = response.playSessionId, liveStreamId = source.liveStreamId, + mediaSourceInfo = source, ) val itemPlayback = currentItemPlayback.copy( @@ -580,7 +630,7 @@ class PlaybackViewModel duration.value = source.runTimeTicks?.ticks loading.value = LoadingState.Success - currentPlayback.value = playback + this@PlaybackViewModel.currentPlayback.value = playback this@PlaybackViewModel.currentItemPlayback.value = itemPlayback player.setMediaItem( mediaItem, @@ -1056,6 +1106,7 @@ data class CurrentPlayback( val playMethod: PlayMethod, val playSessionId: String?, val liveStreamId: String?, + val mediaSourceInfo: MediaSourceInfo, ) sealed interface SubtitleSearch { @@ -1084,8 +1135,32 @@ val Format.idAsInt: Int? } } +/** + * Returns the number of external subtitle streams there are + */ +val MediaSourceInfo.externalSubtitlesCount: Int + get() = + mediaStreams + ?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0 + +/** + * Returns the [MediaStream] for the given subtitle index iff it is external + */ +fun MediaSourceInfo.findExternalSubtitle(subtitleIndex: Int?): MediaStream? = + subtitleIndex?.let { + mediaStreams + ?.firstOrNull { it.type == MediaStreamType.SUBTITLE && it.isExternal && it.index == subtitleIndex } + } + suspend fun onMain(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block) +data class TrackSelectionResult( + val audioSelected: Boolean, + val subtitleSelected: Boolean, +) { + val bothSelected: Boolean = audioSelected && subtitleSelected +} + @OptIn(UnstableApi::class) private fun applyTrackSelections( player: Player, @@ -1094,71 +1169,79 @@ private fun applyTrackSelections( subtitleIndex: Int?, externalSubtitleCount: Int, subtitleIsExternal: Boolean, -) { - if (subtitleIndex != null && subtitleIndex >= 0 && (subtitleIsExternal || supportsDirectPlay)) { - val chosenTrack = - if (subtitleIsExternal) { - player.currentTracks.groups.firstOrNull { group -> - group.type == C.TRACK_TYPE_TEXT && group.isSupported && - (0..= 0 && (subtitleIsExternal || supportsDirectPlay)) { + val chosenTrack = + if (subtitleIsExternal) { + player.currentTracks.groups.firstOrNull { group -> + group.type == C.TRACK_TYPE_TEXT && group.isSupported && + (0.. + group.type == C.TRACK_TYPE_TEXT && group.isSupported && + (0.. - group.type == C.TRACK_TYPE_TEXT && group.isSupported && + group.type == C.TRACK_TYPE_AUDIO && group.isSupported && (0.. - group.type == C.TRACK_TYPE_AUDIO && group.isSupported && - (0..