Merge pull request #1253 from 1hitsong/GlobalVideoPlayer-Part1

This commit is contained in:
Charles Ewert 2023-05-11 09:25:27 -04:00 committed by GitHub
commit 4f79597166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 657 additions and 679 deletions

View File

@ -8,25 +8,25 @@ sub init()
m.top.functionName = "getPlaybackInfoTask" m.top.functionName = "getPlaybackInfoTask"
end sub end sub
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger) function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string, audioTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
currentView = m.global.sceneManager.callFunc("getActiveScene")
currentItem = m.global.queueManager.callFunc("getCurrentItem")
body = { body = {
"DeviceProfile": getDeviceProfile() "DeviceProfile": getDeviceProfile()
} }
params = { params = {
"UserId": get_setting("active_user"), "UserId": get_setting("active_user"),
"StartTimeTicks": startTimeTicks, "StartTimeTicks": currentItem.startingPoint,
"IsPlayback": true, "IsPlayback": true,
"AutoOpenLiveStream": true, "AutoOpenLiveStream": true,
"MaxStreamingBitrate": "140000000", "MaxStreamingBitrate": "140000000",
"MaxStaticBitrate": "140000000", "MaxStaticBitrate": "140000000",
"SubtitleStreamIndex": subtitleTrackIndex "SubtitleStreamIndex": currentView.selectedSubtitle,
"MediaSourceId": currentItem.id,
"AudioStreamIndex": currentItem.selectedAudioStreamIndex
} }
mediaSourceId = id
if mediaSourceId <> "" then params.MediaSourceId = mediaSourceId
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params) req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
req.SetRequest("POST") req.SetRequest("POST")
return postJson(req, FormatJson(body)) return postJson(req, FormatJson(body))

View File

@ -10,30 +10,46 @@ import "pkg:/source/utils/deviceCapabilities.brs"
sub init() sub init()
m.top.functionName = "loadItems" m.top.functionName = "loadItems"
m.top.limit = 60
usersettingLimit = get_user_setting("itemgrid.Limit")
if isValid(usersettingLimit)
m.top.limit = usersettingLimit
end if
end sub end sub
sub loadItems() sub loadItems()
m.top.content = [LoadItems_VideoPlayer(m.top.itemId)] ' Reset intro tracker in case task gets reused
m.top.isIntro = false
' Only show preroll once per queue
if m.global.queueManager.callFunc("isPrerollActive")
' Prerolls not allowed if we're resuming video
if m.global.queueManager.callFunc("getCurrentItem").startingPoint = 0
preRoll = GetIntroVideos(m.top.itemId)
if isValid(preRoll) and preRoll.TotalRecordCount > 0 and isValid(preRoll.items[0])
' If an error is thrown in the Intros plugin, instead of passing the error they pass the entire rick roll music video.
' Bypass the music video and treat it as an error message
if lcase(preRoll.items[0].name) <> "rick roll'd"
m.global.queueManager.callFunc("push", m.global.queueManager.callFunc("getCurrentItem"))
m.top.itemId = preRoll.items[0].id
m.global.queueManager.callFunc("setPrerollStatus", false)
m.top.isIntro = true
end if
end if
end if
end if
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)]
end sub 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, showIntro = true as boolean, allowResumeDialog = true as boolean) as dynamic 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
video = {} video = {}
video.id = id video.id = id
video.content = createObject("RoSGNode", "ContentNode") video.content = createObject("RoSGNode", "ContentNode")
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro, allowResumeDialog) LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)
if video.errorMsg = "introaborted"
return video
end if
if video.content = invalid if video.content = invalid
return invalid return invalid
@ -42,11 +58,12 @@ function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic,
return video return video
end function end function
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, playbackPosition = -1 as integer, forceTranscoding = false as boolean, showIntro = true as boolean, allowResumeDialog = true as boolean) sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, forceTranscoding = false as boolean)
meta = ItemMetaData(video.id) meta = ItemMetaData(video.id)
if not isValid(meta) if not isValid(meta)
video.errorMsg = "Error loading metadata"
video.content = invalid video.content = invalid
return return
end if end if
@ -60,42 +77,24 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
video.content.title = meta.title video.content.title = meta.title
video.showID = meta.showID video.showID = meta.showID
if playbackPosition = -1 user = AboutMe()
playbackPosition = meta.json.UserData.PlaybackPositionTicks if user.Configuration.EnableNextEpisodeAutoPlay
if allowResumeDialog if LCase(m.top.itemType) = "episode"
if playbackPosition > 0 addNextEpisodesToQueue(video.showID)
dialogResult = startPlayBackOver(playbackPosition)
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
if dialogResult.indexselected = -1
'User pressed back, return invalid and don't load video
video.content = invalid
return
else if dialogResult.indexselected = 1
'Start Over selected, change position to 0
playbackPosition = 0
end if
end if
end if end if
end if end if
' For phase 1 of playlist support, we don't support intros yet playbackPosition = 0!
showIntro = false
' Don't attempt to play an intro for an intro video currentItem = m.global.queueManager.callFunc("getCurrentItem")
if showIntro
' Do not play intros when resuming playback if isValid(currentItem) and isValid(currentItem.startingPoint)
if playbackPosition = 0 playbackPosition = currentItem.startingPoint
if not PlayIntroVideo(video.id, audio_stream_idx)
video.errorMsg = "introaborted"
return
end if
end if
end if end if
' PlayStart requires the time to be in seconds
video.content.PlayStart = int(playbackPosition / 10000000) video.content.PlayStart = int(playbackPosition / 10000000)
if not isValid(mediaSourceId) then mediaSourceId = video.id if not isValid(mediaSourceId) then mediaSourceId = video.id
if meta.live then mediaSourceId = "" if meta.live then mediaSourceId = ""
@ -105,6 +104,7 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
video.audioIndex = audio_stream_idx video.audioIndex = audio_stream_idx
if not isValid(m.playbackInfo) if not isValid(m.playbackInfo)
video.errorMsg = "Error loading playback info"
video.content = invalid video.content = invalid
return return
end if end if
@ -163,6 +163,7 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid
' If server does not provide a transcode URL, display a message to the user ' If server does not provide a transcode URL, display a message to the user
m.global.sceneManager.callFunc("userMessage", tr("Error Getting Playback Information"), tr("An error was encountered while playing this item. Server did not provide required transcoding data.")) m.global.sceneManager.callFunc("userMessage", tr("Error Getting Playback Information"), tr("An error was encountered while playing this item. Server did not provide required transcoding data."))
video.errorMsg = "Error getting playback information"
video.content = invalid video.content = invalid
return return
end if end if
@ -175,9 +176,7 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
video.content.setCertificatesFile("common:/certs/ca-bundle.crt") 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 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 video.SelectedSubtitle = subtitle_idx
' is enabled/will be enabled, indexed on the provided list of subtitles
video.SelectedSubtitle = setupSubtitle(video, video.Subtitles, subtitle_idx)
if not fully_external if not fully_external
video.content = authorize_request(video.content) video.content = authorize_request(video.content)
@ -222,18 +221,19 @@ end sub
sub addSubtitlesToVideo(video, meta) sub addSubtitlesToVideo(video, meta)
subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams) subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
safesubs = subtitles["all"]
subtitleTracks = []
if get_user_setting("playback.subs.onlytext") = "true" if get_user_setting("playback.subs.onlytext") = "true"
safesubs = [] safesubs = subtitles["text"]
for each subtitle in subtitles["all"]
if subtitle["IsTextSubtitleStream"]
safesubs.push(subtitle)
end if
end for
video.Subtitles = safesubs
else
video.Subtitles = subtitles["all"]
end if end if
video.content.SubtitleTracks = subtitles["text"]
for each subtitle in safesubs
subtitleTracks.push(subtitle.track)
end for
video.content.SubtitleTracks = subtitleTracks
video.fullSubtitleData = safesubs
end sub end sub
@ -252,26 +252,6 @@ function getTranscodeReasons(url as string) as object
return [] return []
end function end function
'Opens dialog asking user if they want to resume video or start playback over only on the home screen
function startPlayBackOver(time as longinteger)
' If we're inside a play queue, start the episode from the beginning
if m.global.queueManager.callFunc("getCount") > 1 then return { indexselected: 1 }
resumeData = [
"Resume playing at " + ticksToHuman(time) + ".",
"Start over from the beginning."
]
m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), ["Choose an option"], resumeData)
while not isValid(m.global.sceneManager.returnData)
end while
return m.global.sceneManager.returnData
end function
function directPlaySupported(meta as object) as boolean function directPlaySupported(meta as object) as boolean
devinfo = CreateObject("roDeviceInfo") devinfo = CreateObject("roDeviceInfo")
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
@ -314,306 +294,52 @@ function getContainerType(meta as object) as string
return container return container
end function end function
function getAudioFormat(meta as object) as string ' Add next episodes to the playback queue
' Determine the codec of the audio file source sub addNextEpisodesToQueue(showID)
if meta.json.mediaSources = invalid then return "" ' Don't queue next episodes if we already have a playback queue
maxQueueCount = 1
audioInfo = getAudioInfo(meta) if m.top.isIntro
if audioInfo.count() = 0 or audioInfo[0].codec = invalid then return "" maxQueueCount = 2
return audioInfo[0].codec end if
end function
function getAudioInfo(meta as object) as object if m.global.queueManager.callFunc("getCount") > maxQueueCount then return
' Return audio metadata for a given stream
results = []
for each source in meta.json.mediaSources[0].mediaStreams
if source["type"] = "Audio"
results.push(source)
end if
end for
return results
end function
sub autoPlayNextEpisode(videoID as string, showID as string) videoID = m.top.itemId
' use web client setting
if m.user.Configuration.EnableNextEpisodeAutoPlay
' query API for next episode ID
url = Substitute("Shows/{0}/Episodes", showID)
urlParams = { "UserId": get_setting("active_user") }
urlParams.Append({ "StartItemId": videoID })
urlParams.Append({ "Limit": 2 })
resp = APIRequest(url, urlParams)
data = getJson(resp)
if isValid(data) and data.Items.Count() = 2 ' If first item is an intro video, use the next item in the queue
' setup new video node if m.top.isIntro
nextVideo = invalid currentVideo = m.global.queueManager.callFunc("getItemByIndex", 1)
' remove last videoplayer scene
m.global.sceneManager.callFunc("clearPreviousScene") if isValid(currentVideo) and isValid(currentVideo.id)
if isValid(nextVideo) videoID = currentVideo.id
m.global.sceneManager.callFunc("pushScene", nextVideo)
else ' Override showID value since it's for the intro video
m.global.sceneManager.callFunc("popScene") meta = ItemMetaData(videoID)
if isValid(meta)
showID = meta.showID
end if end if
else
' can't play next episode
m.global.sceneManager.callFunc("popScene")
end if end if
else end if
m.global.sceneManager.callFunc("popScene")
url = Substitute("Shows/{0}/Episodes", showID)
urlParams = { "UserId": get_setting("active_user") }
urlParams.Append({ "StartItemId": videoID })
urlParams.Append({ "Limit": 50 })
resp = APIRequest(url, urlParams)
data = getJson(resp)
if isValid(data) and data.Items.Count() > 1
for i = 1 to data.Items.Count() - 1
m.global.queueManager.callFunc("push", data.Items[i])
end for
end if end if
end sub end sub
' Returns an array of playback info to be displayed during playback.
' In the future, with a custom playback info view, we can return an associated array.
function GetPlaybackInfo()
sessions = api_API().sessions.get()
if isValid(sessions) and sessions.Count() > 0
return GetTranscodingStats(sessions[0])
end if
errMsg = tr("Unable to get playback information")
return [errMsg]
end function
function GetTranscodingStats(session)
sessionStats = []
if isValid(session.TranscodingInfo) and session.TranscodingInfo.Count() > 0
transcodingReasons = session.TranscodingInfo.TranscodeReasons
videoCodec = session.TranscodingInfo.VideoCodec
audioCodec = session.TranscodingInfo.AudioCodec
totalBitrate = session.TranscodingInfo.Bitrate
audioChannels = session.TranscodingInfo.AudioChannels
if isValid(transcodingReasons) and transcodingReasons.Count() > 0
sessionStats.push("** " + tr("Transcoding Information") + " **")
for each item in transcodingReasons
sessionStats.push(tr("Reason") + ": " + item)
end for
end if
if isValid(videoCodec)
data = tr("Video Codec") + ": " + videoCodec
if session.TranscodingInfo.IsVideoDirect
data = data + " (" + tr("direct") + ")"
end if
sessionStats.push(data)
end if
if isValid(audioCodec)
data = tr("Audio Codec") + ": " + audioCodec
if session.TranscodingInfo.IsAudioDirect
data = data + " (" + tr("direct") + ")"
end if
sessionStats.push(data)
end if
if isValid(totalBitrate)
data = tr("Total Bitrate") + ": " + getDisplayBitrate(totalBitrate)
sessionStats.push(data)
end if
if isValid(audioChannels)
data = tr("Audio Channels") + ": " + Str(audioChannels)
sessionStats.push(data)
end if
end if
if havePlaybackInfo()
stream = m.playbackInfo.mediaSources[0].MediaStreams[0]
sessionStats.push("** " + tr("Stream Information") + " **")
if isValid(stream.Container)
data = tr("Container") + ": " + stream.Container
sessionStats.push(data)
end if
if isValid(stream.Size)
data = tr("Size") + ": " + stream.Size
sessionStats.push(data)
end if
if isValid(stream.BitRate)
data = tr("Bit Rate") + ": " + getDisplayBitrate(stream.BitRate)
sessionStats.push(data)
end if
if isValid(stream.Codec)
data = tr("Codec") + ": " + stream.Codec
sessionStats.push(data)
end if
if isValid(stream.CodecTag)
data = tr("Codec Tag") + ": " + stream.CodecTag
sessionStats.push(data)
end if
if isValid(stream.VideoRangeType)
data = tr("Video range type") + ": " + stream.VideoRangeType
sessionStats.push(data)
end if
if isValid(stream.PixelFormat)
data = tr("Pixel format") + ": " + stream.PixelFormat
sessionStats.push(data)
end if
if isValid(stream.Width) and isValid(stream.Height)
data = tr("WxH") + ": " + Str(stream.Width) + " x " + Str(stream.Height)
sessionStats.push(data)
end if
if isValid(stream.Level)
data = tr("Level") + ": " + Str(stream.Level)
sessionStats.push(data)
end if
end if
return sessionStats
end function
function havePlaybackInfo()
if not isValid(m.playbackInfo)
return false
end if
if not isValid(m.playbackInfo.mediaSources)
return false
end if
if m.playbackInfo.mediaSources.Count() <= 0
return false
end if
if not isValid(m.playbackInfo.mediaSources[0].MediaStreams)
return false
end if
if m.playbackInfo.mediaSources[0].MediaStreams.Count() <= 0
return false
end if
return true
end function
function getDisplayBitrate(bitrate)
if bitrate > 1000000
return Str(Fix(bitrate / 1000000)) + " Mbps"
else
return Str(Fix(bitrate / 1000)) + " Kbps"
end if
end function
' 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
meta = ItemMetaData(video_id)
if isValid(meta) and isValid(meta.json) and isValid(meta.json.mediaSources)
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
if get_user_setting("playback.subs.onlytext") = "false"
return defaultSubtitleTrack(subtitles["all"]) ' if no appropriate text subs exist, allow non-text
else
return -1
end if
end if
end if
' No valid mediaSources (i.e. LiveTV)
return -1
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[availSubtitleTrackIdx(video, subtitleSelIdx)].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
'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top 'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top
function sortSubtitles(id as string, MediaStreams) function sortSubtitles(id as string, MediaStreams)
m.user = AboutMe() m.user = AboutMe()
tracks = { "forced": [], "default": [], "normal": [] } tracks = { "forced": [], "default": [], "normal": [], "text": [] }
'Too many args for using substitute 'Too many args for using substitute
prefered_lang = m.user.Configuration.SubtitleLanguagePreference prefered_lang = m.user.Configuration.SubtitleLanguagePreference
for each stream in MediaStreams for each stream in MediaStreams
@ -637,6 +363,8 @@ function sortSubtitles(id as string, MediaStreams)
trackType = "forced" trackType = "forced"
else if stream.IsDefault else if stream.IsDefault
trackType = "default" trackType = "default"
else if stream.IsTextSubtitleStream
trackType = "text"
else else
trackType = "normal" trackType = "normal"
end if end if
@ -650,14 +378,9 @@ function sortSubtitles(id as string, MediaStreams)
tracks["default"].append(tracks["normal"]) tracks["default"].append(tracks["normal"])
tracks["forced"].append(tracks["default"]) tracks["forced"].append(tracks["default"])
tracks["forced"].append(tracks["text"])
textTracks = [] return { "all": tracks["forced"], "text": tracks["text"] }
for i = 0 to tracks["forced"].count() - 1
if tracks["forced"][i].IsTextSubtitleStream
textTracks.push(tracks["forced"][i].Track)
end if
end for
return { "all": tracks["forced"], "text": textTracks }
end function end function
function getSubtitleLanguages() function getSubtitleLanguages()
@ -1153,122 +876,3 @@ function getSubtitleLanguages()
"zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki" "zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
} }
end function end function
function CreateSeasonDetailsGroup(series, season)
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.seasonData = ItemMetaData(season.id).json
group.objects = TVEpisodes(series.id, season.id)
group.observeField("episodeSelected", m.port)
group.observeField("quickPlayNode", m.port)
return group
end function
function PlayIntroVideo(video_id, audio_stream_idx) as boolean
' Intro videos only play if user has cinema mode setting enabled
if get_user_setting("playback.cinemamode") = "true"
' Check if server has intro videos setup and available
introVideos = GetIntroVideos(video_id)
if introVideos = invalid then return true
if introVideos.TotalRecordCount > 0
' Bypass joke pre-roll
if lcase(introVideos.items[0].name) = "rick roll'd" then return true
introVideo = LoadItems_VideoPlayer(introVideos.items[0].id, introVideos.items[0].id, audio_stream_idx, defaultSubtitleTrackFromVid(video_id), false, false)
port = CreateObject("roMessagePort")
introVideo.observeField("state", port)
m.global.sceneManager.callFunc("pushScene", introVideo)
introPlaying = true
while introPlaying
msg = wait(0, port)
if type(msg) = "roSGNodeEvent"
if msg.GetData() = "finished"
m.global.sceneManager.callFunc("clearPreviousScene")
introPlaying = false
else if msg.GetData() = "stopped"
introPlaying = false
return false
end if
end if
end while
end if
end if
return true
end function
function CreateMovieDetailsGroup(movie)
group = CreateObject("roSGNode", "MovieDetails")
group.overhangTitle = movie.title
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
movieMetaData = ItemMetaData(movie.id)
group.itemContent = movieMetaData
group.trailerAvailable = false
activeUser = get_setting("active_user")
trailerData = invalid
if isValid(activeUser) and isValid(movie.id)
trailerData = api_API().users.getlocaltrailers(activeUser, movie.id)
end if
if isValid(trailerData)
group.trailerAvailable = trailerData.Count() > 0
end if
buttons = group.findNode("buttons")
for each b in buttons.getChildren(-1, 0)
b.observeField("buttonSelected", m.port)
end for
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
extras.callFunc("loadParts", movieMetaData.json)
return group
end function
function CreateSeriesDetailsGroup(series)
' Get season data early in the function so we can check number of seasons.
seasonData = TVSeasons(series.id)
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
if get_user_setting("ui.tvshows.goStraightToEpisodeListing") = "true" and seasonData.Items.Count() = 1
return CreateSeasonDetailsGroupByID(series.id, seasonData.Items[0].id)
end if
group = CreateObject("roSGNode", "TVShowDetails")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.itemContent = ItemMetaData(series.id)
group.seasonData = seasonData ' Re-use variable from beginning of function
group.observeField("seasonSelected", m.port)
extras = group.findNode("extrasGrid")
extras.observeField("selectedItem", m.port)
extras.callFunc("loadParts", group.itemcontent.json)
return group
end function
function CreateSeasonDetailsGroupByID(seriesID, seasonID)
group = CreateObject("roSGNode", "TVEpisodes")
group.optionsAvailable = false
m.global.sceneManager.callFunc("pushScene", group)
group.seasonData = ItemMetaData(seasonID).json
group.objects = TVEpisodes(seriesID, seasonID)
group.observeField("episodeSelected", m.port)
group.observeField("quickPlayNode", m.port)
return group
end function

View File

@ -3,9 +3,11 @@
<component name="LoadVideoContentTask" extends="Task"> <component name="LoadVideoContentTask" extends="Task">
<interface> <interface>
<field id="itemId" type="string" /> <field id="itemId" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="0" />
<field id="selectedSubtitleIndex" type="integer" value="-1" />
<field id="isIntro" type="boolean" />
<field id="startIndex" type="integer" value="0" /> <field id="startIndex" type="integer" value="0" />
<field id="itemType" type="string" value="" /> <field id="itemType" type="string" value="" />
<field id="limit" type="integer" value="60" />
<field id="metadata" type="assocarray" /> <field id="metadata" type="assocarray" />
<field id="sortField" type="string" value="SortName" /> <field id="sortField" type="string" value="SortName" />
<field id="sortAscending" type="boolean" value="true" /> <field id="sortAscending" type="boolean" value="true" />

View File

@ -1,35 +1,138 @@
import "pkg:/source/utils/misc.brs" import "pkg:/source/utils/misc.brs"
sub init() sub init()
m.content = m.top.findNode("content") m.contentArea = m.top.findNode("contentArea")
m.top.observeField("contentData", "onContentDataChanged") m.radioOptions = m.top.findNode("radioOptions")
m.scrollBarColumn = []
m.top.observeField("contentData", "onContentDataChanged")
m.top.observeFieldScoped("buttonSelected", "onButtonSelected") m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
m.radioOptions.observeField("focusedChild", "onItemFocused")
m.top.id = "OKDialog" m.top.id = "OKDialog"
m.top.height = 900 m.top.height = 900
m.top.title = "What's New?"
m.top.buttons = [tr("OK")]
end sub end sub
' Event handler for when user selected a button
sub onButtonSelected() sub onButtonSelected()
if m.top.buttonSelected = 0 if m.top.buttonSelected = 0
m.global.sceneManager.returnData = m.top.contentData.data[m.content.selectedIndex] m.global.sceneManager.returnData = m.top.contentData.data[m.radioOptions.selectedIndex]
end if
end sub
' Event handler for when user's cursor highlights an option in the option list
sub onItemFocused()
focusedChild = m.radioOptions.focusedChild
if not isValid(focusedChild) then return
moveScrollBar()
' If the option list is scrollable, move the option list to the user's section
if m.scrollBarColumn.count() <> 0
hightedButtonTranslation = m.radioOptions.focusedChild.translation
m.radioOptions.translation = [m.radioOptions.translation[0], -1 * hightedButtonTranslation[1]]
end if
end sub
' Move the popup's scroll bar
sub moveScrollBar()
' If we haven't found the scrollbar column node yet, try to find it now
if m.scrollBarColumn.count() = 0
scrollBar = findNodeBySubtype(m.contentArea, "StdDlgScrollbar")
if scrollBar.count() = 0 or not isValid(scrollBar[0]) or not isValid(scrollBar[0].node)
return
end if
m.scrollBarColumn = findNodeBySubtype(scrollBar[0].node, "Poster")
if m.scrollBarColumn.count() = 0 or not isValid(m.scrollBarColumn[0]) or not isValid(m.scrollBarColumn[0].node)
return
end if
m.scrollBarThumb = findNodeBySubtype(m.scrollBarColumn[0].node, "Poster")
if m.scrollBarThumb.count() = 0 or not isValid(m.scrollBarThumb[0]) or not isValid(m.scrollBarThumb[0].node)
return
end if
m.scrollBarThumb[0].node.blendColor = "#444444"
' If the user presses left then right, it's possible for us to lose focus. Ensure focus stays on the option list.
scrollBar[0].node.observeField("focusedChild", "onScrollBarFocus")
' Hide the default scrollbar background
m.scrollBarColumn[0].node.uri = ""
' Create a new scrollbar background so we can move the original nodes freely
scrollbarBackground = createObject("roSGNode", "Rectangle")
scrollbarBackground.color = "#101010"
scrollbarBackground.opacity = "0.3"
scrollbarBackground.width = "30"
scrollbarBackground.height = m.contentArea.clippingRect.height
scrollbarBackground.translation = [0, 0]
scrollBar[0].node.insertChild(scrollbarBackground, 0)
' Determine the proper scroll amount for the scrollbar
m.scrollAmount = (m.contentArea.clippingRect.height - int(m.scrollBarThumb[0].node.height)) / m.radioOptions.getChildCount()
m.scrollAmount += m.scrollAmount / m.radioOptions.getChildCount()
end if
if not isvalid(m.radioOptions.focusedChild.id) then return
m.scrollBarColumn[0].node.translation = [0, val(m.radioOptions.focusedChild.id) * m.scrollAmount]
end sub
' If somehow the scrollbar gains focus, set focus back to the option list
sub onScrollBarFocus()
m.radioOptions.setFocus(true)
' Ensure scrollbar styles remain in an unfocused state
m.scrollBarThumb[0].node.blendColor = "#353535"
end sub
' Once user selected an item, move cursor down to OK button
sub onItemSelected()
buttonArea = findNodeBySubtype(m.top, "StdDlgButtonArea")
if buttonArea.count() <> 0 and isValid(buttonArea[0]) and isValid(buttonArea[0].node)
buttonArea[0].node.setFocus(true)
end if end if
end sub end sub
sub onContentDataChanged() sub onContentDataChanged()
i = 0 i = 0
for each item in m.top.contentData.data for each item in m.top.contentData.data
cardItem = m.content.CreateChild("StdDlgActionCardItem") cardItem = m.radioOptions.CreateChild("StdDlgActionCardItem")
cardItem.iconType = "radiobutton" cardItem.iconType = "radiobutton"
cardItem.id = i
if isValid(item.selected) if isValid(item.selected)
m.content.selectedIndex = i m.radioOptions.selectedIndex = i
end if end if
textLine = cardItem.CreateChild("SimpleLabel") textLine = cardItem.CreateChild("SimpleLabel")
textLine.text = item.description textLine.text = item.track.description
cardItem.observeField("selected", "onItemSelected")
i++ i++
end for end for
end sub end sub
function onKeyEvent(key as string, press as boolean) as boolean
if key = "right"
' By default RIGHT from the option list selects the OK button
' Instead, keep the user on the option list
return true
end if
if not press then return false
if key = "up"
' By default UP from the OK button is the scrollbar
' Instead, move the user to the option list
if not m.radioOptions.isinFocusChain()
m.radioOptions.setFocus(true)
return true
end if
end if
return false
end function

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<component name="RadioDialog" extends="StandardMessageDialog"> <component name="RadioDialog" extends="StandardMessageDialog" initialFocus="radioOptions">
<children> <children>
<StdDlgContentArea> <StdDlgContentArea id="contentArea">
<StdDlgItemGroup id="content" /> <StdDlgItemGroup id="radioOptions" />
</StdDlgContentArea> </StdDlgContentArea>
</children> </children>
<interface> <interface>

View File

@ -2,5 +2,6 @@
<component name="ChannelData" extends="JFContentItem"> <component name="ChannelData" extends="JFContentItem">
<interface> <interface>
<field id="channelID" type="string" /> <field id="channelID" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="1" />
</interface> </interface>
</component> </component>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8"?>
<!-- The "ContentNode" for displaying the actual Extras Item --> <!-- The "ContentNode" for displaying the actual Extras Item -->
<component name="ExtrasData" extends="ContentNode"> <component name="ExtrasData" extends="ContentNode">
<script type="text/brightscript"> <script type="text/brightscript">
@ -19,6 +19,7 @@
<field id="Type" type="string" /> <field id="Type" type="string" />
<field id="subTitle" type="string" /> <field id="subTitle" type="string" />
<field id="labelText" type="string" /> <field id="labelText" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="1" />
<field id="posterUrl" type="string" /> <field id="posterUrl" type="string" />
<field id="imageWidth" type="integer" value="234" /> <field id="imageWidth" type="integer" value="234" />
<field id="json" type="assocarray" /> <field id="json" type="assocarray" />

View File

@ -14,5 +14,7 @@
<field id="PlayedPercentage" type="float" value="0" /> <field id="PlayedPercentage" type="float" value="0" />
<field id="usePoster" type="bool" value="false" /> <field id="usePoster" type="bool" value="false" />
<field id="isWatched" type="bool" value="false" /> <field id="isWatched" type="bool" value="false" />
<field id="selectedAudioStreamIndex" type="integer" value="1" />
<field id="startingPoint" type="longinteger" value="0" />
</interface> </interface>
</component> </component>

View File

@ -7,5 +7,8 @@
<field id="container" type="string" /> <field id="container" type="string" />
<field id="mediaSourceCount" type="integer" /> <field id="mediaSourceCount" type="integer" />
<field id="mediaSources" type="array" /> <field id="mediaSources" type="array" />
<field id="startingPoint" type="longinteger" value="0" />
<field id="mediaSourceId" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="1" />
</interface> </interface>
</component> </component>

View File

@ -5,5 +5,7 @@
<field id="title" type="string" /> <field id="title" type="string" />
<field id="image" type="node" onChange="setPoster" /> <field id="image" type="node" onChange="setPoster" />
<field id="overview" type="string" /> <field id="overview" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="1" />
<field id="startingPoint" type="longinteger" value="0" />
</interface> </interface>
</component> </component>

View File

@ -244,7 +244,7 @@ sub userMessage(title as string, message as string)
dialog.title = title dialog.title = title
dialog.message = message dialog.message = message
dialog.buttons = [tr("OK")] dialog.buttons = [tr("OK")]
dialog.observeField("buttonSelected", "dismiss_dialog") dialog.observeField("buttonSelected", "dismissDialog")
m.scene.dialog = dialog m.scene.dialog = dialog
end sub end sub
@ -262,7 +262,7 @@ sub standardDialog(title, message)
DialogTextColor: "0xeeeeeeFF" DialogTextColor: "0xeeeeeeFF"
} }
dialog.palette = dlgPalette dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "dismiss_dialog") dialog.observeField("buttonSelected", "dismissDialog")
dialog.title = title dialog.title = title
dialog.contentData = message dialog.contentData = message
dialog.buttons = [tr("OK")] dialog.buttons = [tr("OK")]
@ -284,7 +284,7 @@ sub radioDialog(title, message)
DialogTextColor: "0xeeeeeeFF" DialogTextColor: "0xeeeeeeFF"
} }
dialog.palette = dlgPalette dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "dismiss_dialog") dialog.observeField("buttonSelected", "dismissDialog")
dialog.title = title dialog.title = title
dialog.contentData = message dialog.contentData = message
dialog.buttons = [tr("OK")] dialog.buttons = [tr("OK")]
@ -295,6 +295,7 @@ end sub
' '
' Display dialog to user with an OK button ' Display dialog to user with an OK button
sub optionDialog(title, message, buttons) sub optionDialog(title, message, buttons)
m.top.dataReturned = false
m.top.returnData = invalid m.top.returnData = invalid
m.userselection = false m.userselection = false
@ -327,6 +328,7 @@ sub optionClosed()
indexSelected: -1, indexSelected: -1,
buttonSelected: "" buttonSelected: ""
} }
m.top.dataReturned = true
end sub end sub
' '
@ -337,12 +339,19 @@ sub optionSelected()
indexSelected: m.scene.dialog.buttonSelected, indexSelected: m.scene.dialog.buttonSelected,
buttonSelected: m.scene.dialog.buttons[m.scene.dialog.buttonSelected] buttonSelected: m.scene.dialog.buttons[m.scene.dialog.buttonSelected]
} }
m.top.dataReturned = true
dismiss_dialog() dismissDialog()
end sub end sub
' '
' Close currently displayed dialog ' Close currently displayed dialog
sub dismiss_dialog() sub dismissDialog()
m.scene.dialog.close = true m.scene.dialog.close = true
end sub end sub
'
' Returns bool indicating if dialog is currently displayed
function isDialogOpen() as boolean
return m.scene.dialog <> invalid
end function

View File

@ -11,8 +11,11 @@
<function name="userMessage" /> <function name="userMessage" />
<function name="standardDialog" /> <function name="standardDialog" />
<function name="radioDialog" /> <function name="radioDialog" />
<function name="dismissDialog" />
<function name="isDialogOpen" />
<function name="optionDialog" /> <function name="optionDialog" />
<field id="currentUser" type="string" onChange="updateUser" /> <field id="currentUser" type="string" onChange="updateUser" />
<field id="returnData" type="assocarray" /> <field id="returnData" type="assocarray" />
<field id="dataReturned" type="boolean" />
</interface> </interface>
</component> </component>

View File

@ -9,6 +9,7 @@
<field id="seasonID" type="string" /> <field id="seasonID" type="string" />
<field id="overview" type="string" /> <field id="overview" type="string" />
<field id="type" type="string" value="Episode" /> <field id="type" type="string" value="Episode" />
<field id="startingPoint" type="longinteger" value="0" />
<field id="json" type="assocarray" onChange="setFields" /> <field id="json" type="assocarray" onChange="setFields" />
<field id="selectedAudioStreamIndex" type="integer" /> <field id="selectedAudioStreamIndex" type="integer" />
<field id="favorite" type="boolean" /> <field id="favorite" type="boolean" />

View File

@ -437,6 +437,9 @@ end sub
sub itemSelected() sub itemSelected()
m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1]) m.top.selectedItem = m.top.content.getChild(m.top.rowItemSelected[0]).getChild(m.top.rowItemSelected[1])
'Prevent the selected item event from double firing
m.top.selectedItem = invalid
end sub end sub
function onKeyEvent(key as string, press as boolean) as boolean function onKeyEvent(key as string, press as boolean) as boolean

View File

@ -1,10 +1,18 @@
import "pkg:/source/utils/misc.brs" import "pkg:/source/utils/misc.brs"
import "ViewCreator.brs" import "ViewCreator.brs"
import "pkg:/source/api/Items.brs"
import "pkg:/source/api/baserequest.brs"
import "pkg:/source/utils/config.brs"
import "pkg:/source/api/Image.brs"
import "pkg:/source/utils/deviceCapabilities.brs"
sub init() sub init()
m.hold = []
m.queue = [] m.queue = []
m.originalQueue = [] m.originalQueue = []
m.queueTypes = [] m.queueTypes = []
' Preroll videos only play if user has cinema mode setting enabled
m.isPrerollActive = (get_user_setting("playback.cinemamode") = "true")
m.position = 0 m.position = 0
m.shuffleEnabled = false m.shuffleEnabled = false
end sub end sub
@ -13,9 +21,15 @@ end sub
sub clear() sub clear()
m.queue = [] m.queue = []
m.queueTypes = [] m.queueTypes = []
m.isPrerollActive = (get_user_setting("playback.cinemamode") = "true")
setPosition(0) setPosition(0)
end sub end sub
' Clear all hold content
sub clearHold()
m.hold = []
end sub
' Delete item from play queue at passed index ' Delete item from play queue at passed index
sub deleteAtIndex(index) sub deleteAtIndex(index)
m.queue.Delete(index) m.queue.Delete(index)
@ -32,6 +46,11 @@ function getCurrentItem()
return getItemByIndex(m.position) return getItemByIndex(m.position)
end function end function
' Return the items in the hold
function getHold()
return m.hold
end function
' Return whether or not shuffle is enabled ' Return whether or not shuffle is enabled
function getIsShuffled() function getIsShuffled()
return m.shuffleEnabled return m.shuffleEnabled
@ -47,6 +66,11 @@ function getPosition()
return m.position return m.position
end function end function
' Hold an item
sub hold(newItem)
m.hold.push(newItem)
end sub
' Move queue position back one ' Move queue position back one
sub moveBack() sub moveBack()
m.position-- m.position--
@ -88,16 +112,29 @@ end function
' Play items in queue ' Play items in queue
sub playQueue() sub playQueue()
nextItem = getCurrentItem() nextItem = getCurrentItem()
nextItemMediaType = getItemType(nextItem) if not isValid(nextItem) then return
if not isValid(nextItemMediaType) then return nextItemMediaType = getItemType(nextItem)
if nextItemMediaType = "" then return
if nextItemMediaType = "audio" if nextItemMediaType = "audio"
CreateAudioPlayerView() CreateAudioPlayerView()
else if nextItemMediaType = "video" return
end if
if nextItemMediaType = "video"
CreateVideoPlayerView() CreateVideoPlayerView()
else if nextItemMediaType = "episode" return
end if
if nextItemMediaType = "episode"
CreateVideoPlayerView() CreateVideoPlayerView()
return
end if
if nextItemMediaType = "trailer"
CreateVideoPlayerView()
return
end if end if
end sub end sub
@ -107,6 +144,16 @@ sub pop()
m.queueTypes.pop() m.queueTypes.pop()
end sub end sub
' Return isPrerollActive status
function isPrerollActive() as boolean
return m.isPrerollActive
end function
' Set prerollActive status
sub setPrerollStatus(newStatus as boolean)
m.isPrerollActive = newStatus
end sub
' Push new items to the play queue ' Push new items to the play queue
sub push(newItem) sub push(newItem)
m.queue.push(newItem) m.queue.push(newItem)
@ -180,6 +227,11 @@ sub set(items)
end for end for
end sub end sub
' Set starting point for top item in the queue
sub setTopStartingPoint(positionTicks)
m.queue[0].startingPoint = positionTicks
end sub
function getItemType(item) as string function getItemType(item) as string
if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> "" if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> ""
return LCase(item.json.mediatype) return LCase(item.json.mediatype)
@ -187,5 +239,5 @@ function getItemType(item) as string
return LCase(item.type) return LCase(item.type)
end if end if
return invalid return ""
end function end function

View File

@ -2,9 +2,11 @@
<component name="QueueManager" extends="Group"> <component name="QueueManager" extends="Group">
<interface> <interface>
<function name="clear" /> <function name="clear" />
<function name="clearHold" />
<function name="deleteAtIndex" /> <function name="deleteAtIndex" />
<function name="getCount" /> <function name="getCount" />
<function name="getCurrentItem" /> <function name="getCurrentItem" />
<function name="getHold" />
<function name="getIsShuffled" /> <function name="getIsShuffled" />
<function name="getItemByIndex" /> <function name="getItemByIndex" />
<function name="getPosition" /> <function name="getPosition" />
@ -12,6 +14,8 @@
<function name="getQueueTypes" /> <function name="getQueueTypes" />
<function name="getQueueUniqueTypes" /> <function name="getQueueUniqueTypes" />
<function name="getUnshuffledQueue" /> <function name="getUnshuffledQueue" />
<function name="hold" />
<function name="isPrerollActive" />
<function name="moveBack" /> <function name="moveBack" />
<function name="moveForward" /> <function name="moveForward" />
<function name="peek" /> <function name="peek" />
@ -20,7 +24,9 @@
<function name="push" /> <function name="push" />
<function name="resetShuffle" /> <function name="resetShuffle" />
<function name="set" /> <function name="set" />
<function name="setTopStartingPoint" />
<function name="setPosition" /> <function name="setPosition" />
<function name="setPrerollStatus" />
<function name="toggleShuffle" /> <function name="toggleShuffle" />
<function name="top" /> <function name="top" />
</interface> </interface>

View File

@ -15,8 +15,14 @@ sub CreateVideoPlayerView()
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed") m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed") m.view.observeField("selectSubtitlePressed", "onSelectSubtitlePressed")
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 = createObject("roSGNode", "GetPlaybackInfoTask")
m.getPlaybackInfoTask.videoID = m.global.queueManager.callFunc("getCurrentItem").id m.getPlaybackInfoTask.videoID = mediaSourceId
m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded") m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded")
m.global.sceneManager.callFunc("pushScene", m.view) m.global.sceneManager.callFunc("pushScene", m.view)
@ -30,14 +36,37 @@ end sub
sub onSelectSubtitlePressed() sub onSelectSubtitlePressed()
' None is always first in the subtitle list ' None is always first in the subtitle list
subtitleData = { subtitleData = {
data: [{ "description": "None", "type": "subtitleselection" }] data: [{
"Index": -1,
"IsExternal": false,
"Track": {
"description": "None"
},
"Type": "subtitleselection"
}]
} }
for each item in m.view.content.subtitletracks for each item in m.view.fullSubtitleData
item.type = "subtitleselection" item.type = "subtitleselection"
if item.description = m.selectedSubtitle.description if m.view.selectedSubtitle <> -1
item.selected = true ' 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 <> -1
' 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 end if
subtitleData.data.push(item) subtitleData.data.push(item)
@ -62,14 +91,39 @@ end sub
sub processSubtitleSelection() sub processSubtitleSelection()
m.selectedSubtitle = m.global.sceneManager.returnData m.selectedSubtitle = m.global.sceneManager.returnData
if LCase(m.selectedSubtitle.description) = "none" ' The selected encoded subtitle did not change.
if m.view.selectedSubtitle <> -1 or m.selectedSubtitle.index <> -1
if m.view.selectedSubtitle = m.selectedSubtitle.index then return
end if
' The playbackData is now outdated and must be refreshed
m.playbackData = invalid
if LCase(m.selectedSubtitle.track.description) = "none"
m.view.globalCaptionMode = "Off" m.view.globalCaptionMode = "Off"
m.view.subtitleTrack = "" m.view.subtitleTrack = ""
if m.view.selectedSubtitle <> -1
m.view.selectedSubtitle = -1
end if
return return
end if end if
m.view.globalCaptionMode = "On" if m.selectedSubtitle.IsEncoded
m.view.subtitleTrack = m.selectedSubtitle.TrackName m.view.globalCaptionMode = "Off"
else
m.view.globalCaptionMode = "On"
end if
if m.selectedSubtitle.IsExternal
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
end sub end sub
' User requested playback info ' User requested playback info
@ -96,6 +150,11 @@ end sub
' Playback state change event handlers ' Playback state change event handlers
sub onStateChange() sub onStateChange()
if LCase(m.view.state) = "finished" if LCase(m.view.state) = "finished"
' Close any open dialogs
if m.global.sceneManager.callFunc("isDialogOpen")
m.global.sceneManager.callFunc("dismissDialog")
end if
' If there is something next in the queue, play it ' If there is something next in the queue, play it
if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1 if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
m.global.sceneManager.callFunc("clearPreviousScene") m.global.sceneManager.callFunc("clearPreviousScene")
@ -109,3 +168,21 @@ sub onStateChange()
m.global.audioPlayer.loopMode = "" m.global.audioPlayer.loopMode = ""
end if end if
end sub 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 -1
end function

View File

@ -51,10 +51,11 @@ sub itemContentChanged()
m.top.id = itemData.id m.top.id = itemData.id
m.top.findNode("moviePoster").uri = m.top.itemContent.posterURL m.top.findNode("moviePoster").uri = m.top.itemContent.posterURL
' Set default video source ' Set default video source if user hasn't selected one yet
if itemData.MediaSources <> invalid if m.top.selectedVideoStreamId = "" and isValid(itemData.MediaSources)
m.top.selectedVideoStreamId = itemData.MediaSources[0].id m.top.selectedVideoStreamId = itemData.MediaSources[0].id
end if end if
' Find first Audio Stream and set that as default ' Find first Audio Stream and set that as default
SetDefaultAudioTrack(itemData) SetDefaultAudioTrack(itemData)

View File

@ -31,6 +31,6 @@
<interface> <interface>
<field id="itemContent" type="node" onChange="itemContentChanged" /> <field id="itemContent" type="node" onChange="itemContentChanged" />
<field id="seasonData" type="assocarray" alias="seasons.TVSeasonData" /> <field id="seasonData" type="assocarray" alias="seasons.TVSeasonData" />
<field id="seasonSelected" alias="seasons.rowItemSelected" /> <field id="seasonSelected" alias="seasons.rowItemSelected" alwaysNotify="true" />
</interface> </interface>
</component> </component>

View File

@ -3,13 +3,18 @@ import "pkg:/source/utils/config.brs"
import "pkg:/source/roku_modules/api/api.brs" import "pkg:/source/roku_modules/api/api.brs"
sub init() sub init()
currentItem = m.global.queueManager.callFunc("getCurrentItem") ' Hide the overhang on init to prevent showing 2 clocks
m.top.getScene().findNode("overhang").visible = false
m.top.id = currentItem.id m.currentItem = m.global.queueManager.callFunc("getCurrentItem")
m.top.id = m.currentItem.id
' Load meta data ' Load meta data
m.LoadMetaDataTask = CreateObject("roSGNode", "LoadVideoContentTask") m.LoadMetaDataTask = CreateObject("roSGNode", "LoadVideoContentTask")
m.LoadMetaDataTask.itemId = currentItem.id m.LoadMetaDataTask.itemId = m.currentItem.id
m.LoadMetaDataTask.itemType = m.currentItem.type
m.LoadMetaDataTask.selectedAudioStreamIndex = m.currentItem.selectedAudioStreamIndex
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded") m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
m.LoadMetaDataTask.control = "RUN" m.LoadMetaDataTask.control = "RUN"
@ -17,6 +22,7 @@ sub init()
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer") m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
m.top.observeField("state", "onState") m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange") m.top.observeField("content", "onContentChange")
m.top.observeField("selectedSubtitle", "onSubtitleChange")
m.playbackTimer.observeField("fire", "ReportPlayback") m.playbackTimer.observeField("fire", "ReportPlayback")
m.bufferPercentage = 0 ' Track whether content is being loaded m.bufferPercentage = 0 ' Track whether content is being loaded
@ -46,8 +52,21 @@ sub init()
m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue
end sub end sub
sub onSubtitleChange()
' Save the current video position
m.global.queueManager.callFunc("setTopStartingPoint", int(m.top.position) * 10000000&)
m.top.control = "stop"
m.LoadMetaDataTask.selectedSubtitleIndex = m.top.SelectedSubtitle
m.LoadMetaDataTask.itemId = m.currentItem.id
m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded")
m.LoadMetaDataTask.control = "RUN"
end sub
sub onVideoContentLoaded() sub onVideoContentLoaded()
m.LoadMetaDataTask.unobserveField("content") m.LoadMetaDataTask.unobserveField("content")
m.LoadMetaDataTask.control = "STOP"
' If we have nothing to play, return to previous screen ' If we have nothing to play, return to previous screen
if not isValid(m.LoadMetaDataTask.content) if not isValid(m.LoadMetaDataTask.content)
@ -70,11 +89,22 @@ sub onVideoContentLoaded()
m.top.videoId = m.LoadMetaDataTask.content[0].id m.top.videoId = m.LoadMetaDataTask.content[0].id
m.top.container = m.LoadMetaDataTask.content[0].container m.top.container = m.LoadMetaDataTask.content[0].container
m.top.mediaSourceId = m.LoadMetaDataTask.content[0].mediaSourceId m.top.mediaSourceId = m.LoadMetaDataTask.content[0].mediaSourceId
m.top.fullSubtitleData = m.LoadMetaDataTask.content[0].fullSubtitleData
m.top.audioIndex = m.LoadMetaDataTask.content[0].audio_stream_idx m.top.audioIndex = m.LoadMetaDataTask.content[0].audio_stream_idx
m.top.transcodeParams = m.LoadMetaDataTask.content[0].transcodeparams
if m.LoadMetaDataTask.isIntro
m.top.enableTrickPlay = false
end if
if isValid(m.currentItem.selectedAudioStreamIndex)
m.top.audioTrack = (m.currentItem.selectedAudioStreamIndex + 1).toStr()
else
m.top.audioTrack = "2"
end if
m.top.setFocus(true) m.top.setFocus(true)
m.top.control = "play" m.top.control = "play"
m.top.getScene().findNode("overhang").visible = false
end sub end sub
' Event handler for when video content field changes ' Event handler for when video content field changes

View File

@ -6,7 +6,7 @@
<field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" /> <field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" />
<field id="PlaySessionId" type="string" /> <field id="PlaySessionId" type="string" />
<field id="Subtitles" type="array" /> <field id="Subtitles" type="array" />
<field id="SelectedSubtitle" type="integer" /> <field id="SelectedSubtitle" type="integer" value="-1" alwaysNotify="true" />
<field id="captionMode" type="string" /> <field id="captionMode" type="string" />
<field id="container" type="string" /> <field id="container" type="string" />
<field id="directPlaySupported" type="boolean" /> <field id="directPlaySupported" type="boolean" />
@ -22,6 +22,7 @@
<field id="videoId" type="string" /> <field id="videoId" type="string" />
<field id="mediaSourceId" type="string" /> <field id="mediaSourceId" type="string" />
<field id="fullSubtitleData" type="array" />
<field id="audioIndex" type="integer" /> <field id="audioIndex" type="integer" />
</interface> </interface>

View File

@ -21,6 +21,7 @@ sub Main (args as dynamic) as void
playstateTask.id = "playstateTask" playstateTask.id = "playstateTask"
sceneManager = CreateObject("roSGNode", "SceneManager") sceneManager = CreateObject("roSGNode", "SceneManager")
sceneManager.observeField("dataReturned", m.port)
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager }) m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") }) m.global.addFields({ queueManager: CreateObject("roSGNode", "QueueManager") })
@ -83,7 +84,7 @@ sub Main (args as dynamic) as void
if isValidAndNotEmpty(args.mediaType) and isValidAndNotEmpty(args.contentId) if isValidAndNotEmpty(args.mediaType) and isValidAndNotEmpty(args.contentId)
video = CreateVideoPlayerGroup(args.contentId) video = CreateVideoPlayerGroup(args.contentId)
if isValid(video) and video.errorMsg <> "introaborted" if isValid(video)
sceneManager.callFunc("pushScene", video) sceneManager.callFunc("pushScene", video)
else else
dialog = createObject("roSGNode", "Dialog") dialog = createObject("roSGNode", "Dialog")
@ -118,120 +119,153 @@ sub Main (args as dynamic) as void
group = sceneManager.callFunc("getActiveScene") group = sceneManager.callFunc("getActiveScene")
reportingNode = msg.getRoSGNode() reportingNode = msg.getRoSGNode()
itemNode = reportingNode.quickPlayNode itemNode = reportingNode.quickPlayNode
if itemNode = invalid or itemNode.id = "" then return if isValid(itemNode) and isValid(itemNode.id) and itemNode.id <> ""
if itemNode.type = "Episode" or itemNode.type = "Movie" or itemNode.type = "Video" if itemNode.type = "Episode" or itemNode.type = "Movie" or itemNode.type = "Video"
if itemNode.type = "Episode" and itemNode.selectedAudioStreamIndex <> invalid and itemNode.selectedAudioStreamIndex > 1 audio_stream_idx = 0
video = CreateVideoPlayerGroup(itemNode.id, invalid, itemNode.selectedAudioStreamIndex) if isValid(itemNode.selectedAudioStreamIndex)
else audio_stream_idx = itemNode.selectedAudioStreamIndex
video = CreateVideoPlayerGroup(itemNode.id) end if
end if
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
end if
if LCase(group.subtype()) = "tvepisodes" itemNode.selectedAudioStreamIndex = audio_stream_idx
if isValid(group.lastFocus)
group.lastFocus.setFocus(true) playbackPosition = 0
' Display playback options dialog
if isValid(itemNode.json) and isValid(itemNode.json.userdata) and isValid(itemNode.json.userdata.PlaybackPositionTicks)
playbackPosition = itemNode.json.userdata.PlaybackPositionTicks
end if
if playbackPosition > 0
m.global.queueManager.callFunc("hold", itemNode)
playbackOptionDialog(playbackPosition, itemNode.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", itemNode)
m.global.queueManager.callFunc("playQueue")
end if
' Prevent quick play node from double firing
reportingNode.quickPlayNode = invalid
if LCase(group.subtype()) = "tvepisodes"
if isValid(group.lastFocus)
group.lastFocus.setFocus(true)
end if
end if end if
end if end if
end if end if
else if isNodeEvent(msg, "selectedItem") else if isNodeEvent(msg, "selectedItem")
' If you select a library from ANYWHERE, follow this flow ' If you select a library from ANYWHERE, follow this flow
selectedItem = msg.getData() selectedItem = msg.getData()
if isValid(selectedItem)
selectedItemType = selectedItem.type
m.selectedItemType = selectedItem.type
if selectedItem.type = "CollectionFolder" or selectedItem.type = "BoxSet" if selectedItemType = "CollectionFolder"
if selectedItem.collectionType = "movies" if selectedItem.collectionType = "movies"
group = CreateMovieLibraryView(selectedItem) group = CreateMovieLibraryView(selectedItem)
else if selectedItem.collectionType = "music" else if selectedItem.collectionType = "music"
group = CreateMusicLibraryView(selectedItem)
else
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItemType = "Folder" and selectedItem.json.type = "Genre"
' User clicked on a genre folder
if selectedItem.json.MovieCount > 0
group = CreateMovieLibraryView(selectedItem)
else
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItemType = "Folder" and selectedItem.json.type = "MusicGenre"
group = CreateMusicLibraryView(selectedItem) group = CreateMusicLibraryView(selectedItem)
else sceneManager.callFunc("pushScene", group)
else if selectedItemType = "UserView" or selectedItemType = "Folder" or selectedItemType = "Channel" or selectedItemType = "Boxset"
group = CreateItemGrid(selectedItem) group = CreateItemGrid(selectedItem)
end if sceneManager.callFunc("pushScene", group)
sceneManager.callFunc("pushScene", group) else if selectedItemType = "Episode"
else if selectedItem.type = "Folder" and selectedItem.json.type = "Genre" ' User has selected a TV episode they want us to play
' User clicked on a genre folder audio_stream_idx = 0
if selectedItem.json.MovieCount > 0 if isValid(selectedItem.selectedAudioStreamIndex)
group = CreateMovieLibraryView(selectedItem) audio_stream_idx = selectedItem.selectedAudioStreamIndex
else end if
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "MusicGenre"
group = CreateMusicLibraryView(selectedItem)
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "UserView" or selectedItem.type = "Folder" or selectedItem.type = "Channel" or selectedItem.type = "Boxset"
group = CreateItemGrid(selectedItem)
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Episode"
' play episode
' todo: create an episode page to link here
video_id = selectedItem.id
if selectedItem.selectedAudioStreamIndex <> invalid and selectedItem.selectedAudioStreamIndex > 1
video = CreateVideoPlayerGroup(video_id, invalid, selectedItem.selectedAudioStreamIndex)
else
video = CreateVideoPlayerGroup(video_id)
end if
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
end if
else if selectedItem.type = "Series"
group = CreateSeriesDetailsGroup(selectedItem.json)
else if selectedItem.type = "Season"
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
else if selectedItem.type = "Movie"
' open movie detail page
group = CreateMovieDetailsGroup(selectedItem)
else if selectedItem.type = "Person"
CreatePersonView(selectedItem)
else if selectedItem.type = "TvChannel" or selectedItem.type = "Video" or selectedItem.type = "Program"
' play channel feed
video_id = selectedItem.id
' Show Channel Loading spinner selectedItem.selectedAudioStreamIndex = audio_stream_idx
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Loading Channel Data")
m.scene.dialog = dialog
if LCase(selectedItem.subtype()) = "extrasdata" ' If we are playing a playlist, always start at the beginning
video = CreateVideoPlayerGroup(video_id, invalid, 1, false, true, false) if m.global.queueManager.callFunc("getCount") > 1
else selectedItem.startingPoint = 0
video = CreateVideoPlayerGroup(video_id) m.global.queueManager.callFunc("clear")
end if m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
else
' Display playback options dialog
if selectedItem.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
end if
end if
dialog.close = true
if video <> invalid and video.errorMsg <> "introaborted" else if selectedItemType = "Series"
sceneManager.callFunc("pushScene", video) group = CreateSeriesDetailsGroup(selectedItem.json.id)
else else if selectedItemType = "Season"
dialog = createObject("roSGNode", "Dialog") group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
dialog.id = "OKDialog" else if selectedItemType = "Movie"
dialog.title = tr("Error loading Channel Data") ' open movie detail page
dialog.message = tr("Unable to load Channel Data from the server") group = CreateMovieDetailsGroup(selectedItem)
dialog.buttons = [tr("OK")] else if selectedItemType = "Person"
CreatePersonView(selectedItem)
else if selectedItemType = "TvChannel" or selectedItemType = "Video" or selectedItemType = "Program"
' User selected a Live TV channel / program
' Show Channel Loading spinner
dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Loading Channel Data")
m.scene.dialog = dialog m.scene.dialog = dialog
m.scene.dialog.observeField("buttonSelected", m.port)
' User selected a program. Play the channel the program is on
if LCase(selectedItemType) = "program"
selectedItem.id = selectedItem.json.ChannelId
end if
' Display playback options dialog
if selectedItem.json.userdata.PlaybackPositionTicks > 0
dialog.close = true
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
dialog.close = true
end if
else if selectedItemType = "Photo"
' Nothing to do here, handled in ItemGrid
else if selectedItemType = "MusicArtist"
group = CreateArtistView(selectedItem.json)
if not isValid(group)
message_dialog(tr("Unable to find any albums or songs belonging to this artist"))
end if
else if selectedItemType = "MusicAlbum"
group = CreateAlbumView(selectedItem.json)
else if selectedItemType = "Playlist"
group = CreatePlaylistView(selectedItem.json)
else if selectedItemType = "Audio"
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", selectedItem.json)
m.global.queueManager.callFunc("playQueue")
else
' TODO - switch on more node types
message_dialog("This type is not yet supported: " + selectedItemType + ".")
end if end if
else if selectedItem.type = "Photo"
' Nothing to do here, handled in ItemGrid
else if selectedItem.type = "MusicArtist"
group = CreateArtistView(selectedItem.json)
if not isValid(group)
message_dialog(tr("Unable to find any albums or songs belonging to this artist"))
end if
else if selectedItem.type = "MusicAlbum"
group = CreateAlbumView(selectedItem.json)
else if selectedItem.type = "Playlist"
group = CreatePlaylistView(selectedItem.json)
else if selectedItem.type = "Audio"
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("resetShuffle")
m.global.queueManager.callFunc("push", selectedItem.json)
m.global.queueManager.callFunc("playQueue")
else
' TODO - switch on more node types
message_dialog("This type is not yet supported: " + selectedItem.type + ".")
end if end if
else if isNodeEvent(msg, "movieSelected") else if isNodeEvent(msg, "movieSelected")
' If you select a movie from ANYWHERE, follow this flow ' If you select a movie from ANYWHERE, follow this flow
@ -240,7 +274,7 @@ sub Main (args as dynamic) as void
else if isNodeEvent(msg, "seriesSelected") else if isNodeEvent(msg, "seriesSelected")
' If you select a TV Series from ANYWHERE, follow this flow ' If you select a TV Series from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker") node = getMsgPicker(msg, "picker")
group = CreateSeriesDetailsGroup(node) group = CreateSeriesDetailsGroup(node.id)
else if isNodeEvent(msg, "seasonSelected") else if isNodeEvent(msg, "seasonSelected")
' If you select a TV Season from ANYWHERE, follow this flow ' If you select a TV Season from ANYWHERE, follow this flow
ptr = msg.getData() ptr = msg.getData()
@ -332,19 +366,6 @@ sub Main (args as dynamic) as void
m.global.queueManager.callFunc("playQueue") m.global.queueManager.callFunc("playQueue")
end if end if
else if isNodeEvent(msg, "episodeSelected")
' If you select a TV Episode from ANYWHERE, follow this flow
m.selectedItemType = "Episode"
node = getMsgPicker(msg, "picker")
video_id = node.id
if node.selectedAudioStreamIndex <> invalid and node.selectedAudioStreamIndex > 1
video = CreateVideoPlayerGroup(video_id, invalid, node.selectedAudioStreamIndex)
else
video = CreateVideoPlayerGroup(video_id)
end if
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
end if
else if isNodeEvent(msg, "search_value") else if isNodeEvent(msg, "search_value")
query = msg.getRoSGNode().search_value query = msg.getRoSGNode().search_value
group.findNode("SearchBox").visible = false group.findNode("SearchBox").visible = false
@ -364,9 +385,8 @@ sub Main (args as dynamic) as void
node = getMsgPicker(msg) node = getMsgPicker(msg)
' TODO - swap this based on target.mediatype ' TODO - swap this based on target.mediatype
' types: [ Series (Show), Episode, Movie, Audio, Person, Studio, MusicArtist ] ' types: [ Series (Show), Episode, Movie, Audio, Person, Studio, MusicArtist ]
m.selectedItemType = node.type
if node.type = "Series" if node.type = "Series"
group = CreateSeriesDetailsGroup(node) group = CreateSeriesDetailsGroup(node.id)
else if node.type = "Movie" else if node.type = "Movie"
group = CreateMovieDetailsGroup(node) group = CreateMovieDetailsGroup(node)
else if node.type = "MusicArtist" else if node.type = "MusicArtist"
@ -402,22 +422,25 @@ sub Main (args as dynamic) as void
btn = getButton(msg) btn = getButton(msg)
group = sceneManager.callFunc("getActiveScene") group = sceneManager.callFunc("getActiveScene")
if isValid(btn) and btn.id = "play-button" if isValid(btn) and btn.id = "play-button"
' User chose Play button from movie detail view
' Check if a specific Audio Stream was selected ' Check if a specific Audio Stream was selected
audio_stream_idx = 1 audio_stream_idx = 0
if isValid(group) and isValid(group.selectedAudioStreamIndex) if isValid(group) and isValid(group.selectedAudioStreamIndex)
audio_stream_idx = group.selectedAudioStreamIndex audio_stream_idx = group.selectedAudioStreamIndex
end if end if
' Check to see if a specific video "version" was selected group.itemContent.selectedAudioStreamIndex = audio_stream_idx
mediaSourceId = invalid group.itemContent.id = group.selectedVideoStreamId
if isValid(group) and isValid(group.selectedVideoStreamId)
mediaSourceId = group.selectedVideoStreamId ' Display playback options dialog
end if if group.itemContent.json.userdata.PlaybackPositionTicks > 0
video_id = group.id m.global.queueManager.callFunc("hold", group.itemContent)
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx) playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
if isValid(video) and video.errorMsg <> "introaborted" else
sceneManager.callFunc("pushScene", video) m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", group.itemContent)
m.global.queueManager.callFunc("playQueue")
end if end if
if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group" if isValid(group) and isValid(group.lastFocus) and isValid(group.lastFocus.id) and group.lastFocus.id = "main_group"
@ -432,23 +455,17 @@ sub Main (args as dynamic) as void
end if end if
else if btn <> invalid and btn.id = "trailer-button" else if btn <> invalid and btn.id = "trailer-button"
' User chose to play a trailer from the movie detail view
dialog = createObject("roSGNode", "ProgressDialog") dialog = createObject("roSGNode", "ProgressDialog")
dialog.title = tr("Loading trailer") dialog.title = tr("Loading trailer")
m.scene.dialog = dialog m.scene.dialog = dialog
audio_stream_idx = 1
mediaSourceId = invalid
video_id = group.id
trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), group.id) trailerData = api_API().users.getlocaltrailers(get_setting("active_user"), group.id)
video = invalid
if isValid(trailerData) and isValid(trailerData[0]) and isValid(trailerData[0].id) if isValid(trailerData) and isValid(trailerData[0]) and isValid(trailerData[0].id)
video_id = trailerData[0].id m.global.queueManager.callFunc("clear")
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false) m.global.queueManager.callFunc("set", trailerData)
end if m.global.queueManager.callFunc("playQueue")
if isValid(video) and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
dialog.close = true dialog.close = true
end if end if
@ -535,7 +552,7 @@ sub Main (args as dynamic) as void
else if isNodeEvent(msg, "state") else if isNodeEvent(msg, "state")
node = msg.getRoSGNode() node = msg.getRoSGNode()
if isValid(node) and isValid(node.state) if isValid(node) and isValid(node.state)
if m.selectedItemType = "TvChannel" and node.state = "finished" if node.selectedItemType = "TvChannel" and node.state = "finished"
video = CreateVideoPlayerGroup(node.id) video = CreateVideoPlayerGroup(node.id)
m.global.sceneManager.callFunc("pushScene", video) m.global.sceneManager.callFunc("pushScene", video)
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2) m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
@ -590,7 +607,7 @@ sub Main (args as dynamic) as void
info = msg.GetInfo() info = msg.GetInfo()
if info.DoesExist("mediatype") and info.DoesExist("contentid") if info.DoesExist("mediatype") and info.DoesExist("contentid")
video = CreateVideoPlayerGroup(info.contentId) video = CreateVideoPlayerGroup(info.contentId)
if video <> invalid and video.errorMsg <> "introaborted" if video <> invalid
sceneManager.callFunc("pushScene", video) sceneManager.callFunc("pushScene", video)
else else
dialog = createObject("roSGNode", "Dialog") dialog = createObject("roSGNode", "Dialog")
@ -603,6 +620,45 @@ sub Main (args as dynamic) as void
end if end if
end if end if
end if end if
else if isNodeEvent(msg, "dataReturned")
popupNode = msg.getRoSGNode()
if isValid(popupNode) and isValid(popupNode.returnData)
selectedItem = m.global.queueManager.callFunc("getHold")
m.global.queueManager.callFunc("clearHold")
if isValid(selectedItem) and selectedItem.count() > 0 and isValid(selectedItem[0])
if popupNode.returnData.indexselected = 0
'Resume video from resume point
startingPoint = 0
if isValid(selectedItem[0].json) and isValid(selectedItem[0].json.UserData) and isValid(selectedItem[0].json.UserData.PlaybackPositionTicks)
if selectedItem[0].json.UserData.PlaybackPositionTicks > 0
startingPoint = selectedItem[0].json.UserData.PlaybackPositionTicks
end if
end if
selectedItem[0].startingPoint = startingPoint
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem[0])
m.global.queueManager.callFunc("playQueue")
else if popupNode.returnData.indexselected = 1
'Start Over from beginning selected, set position to 0
selectedItem[0].startingPoint = 0
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem[0])
m.global.queueManager.callFunc("playQueue")
else if popupNode.returnData.indexselected = 2
' User chose Go to series
CreateSeriesDetailsGroup(selectedItem[0].json.SeriesId)
else if popupNode.returnData.indexselected = 3
' User chose Go to season
CreateSeasonDetailsGroupByID(selectedItem[0].json.SeriesId, selectedItem[0].json.seasonID)
else if popupNode.returnData.indexselected = 4
' User chose Go to episode
CreateMovieDetailsGroup(selectedItem[0])
end if
end if
end if
else else
print "Unhandled " type(msg) print "Unhandled " type(msg)
print msg print msg

View File

@ -515,24 +515,24 @@ function CreateMovieDetailsGroup(movie as object) as dynamic
return group return group
end function end function
function CreateSeriesDetailsGroup(series as object) as dynamic function CreateSeriesDetailsGroup(seriesID as string) as dynamic
' validate series node ' validate series node
if not isValid(series) or not isValid(series.id) then return invalid if not isValid(seriesID) or seriesID = "" then return invalid
startLoadingSpinner() startLoadingSpinner()
' get series meta data ' get series meta data
seriesMetaData = ItemMetaData(series.id) seriesMetaData = ItemMetaData(seriesID)
' validate series meta data ' validate series meta data
if not isValid(seriesMetaData) if not isValid(seriesMetaData)
stopLoadingSpinner() stopLoadingSpinner()
return invalid return invalid
end if end if
' Get season data early in the function so we can check number of seasons. ' Get season data early in the function so we can check number of seasons.
seasonData = TVSeasons(series.id) seasonData = TVSeasons(seriesID)
' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists. ' Divert to season details if user setting goStraightToEpisodeListing is enabled and only one season exists.
if get_user_setting("ui.tvshows.goStraightToEpisodeListing") = "true" and seasonData.Items.Count() = 1 if get_user_setting("ui.tvshows.goStraightToEpisodeListing") = "true" and seasonData.Items.Count() = 1
stopLoadingSpinner() stopLoadingSpinner()
return CreateSeasonDetailsGroupByID(series.id, seasonData.Items[0].id) return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id)
end if end if
' start building SeriesDetails view ' start building SeriesDetails view
group = CreateObject("roSGNode", "TVShowDetails") group = CreateObject("roSGNode", "TVShowDetails")
@ -814,3 +814,24 @@ sub UpdateSavedServerList()
end if end if
end if end if
end sub end sub
'Opens dialog asking user if they want to resume video or start playback over only on the home screen
sub playbackOptionDialog(time as longinteger, meta as object)
resumeData = [
tr("Resume playing at ") + ticksToHuman(time) + ".",
tr("Start over from the beginning.")
]
group = m.global.sceneManager.callFunc("getActiveScene")
if LCase(group.subtype()) = "home"
if LCase(meta.type) = "episode"
resumeData.push(tr("Go to series"))
resumeData.push(tr("Go to season"))
resumeData.push(tr("Go to episode"))
end if
end if
m.global.sceneManager.callFunc("optionDialog", tr("Playback Options"), [], resumeData)
end sub