From 454c0212b6b4fe29a4b95f93f8319cea7e888c78 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 10 Aug 2026 20:17:42 -0400 Subject: [PATCH] Fix issues with external subtitles & transcoding (#1790) ## Description Fixes a bug where the external subtitle track would not be activated if playback is transcoded Also fixes a bug where external subtitles would cause MPV to incorrectly switch to transcoding ### Related issues Fixes #1778 ### Testing Emulator with files that have a variety of external SRT, embedded SRT & ASS, & embedded PGS ## Screenshots N/A ## AI or LLM usage None --- .../wholphin/ui/playback/PlaybackViewModel.kt | 93 +++++++++++-------- .../ui/playback/TracksChangedListener.kt | 41 ++++++++ 2 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/playback/TracksChangedListener.kt 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 a3847f63..6587e758 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 @@ -184,6 +184,7 @@ class PlaybackViewModel internal lateinit var currentItem: PlaylistItem internal var forceTranscoding: Boolean = false private var activityListener: TrackActivityPlaybackListener? = null + private var trackChangeListener: TracksChangedListener? = null private val jobs = mutableListOf() private val isPlaylist = destination is Destination.PlaybackList @@ -215,6 +216,9 @@ class PlaybackViewModel it.release() player.removeListener(it) } + this@PlaybackViewModel.trackChangeListener?.let { + player.removeListener(it) + } player.release() mediaSession?.release() } @@ -604,6 +608,9 @@ class PlaybackViewModel withContext(WholphinDispatchers.IO) { val itemId = item.id + trackChangeListener?.let { onMain { player.removeListener(it) } } + trackChangeListener = null + state.value.currentPlayback?.let { currentPlayback -> if (currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY && @@ -715,6 +722,7 @@ class PlaybackViewModel val externalSubtitle = source.findExternalSubtitle(subtitleIndex)?.let { + Timber.v("Using externally delivered subtitle tracks %s", subtitleIndex) it.deliveryUrl?.let { deliveryUrl -> var flags = 0 if (it.isForced) flags = flags.or(C.SELECTION_FLAG_FORCED) @@ -807,48 +815,51 @@ class PlaybackViewModel mediaItem, positionMs, ) - if (transcodeType == PlayMethod.DIRECT_PLAY && (audioIndex != null || subtitleIndex != null)) { - val onTracksChangedListener = - object : Player.Listener { - override fun onTracksChanged(tracks: Tracks) { - Timber.v("onTracksChanged: $tracks") - if (tracks.groups.isNotEmpty()) { - val result = - TrackSelectionUtils.createTrackSelections( - player.trackSelectionParameters, - player.currentTracks, - audioIndex, - subtitleIndex, - source, - ) - Timber.v("onTracksChanged: %s", result) - player.removeListener(this) - if (result.bothSelected) { - player.trackSelectionParameters = - result.trackSelectionParameters - } else { - // Fall back to transcoding - Timber.w( - "Failed to select tracks, falling back to transcoding: %s", - result, - ) - viewModelScope.launchIO { - changeStreams( - item = item, - sourceId = sourceId, - audioIndex = audioIndex, - subtitleIndex = subtitleIndex, - positionMs = positionMs, - enableDirectPlay = false, - enableDirectStream = true, - ) - } - } - viewModelScope.launchIO { loadSubtitleDelay() } - } - } + val onFailure: () -> Unit = { + if (player is MpvPlayer && externalSubtitle != null) { + // MpvPlayer may change tracks more than once to add external subtitles + trackChangeListener?.let { player.addListener(it) } + } else { + viewModelScope.launchIO { + changeStreams( + item = item, + sourceId = sourceId, + audioIndex = audioIndex, + subtitleIndex = subtitleIndex, + positionMs = positionMs, + enableDirectPlay = false, + enableDirectStream = true, + ) } - player.addListener(onTracksChangedListener) + } + } + val onTracksChangedListener = + if (transcodeType == PlayMethod.DIRECT_PLAY && (audioIndex != null || subtitleIndex != null)) { + TracksChangedListener( + player = player, + audioIndex = audioIndex, + subtitleIndex = subtitleIndex, + source = source, + onFailure = onFailure, + ) + } else if (externalSubtitle != null) { + TracksChangedListener( + player = player, + audioIndex = null, // Do not manually activate audio + subtitleIndex = subtitleIndex, + source = source, + onFailure = onFailure, + ) + } else { + null + } + onTracksChangedListener?.let { + player.addListener(it) + this@PlaybackViewModel.trackChangeListener = it + } + + if (subtitleIndex != null) { + viewModelScope.launchIO { loadSubtitleDelay() } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TracksChangedListener.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TracksChangedListener.kt new file mode 100644 index 00000000..c5192153 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TracksChangedListener.kt @@ -0,0 +1,41 @@ +package com.github.damontecres.wholphin.ui.playback + +import androidx.media3.common.Player +import androidx.media3.common.Tracks +import org.jellyfin.sdk.model.api.MediaSourceInfo +import timber.log.Timber + +class TracksChangedListener( + val player: Player, + val audioIndex: Int?, + val subtitleIndex: Int?, + val source: MediaSourceInfo, + val onFailure: () -> Unit, +) : Player.Listener { + override fun onTracksChanged(tracks: Tracks) { + Timber.v("onTracksChanged: $tracks") + if (tracks.groups.isNotEmpty()) { + val result = + TrackSelectionUtils.createTrackSelections( + player.trackSelectionParameters, + player.currentTracks, + audioIndex, + subtitleIndex, + source, + ) + Timber.v("onTracksChanged: %s", result) + player.removeListener(this) + if (result.bothSelected) { + player.trackSelectionParameters = + result.trackSelectionParameters + } else { + // Fall back to transcoding + Timber.w( + "Failed to select tracks, falling back to transcoding: %s", + result, + ) + onFailure.invoke() + } + } + } +}