mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add "Only Forced Subtitles" option to subtitle selection (#589)
## Description Adds a client-side "Only Forced Subtitles" option when selecting subtitles, per the discussion in #571. ### Changes - **TrackIndex.ONLY_FORCED** (`-3`): New constant for the special case - **StreamChoiceService**: Intercepts `ONLY_FORCED` at the top of `chooseSubtitleStream()` before server-side `SubtitlePlaybackMode` logic (which remains untouched) - **UI**: Adds "Only Forced Subtitles" option to both: - In-player subtitle menu (`PlaybackDialog.kt`) - Pre-playback stream selection dialog (`Dialogs.kt`) ### Forced track detection Uses metadata `isForced` flag as primary check, with title-based fallback ("forced", "signs", "songs" patterns) when language matches audio. ### Related issues Closes #571 ### Screenshots N/A - subtitle selection menu only ### AI/LLM usage Used Claude Code for assistance and to draft PR description. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
parent
8bdc8a6f8f
commit
ed67e5575f
8 changed files with 279 additions and 5 deletions
|
|
@ -133,7 +133,7 @@ class ItemPlaybackRepository
|
||||||
Timber.v("Saving track selection %s", toSave)
|
Timber.v("Saving track selection %s", toSave)
|
||||||
toSave = saveItemPlayback(toSave)
|
toSave = saveItemPlayback(toSave)
|
||||||
val seriesId = item.data.seriesId
|
val seriesId = item.data.seriesId
|
||||||
if (seriesId != null && trackIndex != TrackIndex.UNSPECIFIED) {
|
if (seriesId != null && (trackIndex >= 0 || trackIndex == TrackIndex.DISABLED)) {
|
||||||
if (type == MediaStreamType.AUDIO) {
|
if (type == MediaStreamType.AUDIO) {
|
||||||
val stream = source.mediaStreams?.first { it.index == trackIndex }
|
val stream = source.mediaStreams?.first { it.index == trackIndex }
|
||||||
if (stream?.language != null) {
|
if (stream?.language != null) {
|
||||||
|
|
@ -147,7 +147,7 @@ class ItemPlaybackRepository
|
||||||
subtitlesDisabled = true,
|
subtitlesDisabled = true,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val stream = source.mediaStreams?.first { it.index == trackIndex }
|
val stream = source.mediaStreams?.firstOrNull { it.index == trackIndex }
|
||||||
if (stream?.language != null) {
|
if (stream?.language != null) {
|
||||||
streamChoiceService.updateSubtitles(
|
streamChoiceService.updateSubtitles(
|
||||||
item.data,
|
item.data,
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,9 @@ object TrackIndex {
|
||||||
* The user has explicitly disabled the tracks (eg turned off subtitles)
|
* The user has explicitly disabled the tracks (eg turned off subtitles)
|
||||||
*/
|
*/
|
||||||
const val DISABLED = -2
|
const val DISABLED = -2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user wants only forced subtitles (for foreign language dialogue, signs, etc.)
|
||||||
|
*/
|
||||||
|
const val ONLY_FORCED = -3
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,40 @@ class StreamChoiceService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves ONLY_FORCED to an actual subtitle track index.
|
||||||
|
* Returns the original index if not ONLY_FORCED or DISABLED.
|
||||||
|
*/
|
||||||
|
suspend fun resolveSubtitleIndex(
|
||||||
|
source: MediaSourceInfo,
|
||||||
|
audioStreamIndex: Int?,
|
||||||
|
seriesId: UUID?,
|
||||||
|
subtitleIndex: Int,
|
||||||
|
prefs: UserPreferences,
|
||||||
|
): Int? =
|
||||||
|
if (subtitleIndex != TrackIndex.ONLY_FORCED) {
|
||||||
|
subtitleIndex
|
||||||
|
} else {
|
||||||
|
val audioStream =
|
||||||
|
source.mediaStreams?.firstOrNull {
|
||||||
|
it.type == MediaStreamType.AUDIO && it.index == audioStreamIndex
|
||||||
|
}
|
||||||
|
val itemPlayback =
|
||||||
|
ItemPlayback(
|
||||||
|
userId = serverRepository.currentUser.value!!.rowId,
|
||||||
|
itemId = UUID.randomUUID(), // Not used for ONLY_FORCED resolution
|
||||||
|
subtitleIndex = TrackIndex.ONLY_FORCED,
|
||||||
|
)
|
||||||
|
chooseSubtitleStream(
|
||||||
|
source = source,
|
||||||
|
audioStream = audioStream,
|
||||||
|
seriesId = seriesId,
|
||||||
|
itemPlayback = itemPlayback,
|
||||||
|
plc = null,
|
||||||
|
prefs = prefs,
|
||||||
|
)?.index
|
||||||
|
}
|
||||||
|
|
||||||
fun chooseSubtitleStream(
|
fun chooseSubtitleStream(
|
||||||
audioStreamLang: String?,
|
audioStreamLang: String?,
|
||||||
candidates: List<MediaStream>,
|
candidates: List<MediaStream>,
|
||||||
|
|
@ -171,6 +205,14 @@ class StreamChoiceService
|
||||||
): MediaStream? {
|
): MediaStream? {
|
||||||
if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) {
|
if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) {
|
||||||
return null
|
return null
|
||||||
|
} else if (itemPlayback?.subtitleIndex == TrackIndex.ONLY_FORCED) {
|
||||||
|
// Client-side manual override: User selected "Only Forced" in player menu
|
||||||
|
val seriesLang =
|
||||||
|
playbackLanguageChoice?.subtitleLanguage?.takeIf { it.isNotNullOrBlank() }
|
||||||
|
val subtitleLanguage =
|
||||||
|
(seriesLang ?: userConfig?.subtitleLanguagePreference)
|
||||||
|
?.takeIf { it.isNotNullOrBlank() }
|
||||||
|
return findForcedTrack(candidates, subtitleLanguage, audioStreamLang)
|
||||||
} else if (itemPlayback?.subtitleIndexEnabled == true) {
|
} else if (itemPlayback?.subtitleIndexEnabled == true) {
|
||||||
return candidates.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
return candidates.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -261,6 +303,37 @@ class StreamChoiceService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true if the track is forced (via metadata flag or title patterns). */
|
||||||
|
private fun isForcedOrSigns(track: MediaStream): Boolean {
|
||||||
|
if (track.isForced) return true
|
||||||
|
val title = track.title ?: track.displayTitle ?: return false
|
||||||
|
return title.contains("forced", ignoreCase = true) ||
|
||||||
|
title.contains("signs", ignoreCase = true) ||
|
||||||
|
title.contains("songs", ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finds a forced/signs track: subtitle pref -> audio -> unknown -> null. */
|
||||||
|
private fun findForcedTrack(
|
||||||
|
candidates: List<MediaStream>,
|
||||||
|
subtitleLanguage: String?,
|
||||||
|
audioLanguage: String?,
|
||||||
|
): MediaStream? {
|
||||||
|
// 1. User's preferred subtitle language
|
||||||
|
if (subtitleLanguage != null) {
|
||||||
|
candidates
|
||||||
|
.firstOrNull { it.language.equals(subtitleLanguage, true) && isForcedOrSigns(it) }
|
||||||
|
?.let { return it }
|
||||||
|
}
|
||||||
|
// 2. Audio language (for sign-subtitles if no preference match)
|
||||||
|
if (audioLanguage != null) {
|
||||||
|
candidates
|
||||||
|
.firstOrNull { it.language.equals(audioLanguage, true) && isForcedOrSigns(it) }
|
||||||
|
?.let { return it }
|
||||||
|
}
|
||||||
|
// 3. Unknown language forced track
|
||||||
|
return candidates.firstOrNull { it.language.isUnknown && isForcedOrSigns(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val String?.isUnknown: Boolean
|
private val String?.isUnknown: Boolean
|
||||||
|
|
|
||||||
|
|
@ -528,6 +528,16 @@ fun chooseStream(
|
||||||
onClick = { onClick.invoke(TrackIndex.DISABLED) },
|
onClick = { onClick.invoke(TrackIndex.DISABLED) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
add(
|
||||||
|
DialogItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(R.string.only_forced_subtitles))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
},
|
||||||
|
onClick = { onClick.invoke(TrackIndex.ONLY_FORCED) },
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
addAll(
|
addAll(
|
||||||
streams.filter { it.type == type }.mapIndexed { index, stream ->
|
streams.filter { it.type == type }.mapIndexed { index, stream ->
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,8 @@ fun PlaybackDialog(
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(subtitleIndex))
|
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(subtitleIndex))
|
||||||
} else if (subtitleIndex == TrackIndex.DISABLED) {
|
} else if (subtitleIndex == TrackIndex.DISABLED) {
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(TrackIndex.DISABLED))
|
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(TrackIndex.DISABLED))
|
||||||
|
} else if (subtitleIndex == TrackIndex.ONLY_FORCED) {
|
||||||
|
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(TrackIndex.ONLY_FORCED))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSelectSearch = {
|
onSelectSearch = {
|
||||||
|
|
@ -322,6 +324,23 @@ fun SubtitleChoiceBottomDialog(
|
||||||
supportingContent = {},
|
supportingContent = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
selected = currentChoice == TrackIndex.ONLY_FORCED,
|
||||||
|
onClick = {
|
||||||
|
onSelectChoice(TrackIndex.ONLY_FORCED)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
SelectedLeadingContent(currentChoice == TrackIndex.ONLY_FORCED)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.only_forced_subtitles),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
itemsIndexed(choices) { index, choice ->
|
itemsIndexed(choices) { index, choice ->
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
|
||||||
|
|
@ -523,7 +523,13 @@ class PlaybackViewModel
|
||||||
currentItemPlayback.copy(
|
currentItemPlayback.copy(
|
||||||
sourceId = source.id?.toUUIDOrNull(),
|
sourceId = source.id?.toUUIDOrNull(),
|
||||||
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
||||||
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
|
// Preserve special constants (ONLY_FORCED, DISABLED) instead of resolved index
|
||||||
|
subtitleIndex =
|
||||||
|
if (currentItemPlayback.subtitleIndex < 0) {
|
||||||
|
currentItemPlayback.subtitleIndex
|
||||||
|
} else {
|
||||||
|
subtitleIndex ?: TrackIndex.DISABLED
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if (userInitiated) {
|
if (userInitiated) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
|
@ -744,11 +750,27 @@ class PlaybackViewModel
|
||||||
type = MediaStreamType.AUDIO,
|
type = MediaStreamType.AUDIO,
|
||||||
)
|
)
|
||||||
this@PlaybackViewModel.currentItemPlayback.setValueOnMain(itemPlayback)
|
this@PlaybackViewModel.currentItemPlayback.setValueOnMain(itemPlayback)
|
||||||
|
|
||||||
|
// Resolve ONLY_FORCED to actual track based on new audio language
|
||||||
|
val source = currentPlayback.value?.mediaSourceInfo
|
||||||
|
val resolvedSubtitleIndex =
|
||||||
|
if (source != null) {
|
||||||
|
streamChoiceService.resolveSubtitleIndex(
|
||||||
|
source = source,
|
||||||
|
audioStreamIndex = index,
|
||||||
|
seriesId = item.data.seriesId,
|
||||||
|
subtitleIndex = itemPlayback.subtitleIndex,
|
||||||
|
prefs = preferences,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
itemPlayback.subtitleIndex.takeIf { it >= 0 }
|
||||||
|
}
|
||||||
|
|
||||||
changeStreams(
|
changeStreams(
|
||||||
item,
|
item,
|
||||||
itemPlayback,
|
itemPlayback,
|
||||||
index,
|
index,
|
||||||
itemPlayback.subtitleIndex,
|
resolvedSubtitleIndex,
|
||||||
onMain { player.currentPosition },
|
onMain { player.currentPosition },
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
@ -766,11 +788,27 @@ class PlaybackViewModel
|
||||||
type = MediaStreamType.SUBTITLE,
|
type = MediaStreamType.SUBTITLE,
|
||||||
)
|
)
|
||||||
this@PlaybackViewModel.currentItemPlayback.setValueOnMain(itemPlayback)
|
this@PlaybackViewModel.currentItemPlayback.setValueOnMain(itemPlayback)
|
||||||
|
|
||||||
|
// Resolve ONLY_FORCED to actual track index for playback
|
||||||
|
val source = currentPlayback.value?.mediaSourceInfo
|
||||||
|
val resolvedIndex =
|
||||||
|
if (source != null) {
|
||||||
|
streamChoiceService.resolveSubtitleIndex(
|
||||||
|
source = source,
|
||||||
|
audioStreamIndex = itemPlayback.audioIndex,
|
||||||
|
seriesId = item.data.seriesId,
|
||||||
|
subtitleIndex = index,
|
||||||
|
prefs = preferences,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
index.takeIf { it >= 0 }
|
||||||
|
}
|
||||||
|
|
||||||
changeStreams(
|
changeStreams(
|
||||||
item,
|
item,
|
||||||
itemPlayback,
|
itemPlayback,
|
||||||
itemPlayback.audioIndex,
|
itemPlayback.audioIndex,
|
||||||
index,
|
resolvedIndex,
|
||||||
onMain { player.currentPosition },
|
onMain { player.currentPosition },
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@
|
||||||
<string name="no_scheduled_recordings">No scheduled recordings</string>
|
<string name="no_scheduled_recordings">No scheduled recordings</string>
|
||||||
<string name="no_update_available">No update available</string>
|
<string name="no_update_available">No update available</string>
|
||||||
<string name="none">None</string>
|
<string name="none">None</string>
|
||||||
|
<string name="only_forced_subtitles">Only Forced Subtitles</string>
|
||||||
<string name="outro">Outro</string>
|
<string name="outro">Outro</string>
|
||||||
<string name="path">Path</string>
|
<string name="path">Path</string>
|
||||||
<string name="people">People</string>
|
<string name="people">People</string>
|
||||||
|
|
|
||||||
|
|
@ -613,6 +613,132 @@ class TestStreamChoiceServiceMultipleChoices(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for client-side "Only Forced" override (TrackIndex.ONLY_FORCED).
|
||||||
|
* This tests the findForcedTrack function with user subtitle language preference.
|
||||||
|
*/
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class TestStreamChoiceServiceOnlyForcedClientOverride(
|
||||||
|
val input: TestInput,
|
||||||
|
) {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
runTest(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters(name = "{index}: {0}")
|
||||||
|
fun data(): Collection<TestInput> =
|
||||||
|
listOf(
|
||||||
|
// Test 1: Prefer user's subtitle language preference for forced tracks
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 1, // spa forced track (matches user pref)
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = "spa",
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true),
|
||||||
|
subtitle(1, "spa", forced = true),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 2: User subtitle preference matches Signs track via title (not forced flag)
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 0, // eng signs track via title detection
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = "eng",
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = false, title = "Signs & Songs"),
|
||||||
|
subtitle(1, "spa", forced = true),
|
||||||
|
),
|
||||||
|
streamAudioLang = "jpn", // different from subtitle pref
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 3: Falls back to audio-matching forced when no preference match
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 0, // eng forced (audio match, step 2)
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = "spa", // no spa forced exists
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true), // matches audio
|
||||||
|
subtitle(1, "und", forced = true),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 4: Falls back to audio-matching signs when user pref has no match
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 0, // eng signs (audio match fallback)
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = "fre", // user prefers French, no French forced exists
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = false, title = "Signs & Songs"), // matches audio
|
||||||
|
subtitle(1, "spa", forced = true),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 5: Use audio language for signs/songs when NO subtitle preference
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 0, // eng signs track matching audio
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = null, // no preference
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = false, title = "Signs & Songs"),
|
||||||
|
subtitle(1, "spa", forced = true),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 6: Unknown language forced track with no preference
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 0, // unknown forced track
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = null,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, null, forced = true), // unknown/null language
|
||||||
|
subtitle(1, "spa", forced = false),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 7: Unknown language forced track when no audio match
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = 0, // unknown forced track (step 3)
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = null,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "und", forced = true), // unknown language forced
|
||||||
|
subtitle(1, "spa", forced = false),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng", // no eng tracks exist
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
// Test 8: No matching forced track returns null (not irrelevant language)
|
||||||
|
TestInput(
|
||||||
|
expectedIndex = null, // no forced track matches - return null instead of wrong language
|
||||||
|
userSubtitleMode = null,
|
||||||
|
userSubtitleLang = "eng",
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "chi", forced = true), // Chinese forced - wrong language
|
||||||
|
subtitle(1, "spa", forced = true), // Spanish forced - wrong language
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng", // audio is English, no English forced
|
||||||
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.ONLY_FORCED),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class TestInput(
|
data class TestInput(
|
||||||
val expectedIndex: Int?,
|
val expectedIndex: Int?,
|
||||||
val userSubtitleMode: SubtitlePlaybackMode?,
|
val userSubtitleMode: SubtitlePlaybackMode?,
|
||||||
|
|
@ -674,6 +800,7 @@ fun subtitle(
|
||||||
lang: String?,
|
lang: String?,
|
||||||
default: Boolean = false,
|
default: Boolean = false,
|
||||||
forced: Boolean = false,
|
forced: Boolean = false,
|
||||||
|
title: String? = null,
|
||||||
): MediaStream =
|
): MediaStream =
|
||||||
MediaStream(
|
MediaStream(
|
||||||
type = MediaStreamType.SUBTITLE,
|
type = MediaStreamType.SUBTITLE,
|
||||||
|
|
@ -686,6 +813,7 @@ fun subtitle(
|
||||||
isExternal = false,
|
isExternal = false,
|
||||||
isTextSubtitleStream = true,
|
isTextSubtitleStream = true,
|
||||||
supportsExternalStream = true,
|
supportsExternalStream = true,
|
||||||
|
title = title,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun itemPlayback(
|
private fun itemPlayback(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue