From 39dad7666c365b4fb3f543069a8844ec27a45c4b Mon Sep 17 00:00:00 2001 From: cthelight <46094900+cthelight@users.noreply.github.com> Date: Sat, 16 Apr 2022 19:46:08 -0500 Subject: [PATCH 1/2] Subtitles: Auto-select default option On other players (web/andriod app) the user perferences for subtitle behavior are taken into account, and used to make an assumption about subtitle behavior. This patch ports most of that logic here. "Smart" selection is not yet fully-featured, as it requires additional knowledge about audio language preferences. Rather it uses the fallback mechanism, which emulates the "Default" subtitle option. The logic for the different options was based on the main jellyfin repo (specifically sha 49d5fdb33fc9792147c1df03e1d1b051e6b7ec79 in file Emby.Server.Implementations/Library/MediaStreamSelector.cs) Additionally, this implementation specifically prefers text-based subtitles (assuming they match the user's preference) as they are the only ones natively supported by Roku. Also, the subtitle changing mechanism is reworked slightly to make use of the new implementation herein --- source/ShowScenes.brs | 2 +- source/VideoPlayer.brs | 5 +- source/utils/Subtitles.brs | 106 +++++++++++++++++++++++++++++++------ 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/source/ShowScenes.brs b/source/ShowScenes.brs index 0ffdd6668..7757255be 100644 --- a/source/ShowScenes.brs +++ b/source/ShowScenes.brs @@ -364,7 +364,7 @@ end sub function CreateVideoPlayerGroup(video_id, mediaSourceId = invalid, audio_stream_idx = 1) ' Video is Playing - video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx) + video = VideoPlayer(video_id, mediaSourceId, audio_stream_idx, defaultSubtitleTrackFromVid(video_id)) if video = invalid then return invalid video.observeField("selectSubtitlePressed", m.port) video.observeField("state", m.port) diff --git a/source/VideoPlayer.brs b/source/VideoPlayer.brs index bd2e1b9aa..934110123 100644 --- a/source/VideoPlayer.brs +++ b/source/VideoPlayer.brs @@ -184,7 +184,6 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = - video.content.SubtitleTracks = subtitles["text"] ' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles - video.SelectedSubtitle = -1 video.directPlaySupported = playbackInfo.MediaSources[0].SupportsDirectPlay fully_external = false @@ -235,6 +234,10 @@ sub AddVideoContent(video, mediaSourceId, audio_stream_idx = 1, subtitle_idx = - video.content.setCertificatesFile("common:/certs/ca-bundle.crt") video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based + ' Perform relevant setup work for selected subtitle, and return the index of the subtitle + ' is enabled/will be enabled, indexed on the provided list of subtitles + video.SelectedSubtitle = setupSubtitle(video, video.Subtitles, subtitle_idx) + if not fully_external video.content = authorize_request(video.content) end if diff --git a/source/utils/Subtitles.brs b/source/utils/Subtitles.brs index 12479d7fc..47395c410 100644 --- a/source/utils/Subtitles.brs +++ b/source/utils/Subtitles.brs @@ -1,3 +1,90 @@ +' Identify the default subtitle track for a given video id +' returns the server-side track index for the appriate subtitle +function defaultSubtitleTrackFromVid(video_id) as integer + meta = ItemMetaData(video_id) + if meta = invalid then return invalid + subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams) + default_text_subs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text) + if default_text_subs <> -1 + return default_text_subs + else + return defaultSubtitleTrack(subtitles["all"]) ' if no appropriate text subs exist, allow non-text + end if +end function + + +' Identify the default subtitle track +' if "requires_text" is true, only return a track if it is textual +' This allows forcing text subs, since roku requires transcoding of non-text subs +' returns the server-side track index for the appriate subtitle +function defaultSubtitleTrack(sorted_subtitles, require_text = false) as integer + if m.user.Configuration.SubtitleMode = "None" + return -1 ' No subtitles desired: select none + end if + + for each item in sorted_subtitles + ' Only auto-select subtitle if language matches preference + languageMatch = (m.user.Configuration.SubtitleLanguagePreference = item.Track.Language) + ' Ensure textuality of subtitle matches preferenced passed as arg + matchTextReq = ((require_text and item.IsTextSubtitleStream) or not require_text) + if languageMatch and matchTextReq + if m.user.Configuration.SubtitleMode = "Default" and (item.isForced or item.IsDefault or item.IsExternal) + return item.Index ' Finds first forced, or default, or external subs in sorted list + else if m.user.Configuration.SubtitleMode = "Always" and not item.IsForced + return item.Index ' Select the first non-forced subtitle option in the sorted list + else if m.user.Configuration.SubtitleMode = "OnlyForced" and item.IsForced + return item.Index ' Select the first forced subtitle option in the sorted list + else if m.user.Configuration.SubtitlePlaybackMode = "Smart" and (item.isForced or item.IsDefault or item.IsExternal) + ' Simplified "Smart" logic here mimics Default (as that is fallback behavior normally) + ' Avoids detecting preferred audio language (as is utilized in main client) + return item.Index + end if + end if + end for + return -1 ' 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 = -1) as integer + if subtitle_idx = -1 + ' If we are not using text-based subtitles, turn them off + video.globalCaptionMode = "Off" + return -1 + end if + + ' Translate the raw index to one relative to the provided list + subtitleSelIdx = getSubtitleSelIdxFromSubIdx(subtitles, subtitle_idx) + + selectedSubtitle = subtitles[subtitleSelIdx] + + 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[selectedSubtitle.TextIndex].TrackName + 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 = -1 then return -1 + for each item in subtitles + if item.Index = sub_idx + return selIdx + end if + selIdx = selIdx + 1 + end for + return -1 +end function function selectSubtitleTrack(tracks, current = -1) as integer video = m.scene.focusedChild.focusedChild @@ -45,26 +132,13 @@ sub changeSubtitleDuringPlayback(newid) currentSubtitles = video.Subtitles[video.SelectedSubtitle] newSubtitles = video.Subtitles[newid] - if newSubtitles.IsEncoded - - ' Switching to Encoded Subtitle stream + if newSubtitles.IsEncoded or (currentSubtitles <> invalid and currentSubtitles.IsEncoded) + ' With encoded subtitles we need to stop/start playback video.control = "stop" AddVideoContent(video, video.mediaSourceId, video.audioIndex, newSubtitles.Index, video.position * 10000000) video.control = "play" - video.globalCaptionMode = "Off" ' Using encoded subtitles - so turn off text subtitles - - else if currentSubtitles <> invalid and currentSubtitles.IsEncoded - - ' Switching from an Encoded stream to a text stream - video.control = "stop" - AddVideoContent(video, video.mediaSourceId, video.audioIndex, -1, video.position * 10000000) - video.control = "play" - video.globalCaptionMode = "On" - video.subtitleTrack = video.availableSubtitleTracks[newSubtitles.TextIndex].TrackName - else - - ' Switch to Text Subtitle Track + ' Switching from text to text (or none to text) does not require stopping playback video.globalCaptionMode = "On" video.subtitleTrack = video.availableSubtitleTracks[newSubtitles.TextIndex].TrackName end if From 7ee54110957f8b608459f18ecbd01276c3a75531 Mon Sep 17 00:00:00 2001 From: cthelight <46094900+cthelight@users.noreply.github.com> Date: Sun, 22 May 2022 17:54:56 -0500 Subject: [PATCH 2/2] Subtitles: Search by URL not assumed index Currently, when populating subtitleTracks, we assume that the ordering and list of populated subtitle tracks will not change when Roku moves the list into availableSubtitleTracks. This causes an issue with some videos as it is not always consistent. This patch modifies the logic to no-longer inject assumed final indices into our list of text-based subtitles, but instead search through the availableSubtitleTracks array and locate the actual subtitle that refers to the same URL as in our list. In this way we are guaranteed to always tell Roku to play the subtitle we want, no matter how re- ordered the options get. NOTE: The URL gets mildly mangled in the process of copying from subtitleTracks to availableSubtitleTracks, so we need so search via substring, rather than doing a full string comparison. --- source/utils/Subtitles.brs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/source/utils/Subtitles.brs b/source/utils/Subtitles.brs index 47395c410..09244bcdf 100644 --- a/source/utils/Subtitles.brs +++ b/source/utils/Subtitles.brs @@ -1,3 +1,22 @@ +' 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 -1 +end function + ' Identify the default subtitle track for a given video id ' returns the server-side track index for the appriate subtitle function defaultSubtitleTrackFromVid(video_id) as integer @@ -65,7 +84,7 @@ function setupSubtitle(video, subtitles, subtitle_idx = -1) as integer else ' If this is a text-based subtitle, set relevant settings for roku captions video.globalCaptionMode = "On" - video.subtitleTrack = video.availableSubtitleTracks[selectedSubtitle.TextIndex].TrackName + video.subtitleTrack = video.availableSubtitleTracks[availSubtitleTrackIdx(video, subtitleSelIdx)].TrackName end if return subtitleSelIdx @@ -140,7 +159,7 @@ sub changeSubtitleDuringPlayback(newid) else ' Switching from text to text (or none to text) does not require stopping playback video.globalCaptionMode = "On" - video.subtitleTrack = video.availableSubtitleTracks[newSubtitles.TextIndex].TrackName + video.subtitleTrack = video.availableSubtitleTracks[availSubtitleTrackIdx(video, newid)].TrackName end if video.SelectedSubtitle = newid @@ -204,7 +223,6 @@ function sortSubtitles(id as string, MediaStreams) textTracks = [] for i = 0 to tracks["forced"].count() - 1 if tracks["forced"][i].IsTextSubtitleStream - tracks["forced"][i].TextIndex = textTracks.count() textTracks.push(tracks["forced"][i].Track) end if end for