Skip to content

Commit 2006889

Browse files
chore(docs): update code docs
1 parent 0846abe commit 2006889

13 files changed

Lines changed: 407 additions & 21 deletions

docs/components_ItemDetails.bs.html

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,15 +1343,32 @@
13431343
SetUpAudioOptions(allStreams)
13441344
updateOptionsButtonVisibility()
13451345
else if item.type <> "Series" and item.type <> "Season" and item.type <> "Person" and item.type <> "BoxSet" and item.type <> "MusicArtist" and item.type <> "MusicAlbum" and item.type <> "Playlist"
1346-
if m.top.selectedVideoStreamId = "" and isValidAndNotEmpty(item.mediaSourceId)
1346+
mediaSources = invalid
1347+
if isValid(item.mediaSourcesData) and isValidAndNotEmpty(item.mediaSourcesData.mediaSources)
1348+
mediaSources = item.mediaSourcesData.mediaSources
1349+
end if
1350+
1351+
' Select best video source based on device capabilities instead of always using first
1352+
if m.top.selectedVideoStreamId = "" and isValid(mediaSources) and mediaSources.Count() > 0
1353+
bestSourceIndex = findBestVideoSource(mediaSources)
1354+
m.top.selectedVideoStreamId = mediaSources[bestSourceIndex].Id ?? item.mediaSourceId
1355+
else if m.top.selectedVideoStreamId = "" and isValidAndNotEmpty(item.mediaSourceId)
13471356
m.top.selectedVideoStreamId = item.mediaSourceId
13481357
end if
13491358

1359+
' Use the selected source's MediaStreams (not always mediaSources[0])
13501360
allStreams = []
1351-
mediaSources = invalid
1352-
if isValid(item.mediaSourcesData) and isValidAndNotEmpty(item.mediaSourcesData.mediaSources)
1353-
mediaSources = item.mediaSourcesData.mediaSources
1354-
if isValid(mediaSources[0].MediaStreams)
1361+
if isValid(mediaSources)
1362+
for each source in mediaSources
1363+
if source.Id = m.top.selectedVideoStreamId
1364+
if isValid(source.MediaStreams)
1365+
allStreams = source.MediaStreams
1366+
end if
1367+
exit for
1368+
end if
1369+
end for
1370+
' Fallback to first source if selected source not found
1371+
if allStreams.Count() = 0 and isValid(mediaSources[0]) and isValid(mediaSources[0].MediaStreams)
13551372
allStreams = mediaSources[0].MediaStreams
13561373
end if
13571374
end if

docs/components_ItemGrid_LoadVideoContentTask.bs.html

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
id = m.top.itemId
5252
mediaSourceId = invalid
53+
if m.top.mediaSourceId <> "" then mediaSourceId = m.top.mediaSourceId
5354
audio_stream_idx = m.top.selectedAudioStreamIndex
5455
forceTranscoding = m.top.forceTranscoding
5556
bypassDoviPreservation = m.top.bypassDoviPreservation
@@ -92,14 +93,33 @@
9293
mediaSources = meta.mediaSourcesData.mediaSources
9394
end if
9495

96+
' Select best video source if no specific source was requested
97+
if not isValid(mediaSourceId) and isValid(mediaSources) and mediaSources.Count() > 1
98+
bestSourceIndex = findBestVideoSource(mediaSources)
99+
mediaSourceId = mediaSources[bestSourceIndex].Id
100+
m.log.info("Auto-selected video source", "index", bestSourceIndex, "mediaSourceId", mediaSourceId)
101+
end if
102+
95103
' Re-determine audio stream index if no manual selection was made
96104
' Task field = 0 means no manual selection (default value)
97105
' Task field > 0 means user manually selected from MovieDetails/TVListDetails - preserve it
98106
if m.top.selectedAudioStreamIndex = 0
99-
if isValid(mediaSources) and isValid(mediaSources[0].MediaStreams)
107+
' Use the selected source's MediaStreams for audio selection when available
108+
selectedSourceStreams = invalid
109+
if isValid(mediaSourceId) and isValid(mediaSources)
110+
for each source in mediaSources
111+
if source.Id = mediaSourceId and isValid(source.MediaStreams)
112+
selectedSourceStreams = source.MediaStreams
113+
exit for
114+
end if
115+
end for
116+
end if
117+
if isValid(selectedSourceStreams)
100118
' Resolve playDefaultAudioTrack setting (JellyRock override or web client)
101119
playDefault = resolvePlayDefaultAudioTrack(userSettings, userSession.config)
102-
120+
audio_stream_idx = findBestAudioStreamIndex(selectedSourceStreams, playDefault, userSession.config.audioLanguagePreference)
121+
else if isValid(mediaSources) and isValid(mediaSources[0].MediaStreams)
122+
playDefault = resolvePlayDefaultAudioTrack(userSettings, userSession.config)
103123
audio_stream_idx = findBestAudioStreamIndex(mediaSources[0].MediaStreams, playDefault, userSession.config.audioLanguagePreference)
104124
else
105125
' MediaStreams not available - keep existing behavior of using 0
@@ -218,8 +238,9 @@
218238

219239
' Determine final subtitle_idx BEFORE calling ItemPostPlaybackInfo to avoid duplicate API calls.
220240
' defaultSubtitleTrackFromVid() only needs meta and audio_stream_idx, both available at this point.
241+
' Pass mediaSourceId so subtitle selection uses the correct source's streams (not always mediaSources[0]).
221242
if subtitle_idx = SubtitleSelection.notset
222-
subtitle_idx = defaultSubtitleTrackFromVid(meta, audio_stream_idx)
243+
subtitle_idx = defaultSubtitleTrackFromVid(meta, audio_stream_idx, mediaSourceId)
223244
end if
224245

225246
' PlayStart requires the time to be in seconds

docs/components_manager_ViewCreator.bs.html

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
2525
m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
2626
m.view.observeField("selectAudioPressed", "onSelectAudioPressed")
27+
m.view.observeField("selectVideoSourcePressed", "onSelectVideoSourcePressed")
2728

2829
mediaSourceId = m.global.queueManager.callFunc("getCurrentItem").mediaSourceId
2930

@@ -127,6 +128,39 @@
127128
sceneManager.observeField("returnData", "onSelectionMade")
128129
end sub
129130

131+
' onSelectVideoSourcePressed: Display video source selection dialog
132+
'
133+
sub onSelectVideoSourcePressed()
134+
videoSourceData = {
135+
data: []
136+
}
137+
138+
if not isValid(m.view.fullVideoSourceData) then return
139+
140+
hasMultipleSources = m.view.fullVideoSourceData.Count() > 1
141+
142+
for each source in m.view.fullVideoSourceData
143+
sourceItem = {
144+
"Index": source.Id,
145+
"IsExternal": false,
146+
"Track": {
147+
"description": formatVideoSourceTitle(source, hasMultipleSources)
148+
},
149+
"Type": "videosourceselection"
150+
}
151+
152+
if m.view.mediaSourceId = source.Id
153+
sourceItem.selected = true
154+
end if
155+
156+
videoSourceData.data.push(sourceItem)
157+
end for
158+
159+
sceneManager = m.global.sceneManager
160+
sceneManager.callFunc("radioDialog", tr("Select Video Source"), videoSourceData)
161+
sceneManager.observeField("returnData", "onSelectionMade")
162+
end sub
163+
130164
' User has selected something from the radioDialog popup
131165
sub onSelectionMade()
132166
sceneManager = m.global.sceneManager
@@ -144,6 +178,11 @@
144178
processAudioSelection()
145179
return
146180
end if
181+
182+
if LCase(sceneManager.returnData.type) = "videosourceselection"
183+
processVideoSourceSelection()
184+
return
185+
end if
147186
end sub
148187

149188

@@ -159,6 +198,19 @@
159198
end if
160199
end sub
161200

201+
' processVideoSourceSelection: Video source selection handler
202+
' Triggers video reload with the new MediaSource ID
203+
sub processVideoSourceSelection()
204+
selectedSource = m.global.sceneManager.returnData
205+
206+
if isValid(selectedSource) and isValid(selectedSource.index)
207+
' Only trigger reload if source actually changed
208+
if selectedSource.index <> m.view.mediaSourceId
209+
m.view.mediaSourceId = selectedSource.index
210+
end if
211+
end if
212+
end sub
213+
162214
sub processSubtitleSelection()
163215
m.selectedSubtitle = m.global.sceneManager.returnData
164216

docs/components_video_OSD.bs.html

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@
6969
' Cache item reference for use in display methods
7070
m.itemData = item
7171

72+
' Preserve the original item name from the first load.
73+
' Source-switch reloads fetch metadata for an alternate version whose name
74+
' may include year/edition suffixes (e.g., "Dracula (1931) [Colorized]").
75+
if not isValidAndNotEmpty(m.originalItemName)
76+
m.originalItemName = item.name
77+
end if
78+
7279
itemType = item.type
7380

7481
' Determine isMovie/isSeries flags for ratings display settings.
@@ -107,6 +114,13 @@
107114

108115
' Stream counts - read pre-computed values from item node
109116
m.top.numAudioStreams = item.audioStreamCount
117+
' Only update video source count from meta if it's higher than the current value.
118+
' During a source-switch reload, the reloaded meta represents a single MediaSource
119+
' and hasMultipleVideoSources will be false, but the original source list is still valid.
120+
metaVideoSources = item.hasMultipleVideoSources ? 2 : 1
121+
if metaVideoSources > m.top.numVideoSources
122+
m.top.numVideoSources = metaVideoSources
123+
end if
110124

111125
' Runtime — for TvChannel, use the currently-airing program's runtime if available
112126
runTimeTicks = item.runTimeTicks
@@ -149,6 +163,10 @@
149163

150164
' Remove these buttons as needed
151165

166+
' Video Sources - hide for single source or live TV
167+
if m.top.numVideoSources < 2 or m.itemData.type = "TvChannel"
168+
m.buttonMenuLeft.removeChild(m.buttonMenuLeft.findNode("showVideoSourceMenu"))
169+
end if
152170
' Audio Track
153171
if m.top.numAudioStreams < 2
154172
m.buttonMenuLeft.removeChild(m.buttonMenuLeft.findNode("showAudioMenu"))
@@ -208,7 +226,15 @@
208226
if item.type = "TvChannel" and isValid(item.currentProgram) and isValidAndNotEmpty(item.currentProgram.name)
209227
m.videoTitle.text = item.currentProgram.name
210228
else
211-
m.videoTitle.text = item.name
229+
' Use preserved original name (m.originalItemName) to avoid alternate version names
230+
' that may include year/edition suffixes (e.g., "Dracula (1931) [Colorized]").
231+
' Falls back to item.name on first load before the original is cached.
232+
title = m.originalItemName ?? item.name
233+
' Append active video source tag when multiple sources exist (e.g., "[B&W]")
234+
if isValidAndNotEmpty(m.top.videoSourceTag)
235+
title += " " + m.top.videoSourceTag
236+
end if
237+
m.videoTitle.text = title
212238
end if
213239
end sub
214240

docs/components_video_VideoPlayerView.bs.html

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import "pkg:/source/utils/misc.bs"
99
import "pkg:/source/utils/queueBackdropHelper.bs"
1010
import "pkg:/source/utils/session.bs"
11+
import "pkg:/source/utils/streamSelection.bs"
1112
import "pkg:/source/utils/trickplay.bs"
1213

1314
' Child index for position label in Roku's built-in trickPlayBar component.
@@ -37,6 +38,9 @@
3738
m.LoadMetaDataTask.itemId = m.currentItem.id
3839
m.LoadMetaDataTask.itemType = m.currentItem.type
3940
m.LoadMetaDataTask.selectedAudioStreamIndex = m.currentItem.selectedAudioStreamIndex
41+
if isValidAndNotEmpty(m.currentItem.mediaSourceId)
42+
m.LoadMetaDataTask.mediaSourceId = m.currentItem.mediaSourceId
43+
end if
4044
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
4145
m.LoadMetaDataTask.control = "RUN"
4246

@@ -282,6 +286,12 @@
282286
m.top.selectAudioPressed = true
283287
end sub
284288

289+
' handleShowVideoSourceMenuAction: Handles action to show video source selection menu
290+
'
291+
sub handleShowVideoSourceMenuAction()
292+
m.top.selectVideoSourcePressed = true
293+
end sub
294+
285295
' handleShowVideoInfoPopupAction: Handles action to show video info popup
286296
'
287297
sub handleShowVideoInfoPopupAction()
@@ -328,6 +338,11 @@
328338
return
329339
end if
330340

341+
if action = "showvideosourcemenu"
342+
handleShowVideoSourceMenuAction()
343+
return
344+
end if
345+
331346
if action = "showvideoinfopopup"
332347
handleShowVideoInfoPopupAction()
333348
return
@@ -496,6 +511,39 @@
496511
m.LoadMetaDataTask.control = "RUN"
497512
end sub
498513

514+
' onVideoSourceChange: Handles video source switching from OSD
515+
' Saves position, stops playback, re-selects audio for the new source's streams,
516+
' and reloads video content with the new MediaSource ID.
517+
sub onVideoSourceChange()
518+
newSourceId = m.top.mediaSourceId
519+
if not isValidAndNotEmpty(newSourceId) then return
520+
521+
' Save the current video position
522+
m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&)
523+
524+
m.top.control = "stop"
525+
526+
' Re-determine best audio stream for the new source's MediaStreams
527+
audioStreamIdx = 0
528+
if isValid(m.top.fullVideoSourceData)
529+
for each source in m.top.fullVideoSourceData
530+
if source.Id = newSourceId and isValid(source.MediaStreams)
531+
localUser = m.global.user
532+
playDefault = resolvePlayDefaultAudioTrack(localUser.settings, localUser.config)
533+
audioStreamIdx = findBestAudioStreamIndex(source.MediaStreams, playDefault, localUser.config.audioLanguagePreference)
534+
exit for
535+
end if
536+
end for
537+
end if
538+
539+
m.LoadMetaDataTask.selectedSubtitleIndex = -2 ' Reset to auto-detect for new source
540+
m.LoadMetaDataTask.selectedAudioStreamIndex = audioStreamIdx
541+
m.LoadMetaDataTask.itemId = m.currentItem.id
542+
m.LoadMetaDataTask.mediaSourceId = newSourceId
543+
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
544+
m.LoadMetaDataTask.control = "RUN"
545+
end sub
546+
499547
sub onPlaybackErrorDialogClosed(msg)
500548
sourceNode = msg.getRoSGNode()
501549
sourceNode.unobserveField("buttonSelected")
@@ -550,7 +598,6 @@
550598
m.top.PlaySessionId = videoContent[0].PlaySessionId
551599
m.top.videoId = videoContent[0].id
552600
m.top.container = videoContent[0].container
553-
m.top.mediaSourceId = videoContent[0].mediaSourceId
554601
m.top.fullSubtitleData = videoContent[0].fullSubtitleData
555602
m.top.fullAudioData = videoContent[0].fullAudioData
556603
m.top.audioIndex = videoContent[0].audioIndex
@@ -561,12 +608,47 @@
561608
m.top.doviDirectPlayFallbackAvailable = isValid(videoContent[0].doviDirectPlayFallbackAvailable) and videoContent[0].doviDirectPlayFallbackAvailable
562609
m.top.transcodeAvailable = isValid(videoContent[0].transcodeAvailable) and videoContent[0].transcodeAvailable
563610

611+
' Unobserve mediaSourceId before setting to prevent observer loop
612+
m.top.unobserveField("mediaSourceId")
613+
m.top.mediaSourceId = videoContent[0].mediaSourceId
614+
m.top.observeField("mediaSourceId", "onVideoSourceChange")
615+
564616
' Reset retry flags so subsequent reloads (audio/subtitle changes) use fresh evaluation.
565617
m.LoadMetaDataTask.bypassDoviPreservation = false
566618
m.LoadMetaDataTask.forceTranscoding = false
567619

568620
' Pass JellyfinBaseItem node to OSD for typed field access
569621
if isValid(videoContent[0].meta)
622+
' Populate video source data for OSD source switching.
623+
' Only update if the new metadata has more sources than what we already have —
624+
' a source-switch reload fetches metadata for a single MediaSource ID,
625+
' which loses the parent item's full source list.
626+
if isValid(videoContent[0].meta.mediaSourcesData) and isValidAndNotEmpty(videoContent[0].meta.mediaSourcesData.mediaSources)
627+
newSources = videoContent[0].meta.mediaSourcesData.mediaSources
628+
existingCount = 0
629+
if isValid(m.top.fullVideoSourceData) then existingCount = m.top.fullVideoSourceData.Count()
630+
if newSources.Count() >= existingCount
631+
m.top.fullVideoSourceData = newSources
632+
end if
633+
end if
634+
' Pre-set video source count on OSD BEFORE itemData triggers onItemDataChanged().
635+
' A source-switch reload returns metadata for a single MediaSource, so
636+
' hasMultipleVideoSources would be false. Override with the preserved count
637+
' so setButtonStates() keeps the video source button.
638+
if isValid(m.top.fullVideoSourceData) and m.top.fullVideoSourceData.Count() > 1
639+
m.osd.numVideoSources = m.top.fullVideoSourceData.Count()
640+
' Set the active source's name tag for the OSD title (e.g., "[B&W]", "[Colorized]")
641+
currentSourceId = videoContent[0].mediaSourceId
642+
m.osd.videoSourceTag = ""
643+
for each source in m.top.fullVideoSourceData
644+
if source.Id = currentSourceId and isValidAndNotEmpty(source.Name)
645+
m.osd.videoSourceTag = source.Name
646+
exit for
647+
end if
648+
end for
649+
else
650+
m.osd.videoSourceTag = ""
651+
end if
570652
m.osd.itemData = videoContent[0].meta
571653
end if
572654

@@ -1241,6 +1323,7 @@
12411323
m.top.unobserveField("content")
12421324
m.top.unobserveField("selectedSubtitle")
12431325
m.top.unobserveField("audioIndex")
1326+
m.top.unobserveField("mediaSourceId")
12441327
m.top.unobserveField("allowCaptions")
12451328
m.top.unobserveField("subtitleTrack")
12461329
m.top.unobserveField("globalCaptionMode")

docs/data/search.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-Subtitles.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-VideoPlayerView.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-ViewCreator.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-streamSelection.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)