components_manager_ViewCreator.bs
import "pkg:/source/enums/SubtitleSelection.bs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/mediaDisplayTitle.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/queueBackdropHelper.bs"
import "pkg:/source/utils/translate.bs"
' Play Audio
sub CreateAudioPlayerView()
m.view = CreateObject("roSGNode", "AudioPlayerView")
m.view.observeField("state", "onStateChange")
m.global.sceneManager.callFunc("pushScene", m.view)
end sub
' Play Video
sub CreateVideoPlayerView()
m.playbackData = {}
m.selectedSubtitle = {}
m.view = CreateObject("roSGNode", "VideoPlayerView")
' Keep video player invisible during loading to prevent black background from covering backdrop
m.view.visible = false
m.view.observeField("state", "onStateChange")
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
m.view.observeField("selectAudioPressed", "onSelectAudioPressed")
m.view.observeField("selectVideoSourcePressed", "onSelectVideoSourcePressed")
mediaSourceId = m.global.queueManager.callFunc("getCurrentItem").mediaSourceId
if not isValid(mediaSourceId) or mediaSourceId = ""
mediaSourceId = m.global.queueManager.callFunc("getCurrentItem").id
end if
m.getPlaybackInfoTask = createObject("roSGNode", "GetPlaybackInfoTask")
m.getPlaybackInfoTask.videoID = mediaSourceId
m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded")
' Update backdrop for current queue item (handles first item, shuffle, library play, etc.)
updateQueueBackdrop()
m.global.sceneManager.callFunc("pushScene", m.view)
end sub
' -----------------
' Event Handlers
' -----------------
' onSelectAudioPressed: Display audio selection dialog
'
sub onSelectAudioPressed()
audioData = {
data: []
}
m.log.debug("onSelectAudioPressed", "currentAudioIndex", m.view.audioIndex, "trackCount", m.view.fullAudioData.Count())
for each item in m.view.fullAudioData
audioStreamItem = {
"index": item.index,
"isExternal": item.isExternal,
"track": {
"description": formatAudioDisplayTitle(item)
},
"type": "audioselection"
}
isSelected = m.view.audioIndex = item.index
if isSelected
audioStreamItem.selected = true
end if
m.log.debug("Audio track item", "jellyfinIndex", item.index, "isSelected", isSelected, "title", formatAudioDisplayTitle(item))
audioData.data.push(audioStreamItem)
end for
sceneManager = m.global.sceneManager
sceneManager.callFunc("radioDialog", translate(translationKeys.LabelSelectAudio), audioData)
sceneManager.observeField("returnData", "onSelectionMade")
end sub
' User requested subtitle selection popup
sub onSelectSubtitlePressed()
subtitleData = {
data: []
}
for each item in m.view.fullSubtitleData
item.type = "subtitleselection"
if m.view.selectedSubtitle <> SubtitleSelection.NONE
' Subtitle is a track within the file
if item.index = m.view.selectedSubtitle
item.selected = true
end if
else
' Subtitle is from an external source
availableSubtitleTrackIndex = availSubtitleTrackIdx(item.track.TrackName)
if availableSubtitleTrackIndex <> SubtitleSelection.NONE
' Convert Jellyfin subtitle track name to Roku track name
subtitleFullTrackName = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
if subtitleFullTrackName = m.view.subtitleTrack
item.selected = true
end if
end if
end if
' Put the selected item at the top of the option list
if isValid(item.selected) and item.selected
subtitleData.data.Unshift(item)
else
subtitleData.data.push(item)
end if
end for
' Manually create the None option and place at top
subtitleData.data.Unshift({
"Index": SubtitleSelection.NONE,
"IsExternal": false,
"Track": {
"description": "None"
},
"Type": "subtitleselection"
})
sceneManager = m.global.sceneManager
sceneManager.callFunc("radioDialog", translate(translationKeys.LabelSelectSubtitles), subtitleData)
sceneManager.observeField("returnData", "onSelectionMade")
end sub
' onSelectVideoSourcePressed: Display video source selection dialog
'
sub onSelectVideoSourcePressed()
videoSourceData = {
data: []
}
if not isValid(m.view.fullVideoSourceData) then return
hasMultipleSources = m.view.fullVideoSourceData.Count() > 1
for each source in m.view.fullVideoSourceData
sourceItem = {
"Index": source.Id,
"IsExternal": false,
"Track": {
"description": formatVideoSourceTitle(source, hasMultipleSources)
},
"Type": "videosourceselection"
}
if m.view.mediaSourceId = source.Id
sourceItem.selected = true
end if
videoSourceData.data.push(sourceItem)
end for
sceneManager = m.global.sceneManager
sceneManager.callFunc("radioDialog", translate(translationKeys.LabelSelectVideoSource), videoSourceData)
sceneManager.observeField("returnData", "onSelectionMade")
end sub
' User has selected something from the radioDialog popup
sub onSelectionMade()
sceneManager = m.global.sceneManager
sceneManager.unobserveField("returnData")
if not isValid(sceneManager.returnData) then return
if not isValid(sceneManager.returnData.type) then return
if LCase(sceneManager.returnData.type) = "subtitleselection"
processSubtitleSelection()
return
end if
if LCase(sceneManager.returnData.type) = "audioselection"
processAudioSelection()
return
end if
if LCase(sceneManager.returnData.type) = "videosourceselection"
processVideoSourceSelection()
return
end if
end sub
' processAudioSelection: Audio track selection handler
'
sub processAudioSelection()
selectedAudioTrack = m.global.sceneManager.returnData
if isValid(selectedAudioTrack)
if isValid(selectedAudioTrack.index)
m.log.info("processAudioSelection", "selectedJellyfinIndex", selectedAudioTrack.index, "previousAudioIndex", m.view.audioIndex)
m.view.audioIndex = selectedAudioTrack.index
else
m.log.warn("processAudioSelection: selectedAudioTrack.index is invalid", selectedAudioTrack)
end if
end if
end sub
' processVideoSourceSelection: Video source selection handler
' Triggers video reload with the new MediaSource ID
sub processVideoSourceSelection()
selectedSource = m.global.sceneManager.returnData
if isValid(selectedSource) and isValid(selectedSource.index)
' Only trigger reload if source actually changed
if selectedSource.index <> m.view.mediaSourceId
m.view.mediaSourceId = selectedSource.index
end if
end if
end sub
sub processSubtitleSelection()
m.selectedSubtitle = m.global.sceneManager.returnData
' The selected encoded subtitle did not change.
if m.view.selectedSubtitle <> SubtitleSelection.NONE or m.selectedSubtitle.index <> SubtitleSelection.NONE
if m.view.selectedSubtitle = m.selectedSubtitle.index then return
end if
' The playbackData is now outdated and must be refreshed
m.playbackData = invalid
' Find previously selected subtitle and identify if it was encoded
for each item in m.view.fullSubtitleData
if item.index = m.view.selectedSubtitle
m.view.isPreviousSubtitleEncoded = item.IsEncoded
exit for
end if
end for
if LCase(m.selectedSubtitle.track.description) = "none"
m.view.globalCaptionMode = "Off"
m.view.subtitleTrack = ""
if m.view.selectedSubtitle <> SubtitleSelection.NONE
m.view.selectedSubtitle = SubtitleSelection.NONE
end if
return
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"
' 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 = SubtitleSelection.NONE then return
m.view.subtitleTrack = m.view.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName
end if
m.view.selectedSubtitle = m.selectedSubtitle.Index
end sub
' User requested playback info
sub onSelectPlaybackInfoPressed()
' Check if we already have playback info and show it in a popup
if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
m.global.sceneManager.callFunc("standardDialog", translate(translationKeys.LabelPlaybackInfo), m.playbackData.playbackinfo)
return
end if
' Set cached playback info right before running (when it's actually available)
m.getPlaybackInfoTask.cachedPlaybackInfo = m.view.cachedPlaybackInfo
m.getPlaybackInfoTask.control = "RUN"
end sub
' The playback info task has returned data
sub onPlaybackInfoLoaded()
m.playbackData = m.getPlaybackInfoTask.data
' Check if we have playback info and show it in a popup
if isValid(m.playbackData) and isValid(m.playbackData.playbackinfo)
m.global.sceneManager.callFunc("standardDialog", translate(translationKeys.LabelPlaybackInfo), m.playbackData.playbackinfo)
end if
end sub
' Playback state change event handlers
sub onStateChange()
if LCase(m.view.state) = "finished"
' Don't pop the scene mid-retry; the DoVi fallback calls stop which can fire "finished".
if isValid(m.view.isRetrying) and m.view.isRetrying then return
sceneManager = m.global.sceneManager
queueManager = m.global.queueManager
' Close any open dialogs
if sceneManager.callFunc("isDialogOpen")
sceneManager.callFunc("dismissDialog")
end if
' Check if this is a Live TV channel that should auto-restart
currentItem = queueManager.callFunc("getCurrentItem")
if isValid(currentItem)
currentItemType = queueManager.callFunc("getItemType", currentItem)
if currentItemType = "tvchannel"
' Restart the same channel
sceneManager.callFunc("clearPreviousScene")
queueManager.callFunc("playQueue")
return
end if
end if
' If there is something next in the queue, play it
if queueManager.callFunc("getPosition") < queueManager.callFunc("getCount") - 1
sceneManager.callFunc("clearPreviousScene")
queueManager.callFunc("moveForward")
updateQueueBackdrop()
queueManager.callFunc("playQueue")
return
end if
' Playback completed, return user to previous screen
sceneManager.callFunc("popScene")
m.global.audioPlayer.loopMode = ""
end if
end sub
' 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(tracknameToFind as string) as integer
idx = 0
for each availTrack in m.view.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 SubtitleSelection.NONE
end function