From 20cdb6fe5c0bd2ec9e80768a9b1f17c8461f785b Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 12 Dec 2023 22:20:11 -0500 Subject: [PATCH 01/16] ensure lastRunVersion is valid --- source/migrations.bs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/migrations.bs b/source/migrations.bs index 29cb36a37..aecb9d352 100644 --- a/source/migrations.bs +++ b/source/migrations.bs @@ -71,11 +71,13 @@ sub runRegistryUserMigrations() ' app versions < 2.0.0 didn't save LastRunVersion at the user level ' fall back to using the apps lastRunVersion lastRunVersion = m.global.app.lastRunVersion - registry_write("LastRunVersion", lastRunVersion, section) + if isValid(lastRunVersion) + registry_write("LastRunVersion", lastRunVersion, section) + end if end if ' BASE_MIGRATION - if not versionChecker(lastRunVersion, CLIENT_VERSION_REQUIRING_BASE_MIGRATION) + if isValid(lastRunVersion) and not versionChecker(lastRunVersion, CLIENT_VERSION_REQUIRING_BASE_MIGRATION) m.wasMigrated = true print `Running Registry Migration for ${CLIENT_VERSION_REQUIRING_BASE_MIGRATION} for userid: ${section}` From 7b5d553f84d26e609ec3e7fe328eeadb974f7e63 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Sat, 23 Dec 2023 15:15:16 -0500 Subject: [PATCH 02/16] Replace asTimeStringLoc() function with custom code asTimeStringLoc() was added in Roku OS 12. Rewritten for improved legacy support. --- components/Clock.bs | 78 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/components/Clock.bs b/components/Clock.bs index 2137a11cb..4233643d7 100644 --- a/components/Clock.bs +++ b/components/Clock.bs @@ -1,8 +1,16 @@ import "pkg:/source/utils/misc.bs" +' @fileoverview Clock component to display current time formatted based on user's chosen 12 or 24 hour setting + +' Possible clock formats +enum ClockFormat + h12 = "12h" + h24 = "24h" +end enum + sub init() - ' If hideclick setting is checked, exit without setting any variables + ' If hideclick setting is enabled, exit without setting any variables if m.global.session.user.settings["ui.design.hideclock"] return end if @@ -16,11 +24,11 @@ sub init() m.currentTimeTimer.control = "start" ' Default to 12 hour clock - m.format = "short-h12" + m.format = ClockFormat.h12 ' If user has selected a 24 hour clock, update date display format - if LCase(m.global.device.clockFormat) = "24h" - m.format = "short-h24" + if LCase(m.global.device.clockFormat) = ClockFormat.h24 + m.format = ClockFormat.h24 end if end sub @@ -34,6 +42,64 @@ sub onCurrentTimeTimerFire() ' Convert to local time zone m.dateTimeObject.ToLocalTime() - ' Format time as requested - m.clockTime.text = m.dateTimeObject.asTimeStringLoc(m.format) + ' Format time for display - based on 12h/24h setting + formattedTime = formatTimeAsString() + + ' Display time + m.clockTime.text = formattedTime end sub + +' formatTimeAsString: Returns a string with the current time formatted for either a 12 or 24 hour clock +' +' @return {string} current time formatted for either a 12 hour or 24 hour clock +function formatTimeAsString() as string + return m.format = ClockFormat.h12 ? format12HourTime() : format24HourTime() +end function + +' format12HourTime: Returns a string with the current time formatted for a 12 hour clock +' +' @return {string} current time formatted for a 12 hour clock +function format12HourTime() as string + currentHour = m.dateTimeObject.GetHours() + currentMinute = m.dateTimeObject.GetMinutes() + + displayedHour = StrI(currentHour).trim() + displayedMinute = StrI(currentMinute).trim() + meridian = currentHour < 12 ? "am" : "pm" + + if currentHour = 0 + displayedHour = "12" + end if + + if currentHour > 12 + correctedHour = currentHour - 12 + displayedHour = StrI(correctedHour).trim() + end if + + if currentMinute < 10 + displayedMinute = `0${displayedMinute}` + end if + + return `${displayedHour}:${displayedMinute} ${meridian}` +end function + +' format24HourTime: Returns a string with the current time formatted for a 24 hour clock +' +' @return {string} current time formatted for a 24 hour clock +function format24HourTime() as string + currentHour = m.dateTimeObject.GetHours() + currentMinute = m.dateTimeObject.GetMinutes() + + displayedHour = StrI(currentHour).trim() + displayedMinute = StrI(currentMinute).trim() + + if currentHour < 10 + displayedHour = `0${displayedHour}` + end if + + if currentMinute < 10 + displayedMinute = `0${displayedMinute}` + end if + + return `${displayedHour}:${displayedMinute}` +end function From c48061089744a53d4d952965d4a116f30dcf067b Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Sat, 23 Dec 2023 17:44:41 -0500 Subject: [PATCH 03/16] Fix OK button not playing episode from episode list --- components/tvshows/TVEpisodes.bs | 2 ++ source/ShowScenes.bs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/tvshows/TVEpisodes.bs b/components/tvshows/TVEpisodes.bs index ffc225cdc..57164ceb3 100644 --- a/components/tvshows/TVEpisodes.bs +++ b/components/tvshows/TVEpisodes.bs @@ -93,6 +93,8 @@ function onKeyEvent(key as string, press as boolean) as boolean focusedItem = getFocusedItem() if isValid(focusedItem) m.top.selectedItem = focusedItem + 'Prevent the selected item event from double firing + m.top.selectedItem = invalid end if return true end if diff --git a/source/ShowScenes.bs b/source/ShowScenes.bs index 353ee136e..3610b2498 100644 --- a/source/ShowScenes.bs +++ b/source/ShowScenes.bs @@ -811,7 +811,7 @@ function CreateSeasonDetailsGroupByID(seriesID as string, seasonID as string) as group.objects = TVEpisodes(seriesID, seasonID) group.episodeObjects = group.objects ' watch for button presses - group.observeField("episodeSelected", m.port) + group.observeField("selectedItem", m.port) group.observeField("quickPlayNode", m.port) ' don't wait for the extras button stopLoadingSpinner() From a90df38b3a9f7da5bf1cb6b4a7f878e114700c58 Mon Sep 17 00:00:00 2001 From: Jimi Date: Sun, 24 Dec 2023 12:25:01 -0700 Subject: [PATCH 04/16] Check for Live TV --- components/ItemGrid/LoadVideoContentTask.bs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 4e65bdbd2..ede5a62d4 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -77,6 +77,22 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s videotype = LCase(meta.type) + ' Check for any Live TV streams coming from other places other than the TV Guide + if isValid(meta.json) and isValid(meta.json.ChannelId) + if isValid(meta.json.EpisodeTitle) + meta.title = meta.json.EpisodeTitle + else if isValid(meta.json.Name) + meta.title = meta.json.Name + end if + meta.showID = meta.json.id + meta.live = true + if LCase(meta.json.type) = "program" + video.id = meta.json.ChannelId + else + video.id = meta.json.id + end if + end if + if videotype = "episode" or videotype = "series" video.content.contenttype = "episode" end if From 19c92f363ebaa6514c282728a24dc6b0f1433efe Mon Sep 17 00:00:00 2001 From: Jimi Date: Sun, 24 Dec 2023 12:35:23 -0700 Subject: [PATCH 05/16] Copy / Paste error. --- components/ItemGrid/LoadVideoContentTask.bs | 1 - 1 file changed, 1 deletion(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index ede5a62d4..7e0f78f7e 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -84,7 +84,6 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s else if isValid(meta.json.Name) meta.title = meta.json.Name end if - meta.showID = meta.json.id meta.live = true if LCase(meta.json.type) = "program" video.id = meta.json.ChannelId From df6a04f71a39479a7ff01e6ff0d39f4e32071013 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Fri, 29 Dec 2023 09:24:49 -0500 Subject: [PATCH 06/16] Make artist presentation views follow Item Titles setting Fixes #1548 --- components/ItemGrid/MusicLibraryView.bs | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ItemGrid/MusicLibraryView.bs b/components/ItemGrid/MusicLibraryView.bs index 77d79d62c..ceadaaa5b 100644 --- a/components/ItemGrid/MusicLibraryView.bs +++ b/components/ItemGrid/MusicLibraryView.bs @@ -140,14 +140,12 @@ sub loadInitialItems() m.loadItemsTask.itemId = m.top.parentItem.parentFolder else if LCase(m.view) = "artistspresentation" or LCase(m.options.view) = "artistspresentation" m.loadItemsTask.genreIds = "" - m.top.showItemTitles = "hidealways" else if LCase(m.view) = "artistsgrid" or LCase(m.options.view) = "artistsgrid" m.loadItemsTask.genreIds = "" else if LCase(m.view) = "albumartistsgrid" or LCase(m.options.view) = "albumartistsgrid" m.loadItemsTask.genreIds = "" else if LCase(m.view) = "albumartistspresentation" or LCase(m.options.view) = "albumartistspresentation" m.loadItemsTask.genreIds = "" - m.top.showItemTitles = "hidealways" else m.loadItemsTask.itemId = m.top.parentItem.Id end if From a37c7d9a7c41982267cae02c2ae00c69575b5e8c Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Sun, 31 Dec 2023 16:12:54 -0500 Subject: [PATCH 07/16] Fix subtitle selection Fixes #1607 --- components/manager/ViewCreator.bs | 9 +++++---- components/video/VideoPlayerView.bs | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/components/manager/ViewCreator.bs b/components/manager/ViewCreator.bs index 763834432..ebee6b3a9 100644 --- a/components/manager/ViewCreator.bs +++ b/components/manager/ViewCreator.bs @@ -111,19 +111,20 @@ sub processSubtitleSelection() end if if m.selectedSubtitle.IsEncoded + ' Roku can not natively display these subtitles, so turn off the caption mode on the device m.view.globalCaptionMode = "Off" else + ' Roku can natively display these subtitles, ensure the caption mode on the device is on m.view.globalCaptionMode = "On" - end if - if m.selectedSubtitle.IsExternal + ' Roku may rearrange subtitle tracks. Look up track based on name to ensure we get the correct index availableSubtitleTrackIndex = availSubtitleTrackIdx(m.selectedSubtitle.Track.TrackName) if availableSubtitleTrackIndex = -1 then return m.view.subtitleTrack = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName - else - m.view.selectedSubtitle = m.selectedSubtitle.Index end if + + m.view.selectedSubtitle = m.selectedSubtitle.Index end sub ' User requested playback info diff --git a/components/video/VideoPlayerView.bs b/components/video/VideoPlayerView.bs index 1aa96ba96..7e33b1352 100644 --- a/components/video/VideoPlayerView.bs +++ b/components/video/VideoPlayerView.bs @@ -256,6 +256,9 @@ end sub ' Event handler for when selectedSubtitle changes sub onSubtitleChange() + ' If the global caption mode is on, that means Roku can display the subtitles natively and doesn't need a video stop/start + if LCase(m.top.globalCaptionMode) = "on" then return + ' Save the current video position m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&) From 5baa8f49e73b5f82b5a70b274375e976ac307dd6 Mon Sep 17 00:00:00 2001 From: Charles Ewert Date: Tue, 2 Jan 2024 17:02:57 -0500 Subject: [PATCH 08/16] Bump version for release --- Makefile | 2 +- manifest | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index fdef679b8..ef0eade64 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # If you want to get_images, you'll also need convert from ImageMagick ########################################################################## -VERSION := 2.0.0 +VERSION := 2.0.1 ## usage diff --git a/manifest b/manifest index e4fe84332..af9c146dd 100644 --- a/manifest +++ b/manifest @@ -3,7 +3,7 @@ title=Jellyfin major_version=2 minor_version=0 -build_version=0 +build_version=1 ### Main Menu Icons / Channel Poster Artwork diff --git a/package.json b/package.json index fc011f065..03f9eec4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jellyfin-roku", "type": "module", - "version": "2.0.0", + "version": "2.0.1", "description": "Roku app for Jellyfin media server", "dependencies": { "@rokucommunity/bslib": "0.1.1", From 1c274867fcc090aecd2f6629b9759cd9203ad174 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:20:02 -0500 Subject: [PATCH 09/16] Update What's New content --- source/static/whatsNew/2.0.1.json | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 source/static/whatsNew/2.0.1.json diff --git a/source/static/whatsNew/2.0.1.json b/source/static/whatsNew/2.0.1.json new file mode 100644 index 000000000..8c4110685 --- /dev/null +++ b/source/static/whatsNew/2.0.1.json @@ -0,0 +1,34 @@ +[ + { + "description": "Fix migration crash", + "author": "cewert" + }, + { + "description": "Fix selection and display of subtitles that are not encoded", + "author": "1hitsong" + }, + { + "description": "Make music artist presentation views honor the Item Titles setting", + "author": "1hitsong" + }, + { + "description": "Fix launching Live TV channels from outside the guide", + "author": "jimdogx" + }, + { + "description": "Fix default subtitle track selection", + "author": "1hitsong" + }, + { + "description": "Fix video playback for Roku devices running a version of Roku OS less than 12.0", + "author": "1hitsong" + }, + { + "description": "Fix selecting episode using OK button on episode list view", + "author": "1hitsong" + }, + { + "description": "Make GH jobs work with new branch workflow", + "author": "cewert" + } +] \ No newline at end of file From 945a6d3e33eb455946c84bcb1adbc46439116b7d Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:51:24 -0500 Subject: [PATCH 10/16] Update source/static/whatsNew/2.0.1.json Co-authored-by: Charles Ewert --- source/static/whatsNew/2.0.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/static/whatsNew/2.0.1.json b/source/static/whatsNew/2.0.1.json index 8c4110685..d93efc8a0 100644 --- a/source/static/whatsNew/2.0.1.json +++ b/source/static/whatsNew/2.0.1.json @@ -1,6 +1,6 @@ [ { - "description": "Fix migration crash", + "description": "Fix startup crash", "author": "cewert" }, { From 3a9987f29c1e17659e1d25381852a97b6d865776 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:22:56 -0500 Subject: [PATCH 11/16] Fix default subtitle track selection Fixes #1571 --- components/ItemGrid/LoadVideoContentTask.bs | 113 ++++++++++++++++--- components/ItemGrid/LoadVideoContentTask.xml | 2 +- components/video/VideoPlayerView.bs | 5 + components/video/VideoPlayerView.xml | 2 +- 4 files changed, 107 insertions(+), 15 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 7e0f78f7e..f893d551d 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -7,6 +7,11 @@ import "pkg:/source/api/Image.bs" import "pkg:/source/api/userauth.bs" import "pkg:/source/utils/deviceCapabilities.bs" +enum SubtitleSelection + notset = -2 + none = -1 +end enum + sub init() m.user = AboutMe() m.top.functionName = "loadItems" @@ -44,19 +49,18 @@ sub loadItems() id = m.top.itemId mediaSourceId = invalid audio_stream_idx = m.top.selectedAudioStreamIndex - subtitle_idx = m.top.selectedSubtitleIndex forceTranscoding = false - m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)] + m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, forceTranscoding)] end sub -function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) as dynamic +function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) as dynamic video = {} video.id = id video.content = createObject("RoSGNode", "ContentNode") - LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding) + LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, forceTranscoding) if video.content = invalid return invalid @@ -65,9 +69,10 @@ function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, return video end function -sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean) +sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) meta = ItemMetaData(video.id) + subtitle_idx = m.top.selectedSubtitleIndex if not isValid(meta) video.errorMsg = "Error loading metadata" @@ -122,16 +127,38 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s if meta.live then mediaSourceId = "" m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition) - video.videoId = video.id - video.mediaSourceId = mediaSourceId - video.audioIndex = audio_stream_idx - if not isValid(m.playbackInfo) video.errorMsg = "Error loading playback info" video.content = invalid return end if + addSubtitlesToVideo(video, meta) + + ' Enable default subtitle track + if subtitle_idx = SubtitleSelection.notset + defaultSubtitleIndex = defaultSubtitleTrackFromVid(video.id) + + if defaultSubtitleIndex <> SubtitleSelection.none + video.SelectedSubtitle = defaultSubtitleIndex + subtitle_idx = defaultSubtitleIndex + + m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition) + if not isValid(m.playbackInfo) + video.errorMsg = "Error loading playback info" + video.content = invalid + return + end if + + addSubtitlesToVideo(video, meta) + end if + end if + + video.videoId = video.id + video.mediaSourceId = mediaSourceId + video.audioIndex = audio_stream_idx + video.SelectedSubtitle = subtitle_idx + video.PlaySessionId = m.playbackInfo.PlaySessionId if meta.live @@ -145,8 +172,6 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s m.playbackInfo = meta.json end if - addSubtitlesToVideo(video, meta) - if meta.live video.transcodeParams = { "MediaSourceId": m.playbackInfo.MediaSources[0].Id, @@ -198,13 +223,75 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s setCertificateAuthority(video.content) video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based - video.SelectedSubtitle = subtitle_idx - if not fully_external video.content = authRequest(video.content) end if end sub +' defaultSubtitleTrackFromVid: Identifies the default subtitle track given video id +' +' @param {dynamic} video_id - id of video user is playing +' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found +function defaultSubtitleTrackFromVid(video_id) as integer + if m.global.session.user.configuration.SubtitleMode = "None" + return SubtitleSelection.none ' No subtitles desired: return none + end if + + meta = ItemMetaData(video_id) + + if not isValid(meta) then return SubtitleSelection.none + if not isValid(meta.json) then return SubtitleSelection.none + if not isValidAndNotEmpty(meta.json.mediaSources) then return SubtitleSelection.none + if not isValidAndNotEmpty(meta.json.MediaSources[0].MediaStreams) then return SubtitleSelection.none + + 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 <> SubtitleSelection.none + return default_text_subs + end if + + if not m.global.session.user.settings["playback.subs.onlytext"] + return defaultSubtitleTrack(subtitles["all"]) ' if no appropriate text subs exist, allow non-text + end if + + return SubtitleSelection.none +end function + +' defaultSubtitleTrack: +' +' @param {dynamic} sorted_subtitles - array of subtitles sorted by type and language +' @param {boolean} [require_text=false] - indicates if only text subtitles should be considered +' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found +function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) as integer + for each item in sorted_subtitles + ' Only auto-select subtitle if language matches SubtitleLanguagePreference + languageMatch = true + if m.global.session.user.configuration.SubtitleLanguagePreference <> "" + languageMatch = (m.global.session.user.configuration.SubtitleLanguagePreference = item.Track.Language) + end if + + ' 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.global.session.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.global.session.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.global.session.user.configuration.SubtitleMode = "OnlyForced" and item.IsForced + return item.Index ' Select the first forced subtitle option in the sorted list + else if m.global.session.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 SubtitleSelection.none ' Keep current default behavior of "None", if no correct subtitle is identified +end function + sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external) protocol = LCase(m.playbackInfo.MediaSources[0].Protocol) if protocol <> "file" diff --git a/components/ItemGrid/LoadVideoContentTask.xml b/components/ItemGrid/LoadVideoContentTask.xml index bf4a88295..f935c3e33 100644 --- a/components/ItemGrid/LoadVideoContentTask.xml +++ b/components/ItemGrid/LoadVideoContentTask.xml @@ -4,7 +4,7 @@ - + diff --git a/components/video/VideoPlayerView.bs b/components/video/VideoPlayerView.bs index 7e33b1352..dfe4eb757 100644 --- a/components/video/VideoPlayerView.bs +++ b/components/video/VideoPlayerView.bs @@ -323,6 +323,11 @@ sub onVideoContentLoaded() m.top.transcodeParams = videoContent[0].transcodeparams m.chapters = videoContent[0].chapters + ' Allow default subtitles + m.top.unobserveField("selectedSubtitle") + m.top.selectedSubtitle = videoContent[0].selectedSubtitle + m.top.observeField("selectedSubtitle", "onSubtitleChange") + m.osd.itemTitleText = m.top.content.title populateChapterMenu() diff --git a/components/video/VideoPlayerView.xml b/components/video/VideoPlayerView.xml index 3469b17d9..e33724b00 100644 --- a/components/video/VideoPlayerView.xml +++ b/components/video/VideoPlayerView.xml @@ -6,7 +6,7 @@ - + From 286749ce4cf3087087764652bf0c927519cf3419 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:59:56 -0500 Subject: [PATCH 12/16] Code cleanup --- components/ItemGrid/LoadVideoContentTask.bs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index f893d551d..2b5fd65e2 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -264,24 +264,27 @@ end function ' @param {boolean} [require_text=false] - indicates if only text subtitles should be considered ' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) as integer + userConfig = m.global.session.user.configuration + subtitleMode = LCase(userConfig.SubtitleMode) + for each item in sorted_subtitles ' Only auto-select subtitle if language matches SubtitleLanguagePreference languageMatch = true - if m.global.session.user.configuration.SubtitleLanguagePreference <> "" - languageMatch = (m.global.session.user.configuration.SubtitleLanguagePreference = item.Track.Language) + if userConfig.SubtitleLanguagePreference <> "" + languageMatch = (userConfig.SubtitleLanguagePreference = item.Track.Language) end if ' 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.global.session.user.configuration.SubtitleMode = "Default" and (item.isForced or item.IsDefault or item.IsExternal) + if 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.global.session.user.configuration.SubtitleMode = "Always" and not item.IsForced + else if subtitleMode = "always" and not item.IsForced return item.Index ' Select the first non-forced subtitle option in the sorted list - else if m.global.session.user.configuration.SubtitleMode = "OnlyForced" and item.IsForced + else if subtitleMode = "onlyforced" and item.IsForced return item.Index ' Select the first forced subtitle option in the sorted list - else if m.global.session.user.configuration.SubtitlePlaybackMode = "Smart" and (item.isForced or item.IsDefault or item.IsExternal) + else if LCase(userConfig.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 From 7c8719cbf9c1ac6e6745c8e3be686ca27cb76085 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Wed, 3 Jan 2024 07:53:31 -0500 Subject: [PATCH 13/16] Check var is valid before LCase() --- components/ItemGrid/LoadVideoContentTask.bs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 2b5fd65e2..ea14db3b6 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -265,7 +265,9 @@ end function ' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) as integer userConfig = m.global.session.user.configuration - subtitleMode = LCase(userConfig.SubtitleMode) + + subtitlePlaybackMode = isValid(userConfig.SubtitlePlaybackMode) ? LCase(userConfig.SubtitlePlaybackMode) : "" + subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : "" for each item in sorted_subtitles ' Only auto-select subtitle if language matches SubtitleLanguagePreference @@ -284,7 +286,7 @@ function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) return item.Index ' Select the first non-forced subtitle option in the sorted list else if subtitleMode = "onlyforced" and item.IsForced return item.Index ' Select the first forced subtitle option in the sorted list - else if LCase(userConfig.SubtitlePlaybackMode) = "smart" and (item.isForced or item.IsDefault or item.IsExternal) + else if 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 From 497615e2146b4677997395c5e7feacd175304e0d Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Thu, 4 Jan 2024 20:32:09 -0500 Subject: [PATCH 14/16] Fix variable styles --- components/ItemGrid/LoadVideoContentTask.bs | 25 ++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index ea14db3b6..a06c543ad 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -230,14 +230,14 @@ end sub ' defaultSubtitleTrackFromVid: Identifies the default subtitle track given video id ' -' @param {dynamic} video_id - id of video user is playing +' @param {dynamic} videoID - id of video user is playing ' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found -function defaultSubtitleTrackFromVid(video_id) as integer +function defaultSubtitleTrackFromVid(videoID) as integer if m.global.session.user.configuration.SubtitleMode = "None" return SubtitleSelection.none ' No subtitles desired: return none end if - meta = ItemMetaData(video_id) + meta = ItemMetaData(videoID) if not isValid(meta) then return SubtitleSelection.none if not isValid(meta.json) then return SubtitleSelection.none @@ -246,9 +246,9 @@ function defaultSubtitleTrackFromVid(video_id) as integer 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 <> SubtitleSelection.none - return default_text_subs + defaultTextSubs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text) + if defaultTextSubs <> SubtitleSelection.none + return defaultTextSubs end if if not m.global.session.user.settings["playback.subs.onlytext"] @@ -260,16 +260,15 @@ end function ' defaultSubtitleTrack: ' -' @param {dynamic} sorted_subtitles - array of subtitles sorted by type and language -' @param {boolean} [require_text=false] - indicates if only text subtitles should be considered +' @param {dynamic} sortedSubtitles - array of subtitles sorted by type and language +' @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} is one is not found -function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) as integer +function defaultSubtitleTrack(sortedSubtitles, requireText = false as boolean) as integer userConfig = m.global.session.user.configuration - subtitlePlaybackMode = isValid(userConfig.SubtitlePlaybackMode) ? LCase(userConfig.SubtitlePlaybackMode) : "" subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : "" - for each item in sorted_subtitles + for each item in sortedSubtitles ' Only auto-select subtitle if language matches SubtitleLanguagePreference languageMatch = true if userConfig.SubtitleLanguagePreference <> "" @@ -277,7 +276,7 @@ function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) end if ' Ensure textuality of subtitle matches preferenced passed as arg - matchTextReq = ((require_text and item.IsTextSubtitleStream) or not require_text) + matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText) if languageMatch and matchTextReq if subtitleMode = "default" and (item.isForced or item.IsDefault or item.IsExternal) @@ -286,7 +285,7 @@ function defaultSubtitleTrack(sorted_subtitles, require_text = false as boolean) return item.Index ' Select the first non-forced subtitle option in the sorted list else if subtitleMode = "onlyforced" and item.IsForced return item.Index ' Select the first forced subtitle option in the sorted list - else if subtitlePlaybackMode = "smart" and (item.isForced or item.IsDefault or item.IsExternal) + else if subtitleMode = "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 From 8601adfa14200b9592b4c1d72405e84104ee7675 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:42:35 -0500 Subject: [PATCH 15/16] Improve default subtitle selection logic --- components/ItemGrid/LoadVideoContentTask.bs | 49 ++++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index a06c543ad..833bb9f73 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -245,14 +245,15 @@ function defaultSubtitleTrackFromVid(videoID) as integer if not isValidAndNotEmpty(meta.json.MediaSources[0].MediaStreams) then return SubtitleSelection.none subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams) + selectedAudioLanguage = meta.json.MediaSources[0].MediaStreams[m.top.selectedAudioStreamIndex].Language ?? "" - defaultTextSubs = defaultSubtitleTrack(subtitles["all"], true) ' Find correct subtitle track (forced text) + defaultTextSubs = defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage, true) ' Find correct subtitle track (forced text) if defaultTextSubs <> SubtitleSelection.none return defaultTextSubs end if if not m.global.session.user.settings["playback.subs.onlytext"] - return defaultSubtitleTrack(subtitles["all"]) ' if no appropriate text subs exist, allow non-text + return defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage) ' if no appropriate text subs exist, allow non-text end if return SubtitleSelection.none @@ -261,13 +262,21 @@ 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} is one is not found -function defaultSubtitleTrack(sortedSubtitles, requireText = false as boolean) as integer +function defaultSubtitleTrack(sortedSubtitles, selectedAudioLanguage as string, requireText = false as boolean) as integer userConfig = m.global.session.user.configuration subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : "" + allowSmartMode = false + + ' Only evaluate selected audio language if we have a value + if selectedAudioLanguage <> "" + allowSmartMode = selectedAudioLanguage <> userConfig.SubtitleLanguagePreference + end if + for each item in sortedSubtitles ' Only auto-select subtitle if language matches SubtitleLanguagePreference languageMatch = true @@ -279,20 +288,38 @@ function defaultSubtitleTrack(sortedSubtitles, requireText = false as boolean) a matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText) if languageMatch and matchTextReq - if 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 subtitleMode = "always" and not item.IsForced - return item.Index ' Select the first non-forced subtitle option in the sorted list + if subtitleMode = "default" and (item.isForced or item.IsDefault) + ' Return first forced or default subtitle track + return item.Index + else if subtitleMode = "always" + ' Return the first found subtitle track + return item.Index else if subtitleMode = "onlyforced" and item.IsForced - return item.Index ' Select the first forced subtitle option in the sorted list - else if subtitleMode = "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 first forced subtitle track + return item.Index + else if subtitleMode = "smart" and allowSmartMode + ' Return the first found subtitle track return item.Index end if end if end for + ' User has chosed 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 preferenced 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 From b5ad6384494e3b870335357aef90a1695cb4b0f2 Mon Sep 17 00:00:00 2001 From: 1hitsong <3330318+1hitsong@users.noreply.github.com> Date: Thu, 4 Jan 2024 22:40:31 -0500 Subject: [PATCH 16/16] Update default subtitle logic to work with custom subtitle function --- components/ItemGrid/LoadVideoContentTask.bs | 5 ++- components/video/VideoPlayerView.bs | 47 ++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 833bb9f73..fa2a7d0e2 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -151,13 +151,16 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s end if addSubtitlesToVideo(video, meta) + else + video.SelectedSubtitle = subtitle_idx end if + else + video.SelectedSubtitle = subtitle_idx end if video.videoId = video.id video.mediaSourceId = mediaSourceId video.audioIndex = audio_stream_idx - video.SelectedSubtitle = subtitle_idx video.PlaySessionId = m.playbackInfo.PlaySessionId diff --git a/components/video/VideoPlayerView.bs b/components/video/VideoPlayerView.bs index dfe4eb757..c2a893061 100644 --- a/components/video/VideoPlayerView.bs +++ b/components/video/VideoPlayerView.bs @@ -323,11 +323,6 @@ sub onVideoContentLoaded() m.top.transcodeParams = videoContent[0].transcodeparams m.chapters = videoContent[0].chapters - ' Allow default subtitles - m.top.unobserveField("selectedSubtitle") - m.top.selectedSubtitle = videoContent[0].selectedSubtitle - m.top.observeField("selectedSubtitle", "onSubtitleChange") - m.osd.itemTitleText = m.top.content.title populateChapterMenu() @@ -340,6 +335,29 @@ sub onVideoContentLoaded() m.top.allowCaptions = true end if + ' Allow default subtitles + m.top.unobserveField("selectedSubtitle") + + ' Set subtitleTrack property is subs are natively supported by Roku + selectedSubtitle = invalid + for each subtitle in m.top.fullSubtitleData + if subtitle.Index = videoContent[0].selectedSubtitle + selectedSubtitle = subtitle + exit for + end if + end for + + if isValid(selectedSubtitle) + availableSubtitleTrackIndex = availSubtitleTrackIdx(selectedSubtitle.Track.TrackName) + if availableSubtitleTrackIndex <> -1 + m.top.subtitleTrack = m.top.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName + end if + end if + + m.top.selectedSubtitle = videoContent[0].selectedSubtitle + + m.top.observeField("selectedSubtitle", "onSubtitleChange") + if isValid(m.top.audioIndex) m.top.audioTrack = (m.top.audioIndex + 1).toStr() else @@ -587,6 +605,25 @@ function stateAllowsOSD() as boolean return inArray(validStates, m.top.state) end function + +' availSubtitleTrackIdx: Returns Roku's index for requested subtitle track +' +' @param {string} tracknameToFind - TrackName for subtitle we're looking to match +' @return {integer} indicating Roku's index for requested subtitle track. Returns -1 if not found +function availSubtitleTrackIdx(tracknameToFind as string) as integer + idx = 0 + for each availTrack in m.top.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, tracknameToFind) + return idx + end if + idx = idx + 1 + end for + return -1 +end function + function onKeyEvent(key as string, press as boolean) as boolean ' Keypress handler while user is inside the chapter menu