Fix smart & default subtitle selection logic (#579)

## Description
Fixes the logic for subtitle selection when using `Smart` or `Default`
server modes. Also takes into account unknown languages.

This is heavily inspired by [the server selection
logic](https://github.com/jellyfin/jellyfin/blob/release-10.11.z/Emby.Server.Implementations/Library/MediaStreamSelector.cs).
For better or for worse, Wholphin implements this client side.

### Related issues
Fixes #570
This commit is contained in:
Ray 2025-12-27 14:22:40 -05:00 committed by GitHub
parent fd1feddab3
commit 494ff07b72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 591 additions and 25 deletions

View file

@ -153,7 +153,7 @@ class StreamChoiceService
return source.mediaStreams?.letNotEmpty { streams ->
val candidates = streams.filter { it.type == MediaStreamType.SUBTITLE }
chooseSubtitleStream(
audioStream,
audioStream?.language,
candidates,
itemPlayback,
plc,
@ -163,7 +163,7 @@ class StreamChoiceService
}
fun chooseSubtitleStream(
audioStream: MediaStream?,
audioStreamLang: String?,
candidates: List<MediaStream>,
itemPlayback: ItemPlayback?,
playbackLanguageChoice: PlaybackLanguageChoice?,
@ -198,48 +198,59 @@ class StreamChoiceService
userConfig?.subtitleMode ?: SubtitlePlaybackMode.DEFAULT
}
}
val candidates =
candidates.sortedWith(
compareBy<MediaStream>(
{ it.isDefault },
{ !it.isForced && it.language.equals(subtitleLanguage, true) },
{ it.isForced && it.language.equals(subtitleLanguage, true) },
{ it.isForced && it.language.isUnknown },
{ it.isForced },
),
)
return when (subtitleMode) {
SubtitlePlaybackMode.ALWAYS -> {
if (subtitleLanguage != null) {
candidates.firstOrNull { it.language == subtitleLanguage }
if (subtitleLanguage.isNotNullOrBlank()) {
candidates.firstOrNull {
it.language.equals(subtitleLanguage, true) ||
it.language.isUnknown
}
} else {
candidates.firstOrNull()
}
}
SubtitlePlaybackMode.ONLY_FORCED -> {
if (subtitleLanguage != null) {
if (subtitleLanguage.isNotNullOrBlank()) {
candidates.firstOrNull { it.language == subtitleLanguage && it.isForced }
?: candidates.firstOrNull { it.language.isUnknown && it.isForced }
} else {
candidates.firstOrNull { it.isForced }
}
}
SubtitlePlaybackMode.SMART -> {
val audioLanguage = userConfig?.audioLanguagePreference
val audioStreamLang = audioStream?.language
if (audioLanguage.isNotNullOrBlank() && audioStreamLang.isNotNullOrBlank() && audioLanguage != audioStreamLang) {
candidates.firstOrNull { it.language == subtitleLanguage }
if (subtitleLanguage.isNotNullOrBlank()) {
val audioLanguage = userConfig?.audioLanguagePreference
if (audioLanguage.isNotNullOrBlank() && audioLanguage != audioStreamLang) {
candidates.firstOrNull { it.language == subtitleLanguage }
?: candidates.firstOrNull { it.language.isUnknown }
} else {
candidates.firstOrNull { it.isForced && it.language == subtitleLanguage }
?: candidates.firstOrNull { it.isForced && it.language.isUnknown }
}
} else {
null
candidates.firstOrNull { it.isForced }
}
}
SubtitlePlaybackMode.DEFAULT -> {
subtitleLanguage?.let { lang ->
// Find best track that is in the preferred language
(
candidates.firstOrNull { it.isDefault && it.isForced && it.language == lang }
?: candidates.firstOrNull { it.isDefault && it.language == lang }
?: candidates.firstOrNull { it.isForced && it.language == lang }
)
if (subtitleLanguage.isNotNullOrBlank()) {
candidates.firstOrNull { it.language == subtitleLanguage && (it.isDefault || it.isForced) }
?: candidates.firstOrNull { it.isDefault || it.isForced }
} else {
candidates.firstOrNull { it.isDefault || it.isForced }
}
?: (
// If none in preferred language, just find the best track
candidates.firstOrNull { it.isDefault && it.isForced }
?: candidates.firstOrNull { it.isDefault }
?: candidates.firstOrNull { it.isForced }
)
}
SubtitlePlaybackMode.NONE -> {
@ -249,3 +260,12 @@ class StreamChoiceService
}
}
}
private val String?.isUnknown: Boolean
get() =
this == null ||
this.equals("unknown", true) ||
this.equals("und", true) ||
this.equals("undetermined", true) ||
this.equals("mul", true) ||
this.equals("zxx", true)