source_utils_quickplay.bs
import "pkg:/source/utils/nodeHelpers.bs"
import "pkg:/source/utils/streamSelection.bs"
import "pkg:/source/utils/versionResume.bs"
' Render-thread-safe Quick Play helpers.
' API-calling quickplay logic lives in QuickPlayTask (components/tasks/QuickPlayTask.bs).
namespace quickplay
' Takes an array of items and adds to global queue.
' Also shuffles the playlist if asked
sub pushToQueue(queueArray as object, shufflePlay = false as boolean)
if isValidAndNotEmpty(queueArray)
for each item in queueArray
m.global.queueManager.callFunc("push", nodeHelpers.createQueueItem(item))
end for
' shuffle the playlist if asked
if shufflePlay and m.global.queueManager.callFunc("getCount") > 1
m.global.queueManager.callFunc("toggleShuffle")
end if
end if
end sub
' A single video file: a movie, episode, recording, video or music video.
sub video(itemNode as object)
if not isValid(itemNode) or not isValid(itemNode.id) or itemNode.id = "" then return
' Create a queue-specific item to avoid modifying the UI node
' This prevents the quickPlayNode field observer from firing twice
queueItem = nodeHelpers.createQueueItem(itemNode)
if not isValid(queueItem) then return
' Select the video source. On a per-version server the choice needs each version's own
' position, which cannot be read from here, so it is left to LoadVideoContentTask; without
' per-version positions any version resumes at the item's one position, so the best for the
' device is picked here. A source chosen here travels as mediaSourceId, which the loader
' uses without overwriting the base item ID needed for metadata/reporting.
audioStreams = itemNode.audioStreams
deferVersionToLoader = false
if isValid(itemNode.mediaSourcesData) and isValidAndNotEmpty(itemNode.mediaSourcesData.mediaSources)
mediaSources = itemNode.mediaSourcesData.mediaSources
if mediaSources.Count() > 1
if versionResume.isPerVersionServer(m.global.server.version)
' Which version resumes depends on every version's own position, and each of those
' is a request, impossible here, on the render thread with no fetching. So hand the
' choice to LoadVideoContentTask, which makes it with the positions in hand.
'
' Standing down means actively clearing the id, not only declining to set one: the
' transformer pre-fills item.mediaSourceId with MediaSources[0].Id and
' createQueueItem copies it, so a quick play would otherwise arrive at the loader
' looking like an explicit pick and skip the choice altogether. The details screen's
' real pick still travels as selectedVideoStreamId and is still honored.
deferVersionToLoader = true
' Free signal, no request. The loader needs to know this is a resume BEFORE it has
' chosen a version: startingPoint cannot say so when the item's own position is 0
' and an alternate holds one.
queueItem.hasVersionInProgress = versionResume.anyVersionInProgress(itemNode.id, itemNode.playbackPositionTicks, mediaSources, true)
else
' One position per item, so any version resumes at it: take the best for the device.
bestSource = mediaSources[findBestVideoSource(mediaSources)]
if isValidAndNotEmpty(bestSource.Id)
queueItem.mediaSourceId = bestSource.Id
end if
' Use the selected source's audio streams for best audio selection
if isValid(bestSource.MediaStreams)
audioStreams = []
for each stream in bestSource.MediaStreams
if isValid(stream.Type) and LCase(stream.Type) = "audio"
audioStreams.push(stream)
end if
end for
end if
end if
end if
end if
if deferVersionToLoader then queueItem.mediaSourceId = ""
' Get user session for audio selection
localUser = m.global.user
' audioStreams is pre-filtered to audio-only by the transformer (or re-filtered above).
' findBestAudioStreamIndex filters by Type="audio" internally, so passing
' pre-filtered audio streams works correctly.
' Left at 0 when the version is the loader's to choose: 0 is what tells it to pick the
' audio track itself, and any index decided here would belong to the item's OWN version's
' stream list rather than the one it ends up playing. Likewise when the pick needs the
' series' original language, which only the loader can fetch.
audioStreamIndex = 0
if isValidAndNotEmpty(audioStreams) and not deferVersionToLoader and not needsSeriesOriginalLanguage(itemNode, localUser.settings, localUser.config)
audioStreamIndex = selectAudioStreamIndex(audioStreams, localUser.settings, localUser.config, itemNode.originalLanguage ?? "")
end if
queueItem.selectedAudioStreamIndex = audioStreamIndex
' The item's saved position: on a per-version server the loader may resume another version
' at that version's own position instead.
nodeHelpers.setResumeStart(queueItem, itemNode.playbackPositionTicks)
m.global.queueManager.callFunc("push", queueItem)
end sub
' A single audio file.
sub audio(itemNode as object)
if not isValid(itemNode) or not isValid(itemNode.id) then return
' Create a queue-specific item to avoid modifying the UI node
queueItem = nodeHelpers.createQueueItem(itemNode)
if not isValid(queueItem) then return
m.global.queueManager.callFunc("push", queueItem)
end sub
' A single photo.
'
' Route to the /photo view. This runs on the QueueManager render thread (no router chain), so
' it signals the launch via
' m.global.photoLaunchRequest; JRScene turns that into navigateTo("/photo") with the AA as
' route context. itemsNode IS the single photo node; PhotoDetails reads it on mount.
sub photo(itemNode as object)
if not isValid(itemNode) or not isValid(itemNode.id) then return
m.global.photoLaunchRequest = { itemsNode: itemNode, itemIndex: 0 }
end sub
' Quick Play A TVChannel
sub tvChannel(itemNode as object)
if not isValid(itemNode) or not isValidAndNotEmpty(itemNode.id)
print "ERROR: quickplay.tvChannel() - missing itemNode or id"
stopLoadingSpinner()
return
end if
' Push TV channel queue item; keep spinner active until video content loads
m.global.queueManager.callFunc("push", nodeHelpers.createQueueItem(itemNode))
end sub
' Quick Play A Live Program
sub program(itemNode as object)
if not isValid(itemNode) or not isValidAndNotEmpty(itemNode.channelId)
print "ERROR: quickplay.program() - missing ChannelId"
stopLoadingSpinner()
return
end if
' Play the channel this program is on, not the program itself.
' Override id and type on the queue item so the playback pipeline targets the channel.
' Keep spinner active until video content loads.
queueItem = nodeHelpers.createQueueItem(itemNode)
queueItem.id = itemNode.channelId
queueItem.type = "TvChannel"
m.global.queueManager.callFunc("push", queueItem)
end sub
end namespace