source_utils_Subtitles.bs
import "pkg:/source/enums/SubtitleSelection.bs"
' Roku translates the info provided in subtitleTracks into availableSubtitleTracks
' Including ignoring tracks, if they are not understood, thus making indexing unpredictable.
' This function translates between our internel selected subtitle index
' and the corresponding index in availableSubtitleTracks.
function availSubtitleTrackIdx(video, sub_idx) as integer
url = video.Subtitles[sub_idx].Track.TrackName
idx = 0
for each availTrack in video.availableSubtitleTracks
' The TrackName must contain the URL we supplied originally, though
' Roku mangles the name a bit, so we check if the URL is a substring, rather
' than strict equality
if Instr(1, availTrack.TrackName, url)
return idx
end if
idx = idx + 1
end for
return SubtitleSelection.none
end function
' defaultSubtitleTrackFromVid: Identifies the default subtitle track given metadata and audio index
'
' @param {object} meta - metadata object containing MediaSources with MediaStreams
' @param {integer} selectedAudioIndex - index of selected audio stream (used for Smart mode language matching)
' @return {integer} subtitle track index or SubtitleSelection.none if not found
function defaultSubtitleTrackFromVid(meta as object, selectedAudioIndex as integer) as integer
userSession = m.global.user
if userSession.config.subtitleMode = "None"
return SubtitleSelection.none ' No subtitles desired: return none
end if
if not isValid(meta) then return SubtitleSelection.none
mediaSources = invalid
if isValid(meta.mediaSourcesData) and isValidAndNotEmpty(meta.mediaSourcesData.mediaSources)
mediaSources = meta.mediaSourcesData.mediaSources
end if
if not isValid(mediaSources) or not isValidAndNotEmpty(mediaSources[0].MediaStreams) then return SubtitleSelection.none
allStreams = mediaSources[0].MediaStreams
subtitles = sortSubtitles(allStreams)
selectedAudioLanguage = ""
' Find the audio stream with the matching Jellyfin index
audioMediaStream = invalid
for each stream in allStreams
if isValid(stream.index) and stream.index = selectedAudioIndex
audioMediaStream = stream
exit for
end if
end for
' Ensure audio media stream is valid before using language property
if isValid(audioMediaStream)
selectedAudioLanguage = audioMediaStream.Language ?? ""
end if
defaultTextSubs = defaultSubtitleTrack(subtitles["text"], selectedAudioLanguage, true) ' Find correct subtitle track (forced text)
if defaultTextSubs <> SubtitleSelection.none
return defaultTextSubs
end if
if not userSession.settings.playbackSubsOnlyText
return defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage) ' if no appropriate text subs exist, allow non-text
end if
return SubtitleSelection.none
end function
' defaultSubtitleTrack:
'
' @param {dynamic} sortedSubtitles - array of subtitles sorted by type and language
' @param {string} selectedAudioLanguage - language for selected audio track
' @param {boolean} [requireText=false] - indicates if only text subtitles should be considered
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} if one is not found
function defaultSubtitleTrack(sortedSubtitles, selectedAudioLanguage as string, requireText = false as boolean) as integer
' ONE rendezvous to get user node
localUser = m.global.user
subtitleMode = isValid(localUser.config.subtitleMode) ? LCase(localUser.config.subtitleMode) : ""
allowSmartMode = false
' Only evaluate selected audio language if we have a value
if selectedAudioLanguage <> ""
allowSmartMode = selectedAudioLanguage <> localUser.config.subtitleLanguagePreference
end if
for each item in sortedSubtitles
' Only auto-select subtitle if language matches SubtitleLanguagePreference
languageMatch = true
if localUser.config.subtitleLanguagePreference <> ""
languageMatch = (localUser.config.subtitleLanguagePreference = item.Track.Language)
end if
' Ensure textuality of subtitle matches preference passed as arg
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
if languageMatch and matchTextReq
if subtitleMode = "default" and (item.IsForced or item.IsDefault)
' Return first forced or default subtitle track
return item.Index
else if subtitleMode = "always" and not item.IsForced
' Return the first non-forced subtitle track (full subs preferred over forced)
return item.Index
else if subtitleMode = "onlyforced" and item.IsForced
' Return first forced subtitle track
return item.Index
else if subtitleMode = "smart" and allowSmartMode and not item.IsForced
' Return the first non-forced subtitle track (full subs when audio differs from preference)
return item.Index
end if
end if
end for
' Always mode fallback: if no full (non-forced) subtitles found, fall back to forced/default
if subtitleMode = "always"
for each item in sortedSubtitles
' Ensure textuality of subtitle matches preference passed as arg
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
if matchTextReq
if item.IsForced or item.IsDefault
' Return first forced or default subtitle track as fallback
return item.Index
end if
end if
end for
end if
' User has chosen smart subtitle mode
' We already attempted to load subtitles in preferred language, but none were found.
' Fall back to default behaviour while ignoring preferredlanguage
if subtitleMode = "smart" and allowSmartMode
for each item in sortedSubtitles
' Ensure textuality of subtitle matches preference passed as arg
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
if matchTextReq
if item.IsForced or item.IsDefault
' Return first forced or default subtitle track
return item.Index
end if
end if
end for
end if
return SubtitleSelection.none ' Keep current default behavior of "None", if no correct subtitle is identified
end function
' Given a set of subtitles, and a subtitle index (the index on the server, not in the list provided)
' this will set all relevant settings for roku (mainly closed captions) and return the index of the
' subtitle track specified, but indexed based on the provided list of subtitles
function setupSubtitle(video, subtitles, subtitle_idx = SubtitleSelection.none) as integer
if subtitle_idx = SubtitleSelection.none
' If we are not using text-based subtitles, turn them off
return SubtitleSelection.none
end if
' Translate the raw index to one relative to the provided list
subtitleSelIdx = getSubtitleSelIdxFromSubIdx(subtitles, subtitle_idx)
selectedSubtitle = subtitles[subtitleSelIdx]
if isValid(selectedSubtitle) and isValid(selectedSubtitle.IsEncoded)
if selectedSubtitle.IsEncoded
' With encoded subtitles, turn off captions
video.globalCaptionMode = "Off"
else
' If this is a text-based subtitle, set relevant settings for roku captions
video.globalCaptionMode = "On"
video.subtitleTrack = video.availableSubtitleTracks[availSubtitleTrackIdx(video, subtitleSelIdx)].TrackName
end if
end if
return subtitleSelIdx
end function
' The subtitle index on the server differs from the index we track locally
' This function converts the former into the latter
function getSubtitleSelIdxFromSubIdx(subtitles, sub_idx) as integer
selIdx = 0
if sub_idx = SubtitleSelection.none then return SubtitleSelection.none
for each item in subtitles
if item.Index = sub_idx
return selIdx
end if
selIdx = selIdx + 1
end for
return SubtitleSelection.none
end function
'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top
function sortSubtitles(MediaStreams)
tracks = { "forced": [], "default": [], "normal": [], "textForced": [], "textDefault": [], "textNormal": [] }
' ONE rendezvous to get user node
localUser = m.global.user
prefered_lang = localUser.config.subtitleLanguagePreference
for each stream in MediaStreams
if stream.type = "Subtitle"
url = ""
if isValid(stream.DeliveryUrl)
url = buildURL(stream.DeliveryUrl)
end if
stream = {
"Track": { "Language": stream.language, "Description": stream.displaytitle, "TrackName": url },
"IsTextSubtitleStream": stream.IsTextSubtitleStream ?? false,
"Index": stream.index,
"IsDefault": stream.IsDefault ?? false,
"IsForced": stream.IsForced ?? false,
"IsExternal": stream.IsExternal ?? false,
"IsEncoded": stream.DeliveryMethod = "Encode"
}
if stream.IsForced = true
trackType = "forced"
textType = "textForced"
else if stream.IsDefault = true
trackType = "default"
textType = "textDefault"
else
trackType = "normal"
textType = "textNormal"
end if
if prefered_lang <> "" and prefered_lang = stream.Track.Language
tracks[trackType].unshift(stream)
if stream.IsTextSubtitleStream
tracks[textType].unshift(stream)
end if
else
tracks[trackType].push(stream)
if stream.IsTextSubtitleStream
tracks[textType].push(stream)
end if
end if
end if
end for
tracks["default"].append(tracks["normal"])
tracks["forced"].append(tracks["default"])
' Merge text buckets with same priority ordering: forced > default > normal
tracks["textDefault"].append(tracks["textNormal"])
tracks["textForced"].append(tracks["textDefault"])
return { "all": tracks["forced"], "text": tracks["textForced"] }
end function