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
This commit is contained in:
Damontecres 2026-08-10 20:17:42 -04:00 • committed by GitHub
parent 7581b189aa
commit 454c0212b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 41 deletions

View file

@ -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<Job>()
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() }
}
}
}

View file

@ -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()
}
}
}
}