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"
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 = {
"DeviceProfile": getDeviceProfile()
}
params = {
"UserId": get_setting("active_user"),
"StartTimeTicks": startTimeTicks,
"StartTimeTicks": currentItem.startingPoint,
"IsPlayback": true,
"AutoOpenLiveStream": true,
"MaxStreamingBitrate": "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.SetRequest("POST")
return postJson(req, FormatJson(body))

View File

@ -10,30 +10,46 @@ import "pkg:/source/utils/deviceCapabilities.brs"
sub init()
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
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
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.id = id
video.content = createObject("RoSGNode", "ContentNode")
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, -1, forceTranscoding, showIntro, allowResumeDialog)
if video.errorMsg = "introaborted"
return video
end if
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, subtitle_idx, forceTranscoding)
if video.content = invalid
return invalid
@ -42,11 +58,12 @@ function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic,
return video
end function
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, subtitle_idx = -1 as integer, 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)
if not isValid(meta)
video.errorMsg = "Error loading metadata"
video.content = invalid
return
end if
@ -60,42 +77,24 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s
video.content.title = meta.title
video.showID = meta.showID
if playbackPosition = -1
playbackPosition = meta.json.UserData.PlaybackPositionTicks
if allowResumeDialog
if playbackPosition > 0
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
user = AboutMe()
if user.Configuration.EnableNextEpisodeAutoPlay
if LCase(m.top.itemType) = "episode"
addNextEpisodesToQueue(video.showID)
end if
end if
' For phase 1 of playlist support, we don't support intros yet
showIntro = false
playbackPosition = 0!
' Don't attempt to play an intro for an intro video
if showIntro
' Do not play intros when resuming playback
if playbackPosition = 0
if not PlayIntroVideo(video.id, audio_stream_idx)
video.errorMsg = "introaborted"
return
end if
end if
currentItem = m.global.queueManager.callFunc("getCurrentItem")
if isValid(currentItem) and isValid(currentItem.startingPoint)
playbackPosition = currentItem.startingPoint
end if
' PlayStart requires the time to be in seconds
video.content.PlayStart = int(playbackPosition / 10000000)
if not isValid(mediaSourceId) then mediaSourceId = video.id
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
if not isValid(m.playbackInfo)
video.errorMsg = "Error loading playback info"
video.content = invalid
return
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 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."))
video.errorMsg = "Error getting playback information"
video.content = invalid
return
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.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
' is enabled/will be enabled, indexed on the provided list of subtitles
video.SelectedSubtitle = setupSubtitle(video, video.Subtitles, subtitle_idx)
video.SelectedSubtitle = subtitle_idx
if not fully_external
video.content = authorize_request(video.content)
@ -222,18 +221,19 @@ end sub
sub addSubtitlesToVideo(video, meta)
subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
safesubs = subtitles["all"]
subtitleTracks = []
if get_user_setting("playback.subs.onlytext") = "true"
safesubs = []
for each subtitle in subtitles["all"]
if subtitle["IsTextSubtitleStream"]
safesubs.push(subtitle)
safesubs = subtitles["text"]
end if
for each subtitle in safesubs
subtitleTracks.push(subtitle.track)
end for
video.Subtitles = safesubs
else
video.Subtitles = subtitles["all"]
end if
video.content.SubtitleTracks = subtitles["text"]
video.content.SubtitleTracks = subtitleTracks
video.fullSubtitleData = safesubs
end sub
@ -252,26 +252,6 @@ function getTranscodeReasons(url as string) as object
return []
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
devinfo = CreateObject("roDeviceInfo")
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
end function
function getAudioFormat(meta as object) as string
' Determine the codec of the audio file source
if meta.json.mediaSources = invalid then return ""
' Add next episodes to the playback queue
sub addNextEpisodesToQueue(showID)
' Don't queue next episodes if we already have a playback queue
maxQueueCount = 1
audioInfo = getAudioInfo(meta)
if audioInfo.count() = 0 or audioInfo[0].codec = invalid then return ""
return audioInfo[0].codec
end function
function getAudioInfo(meta as object) as object
' Return audio metadata for a given stream
results = []
for each source in meta.json.mediaSources[0].mediaStreams
if source["type"] = "Audio"
results.push(source)
if m.top.isIntro
maxQueueCount = 2
end if
if m.global.queueManager.callFunc("getCount") > maxQueueCount then return
videoID = m.top.itemId
' If first item is an intro video, use the next item in the queue
if m.top.isIntro
currentVideo = m.global.queueManager.callFunc("getItemByIndex", 1)
if isValid(currentVideo) and isValid(currentVideo.id)
videoID = currentVideo.id
' Override showID value since it's for the intro video
meta = ItemMetaData(videoID)
if isValid(meta)
showID = meta.showID
end if
end if
end if
end for
return results
end function
sub autoPlayNextEpisode(videoID as string, showID as string)
' 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 })
urlParams.Append({ "Limit": 50 })
resp = APIRequest(url, urlParams)
data = getJson(resp)
if isValid(data) and data.Items.Count() = 2
' setup new video node
nextVideo = invalid
' remove last videoplayer scene
m.global.sceneManager.callFunc("clearPreviousScene")
if isValid(nextVideo)
m.global.sceneManager.callFunc("pushScene", nextVideo)
else
m.global.sceneManager.callFunc("popScene")
end if
else
' can't play next episode
m.global.sceneManager.callFunc("popScene")
end if
else
m.global.sceneManager.callFunc("popScene")
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 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
function sortSubtitles(id as string, MediaStreams)
m.user = AboutMe()
tracks = { "forced": [], "default": [], "normal": [] }
tracks = { "forced": [], "default": [], "normal": [], "text": [] }
'Too many args for using substitute
prefered_lang = m.user.Configuration.SubtitleLanguagePreference
for each stream in MediaStreams
@ -637,6 +363,8 @@ function sortSubtitles(id as string, MediaStreams)
trackType = "forced"
else if stream.IsDefault
trackType = "default"
else if stream.IsTextSubtitleStream
trackType = "text"
else
trackType = "normal"
end if
@ -650,14 +378,9 @@ function sortSubtitles(id as string, MediaStreams)
tracks["default"].append(tracks["normal"])
tracks["forced"].append(tracks["default"])
tracks["forced"].append(tracks["text"])
textTracks = []
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 }
return { "all": tracks["forced"], "text": tracks["text"] }
end function
function getSubtitleLanguages()
@ -1153,122 +876,3 @@ function getSubtitleLanguages()
"zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
}
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">
<interface>
<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="itemType" type="string" value="" />
<field id="limit" type="integer" value="60" />
<field id="metadata" type="assocarray" />
<field id="sortField" type="string" value="SortName" />
<field id="sortAscending" type="boolean" value="true" />

View File

@ -1,35 +1,138 @@
import "pkg:/source/utils/misc.brs"
sub init()
m.content = m.top.findNode("content")
m.top.observeField("contentData", "onContentDataChanged")
m.contentArea = m.top.findNode("contentArea")
m.radioOptions = m.top.findNode("radioOptions")
m.scrollBarColumn = []
m.top.observeField("contentData", "onContentDataChanged")
m.top.observeFieldScoped("buttonSelected", "onButtonSelected")
m.radioOptions.observeField("focusedChild", "onItemFocused")
m.top.id = "OKDialog"
m.top.height = 900
m.top.title = "What's New?"
m.top.buttons = [tr("OK")]
end sub
' Event handler for when user selected a button
sub onButtonSelected()
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 sub
sub onContentDataChanged()
i = 0
for each item in m.top.contentData.data
cardItem = m.content.CreateChild("StdDlgActionCardItem")
cardItem = m.radioOptions.CreateChild("StdDlgActionCardItem")
cardItem.iconType = "radiobutton"
cardItem.id = i
if isValid(item.selected)
m.content.selectedIndex = i
m.radioOptions.selectedIndex = i
end if
textLine = cardItem.CreateChild("SimpleLabel")
textLine.text = item.description
textLine.text = item.track.description
cardItem.observeField("selected", "onItemSelected")
i++
end for
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"?>
<component name="RadioDialog" extends="StandardMessageDialog">
<component name="RadioDialog" extends="StandardMessageDialog" initialFocus="radioOptions">
<children>
<StdDlgContentArea>
<StdDlgItemGroup id="content" />
<StdDlgContentArea id="contentArea">
<StdDlgItemGroup id="radioOptions" />
</StdDlgContentArea>
</children>
<interface>

View File

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

View File

@ -19,6 +19,7 @@
<field id="Type" type="string" />
<field id="subTitle" type="string" />
<field id="labelText" type="string" />
<field id="selectedAudioStreamIndex" type="integer" value="1" />
<field id="posterUrl" type="string" />
<field id="imageWidth" type="integer" value="234" />
<field id="json" type="assocarray" />

View File

@ -14,5 +14,7 @@
<field id="PlayedPercentage" type="float" value="0" />
<field id="usePoster" 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>
</component>

View File

@ -7,5 +7,8 @@
<field id="container" type="string" />
<field id="mediaSourceCount" type="integer" />
<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>
</component>

View File

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

View File

@ -244,7 +244,7 @@ sub userMessage(title as string, message as string)
dialog.title = title
dialog.message = message
dialog.buttons = [tr("OK")]
dialog.observeField("buttonSelected", "dismiss_dialog")
dialog.observeField("buttonSelected", "dismissDialog")
m.scene.dialog = dialog
end sub
@ -262,7 +262,7 @@ sub standardDialog(title, message)
DialogTextColor: "0xeeeeeeFF"
}
dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "dismiss_dialog")
dialog.observeField("buttonSelected", "dismissDialog")
dialog.title = title
dialog.contentData = message
dialog.buttons = [tr("OK")]
@ -284,7 +284,7 @@ sub radioDialog(title, message)
DialogTextColor: "0xeeeeeeFF"
}
dialog.palette = dlgPalette
dialog.observeField("buttonSelected", "dismiss_dialog")
dialog.observeField("buttonSelected", "dismissDialog")
dialog.title = title
dialog.contentData = message
dialog.buttons = [tr("OK")]
@ -295,6 +295,7 @@ end sub
'
' Display dialog to user with an OK button
sub optionDialog(title, message, buttons)
m.top.dataReturned = false
m.top.returnData = invalid
m.userselection = false
@ -327,6 +328,7 @@ sub optionClosed()
indexSelected: -1,
buttonSelected: ""
}
m.top.dataReturned = true
end sub
'
@ -337,12 +339,19 @@ sub optionSelected()
indexSelected: m.scene.dialog.buttonSelected,
buttonSelected: m.scene.dialog.buttons[m.scene.dialog.buttonSelected]
}
m.top.dataReturned = true
dismiss_dialog()
dismissDialog()
end sub
'
' Close currently displayed dialog
sub dismiss_dialog()
sub dismissDialog()
m.scene.dialog.close = true
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="standardDialog" />
<function name="radioDialog" />
<function name="dismissDialog" />
<function name="isDialogOpen" />
<function name="optionDialog" />
<field id="currentUser" type="string" onChange="updateUser" />
<field id="returnData" type="assocarray" />
<field id="dataReturned" type="boolean" />
</interface>
</component>

View File

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

View File

@ -437,6 +437,9 @@ end sub
sub itemSelected()
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
function onKeyEvent(key as string, press as boolean) as boolean

View File

@ -1,10 +1,18 @@
import "pkg:/source/utils/misc.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()
m.hold = []
m.queue = []
m.originalQueue = []
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.shuffleEnabled = false
end sub
@ -13,9 +21,15 @@ end sub
sub clear()
m.queue = []
m.queueTypes = []
m.isPrerollActive = (get_user_setting("playback.cinemamode") = "true")
setPosition(0)
end sub
' Clear all hold content
sub clearHold()
m.hold = []
end sub
' Delete item from play queue at passed index
sub deleteAtIndex(index)
m.queue.Delete(index)
@ -32,6 +46,11 @@ function getCurrentItem()
return getItemByIndex(m.position)
end function
' Return the items in the hold
function getHold()
return m.hold
end function
' Return whether or not shuffle is enabled
function getIsShuffled()
return m.shuffleEnabled
@ -47,6 +66,11 @@ function getPosition()
return m.position
end function
' Hold an item
sub hold(newItem)
m.hold.push(newItem)
end sub
' Move queue position back one
sub moveBack()
m.position--
@ -88,16 +112,29 @@ end function
' Play items in queue
sub playQueue()
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"
CreateAudioPlayerView()
else if nextItemMediaType = "video"
return
end if
if nextItemMediaType = "video"
CreateVideoPlayerView()
else if nextItemMediaType = "episode"
return
end if
if nextItemMediaType = "episode"
CreateVideoPlayerView()
return
end if
if nextItemMediaType = "trailer"
CreateVideoPlayerView()
return
end if
end sub
@ -107,6 +144,16 @@ sub pop()
m.queueTypes.pop()
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
sub push(newItem)
m.queue.push(newItem)
@ -180,6 +227,11 @@ sub set(items)
end for
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
if isValid(item) and isValid(item.json) and isValid(item.json.mediatype) and item.json.mediatype <> ""
return LCase(item.json.mediatype)
@ -187,5 +239,5 @@ function getItemType(item) as string
return LCase(item.type)
end if
return invalid
return ""
end function

View File

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

View File

@ -15,8 +15,14 @@ sub CreateVideoPlayerView()
m.view.observeField("selectPlaybackInfoPressed", "onSelectPlaybackInfoPressed")
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.videoID = m.global.queueManager.callFunc("getCurrentItem").id
m.getPlaybackInfoTask.videoID = mediaSourceId
m.getPlaybackInfoTask.observeField("data", "onPlaybackInfoLoaded")
m.global.sceneManager.callFunc("pushScene", m.view)
@ -30,15 +36,38 @@ end sub
sub onSelectSubtitlePressed()
' None is always first in the subtitle list
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"
if item.description = m.selectedSubtitle.description
if m.view.selectedSubtitle <> -1
' 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
subtitleData.data.push(item)
end for
@ -62,14 +91,39 @@ end sub
sub processSubtitleSelection()
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.subtitleTrack = ""
if m.view.selectedSubtitle <> -1
m.view.selectedSubtitle = -1
end if
return
end if
if m.selectedSubtitle.IsEncoded
m.view.globalCaptionMode = "Off"
else
m.view.globalCaptionMode = "On"
m.view.subtitleTrack = m.selectedSubtitle.TrackName
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
' User requested playback info
@ -96,6 +150,11 @@ end sub
' Playback state change event handlers
sub onStateChange()
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 m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1
m.global.sceneManager.callFunc("clearPreviousScene")
@ -109,3 +168,21 @@ sub onStateChange()
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 -1
end function

View File

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

View File

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

View File

@ -3,13 +3,18 @@ import "pkg:/source/utils/config.brs"
import "pkg:/source/roku_modules/api/api.brs"
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
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.control = "RUN"
@ -17,6 +22,7 @@ sub init()
m.bufferCheckTimer = m.top.findNode("bufferCheckTimer")
m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange")
m.top.observeField("selectedSubtitle", "onSubtitleChange")
m.playbackTimer.observeField("fire", "ReportPlayback")
m.bufferPercentage = 0 ' Track whether content is being loaded
@ -46,8 +52,21 @@ sub init()
m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue
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()
m.LoadMetaDataTask.unobserveField("content")
m.LoadMetaDataTask.control = "STOP"
' If we have nothing to play, return to previous screen
if not isValid(m.LoadMetaDataTask.content)
@ -70,11 +89,22 @@ sub onVideoContentLoaded()
m.top.videoId = m.LoadMetaDataTask.content[0].id
m.top.container = m.LoadMetaDataTask.content[0].container
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.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.control = "play"
m.top.getScene().findNode("overhang").visible = false
end sub
' Event handler for when video content field changes

View File

@ -6,7 +6,7 @@
<field id="selectPlaybackInfoPressed" type="boolean" alwaysNotify="true" />
<field id="PlaySessionId" type="string" />
<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="container" type="string" />
<field id="directPlaySupported" type="boolean" />
@ -22,6 +22,7 @@
<field id="videoId" type="string" />
<field id="mediaSourceId" type="string" />
<field id="fullSubtitleData" type="array" />
<field id="audioIndex" type="integer" />
</interface>

View File

@ -21,6 +21,7 @@ sub Main (args as dynamic) as void
playstateTask.id = "playstateTask"
sceneManager = CreateObject("roSGNode", "SceneManager")
sceneManager.observeField("dataReturned", m.port)
m.global.addFields({ app_loaded: false, playstateTask: playstateTask, sceneManager: sceneManager })
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)
video = CreateVideoPlayerGroup(args.contentId)
if isValid(video) and video.errorMsg <> "introaborted"
if isValid(video)
sceneManager.callFunc("pushScene", video)
else
dialog = createObject("roSGNode", "Dialog")
@ -118,30 +119,49 @@ sub Main (args as dynamic) as void
group = sceneManager.callFunc("getActiveScene")
reportingNode = msg.getRoSGNode()
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" and itemNode.selectedAudioStreamIndex <> invalid and itemNode.selectedAudioStreamIndex > 1
video = CreateVideoPlayerGroup(itemNode.id, invalid, itemNode.selectedAudioStreamIndex)
audio_stream_idx = 0
if isValid(itemNode.selectedAudioStreamIndex)
audio_stream_idx = itemNode.selectedAudioStreamIndex
end if
itemNode.selectedAudioStreamIndex = audio_stream_idx
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
video = CreateVideoPlayerGroup(itemNode.id)
end if
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
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
else if isNodeEvent(msg, "selectedItem")
' If you select a library from ANYWHERE, follow this flow
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"
group = CreateMovieLibraryView(selectedItem)
else if selectedItem.collectionType = "music"
@ -150,7 +170,7 @@ sub Main (args as dynamic) as void
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "Genre"
else if selectedItemType = "Folder" and selectedItem.json.type = "Genre"
' User clicked on a genre folder
if selectedItem.json.MovieCount > 0
group = CreateMovieLibraryView(selectedItem)
@ -158,80 +178,94 @@ sub Main (args as dynamic) as void
group = CreateItemGrid(selectedItem)
end if
sceneManager.callFunc("pushScene", group)
else if selectedItem.type = "Folder" and selectedItem.json.type = "MusicGenre"
else if selectedItemType = "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"
else if selectedItemType = "UserView" or selectedItemType = "Folder" or selectedItemType = "Channel" or selectedItemType = "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 if selectedItemType = "Episode"
' User has selected a TV episode they want us to play
audio_stream_idx = 0
if isValid(selectedItem.selectedAudioStreamIndex)
audio_stream_idx = selectedItem.selectedAudioStreamIndex
end if
selectedItem.selectedAudioStreamIndex = audio_stream_idx
' If we are playing a playlist, always start at the beginning
if m.global.queueManager.callFunc("getCount") > 1
selectedItem.startingPoint = 0
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
else
video = CreateVideoPlayerGroup(video_id)
' 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
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"
else if selectedItemType = "Series"
group = CreateSeriesDetailsGroup(selectedItem.json.id)
else if selectedItemType = "Season"
group = CreateSeasonDetailsGroupByID(selectedItem.json.SeriesId, selectedItem.id)
else if selectedItem.type = "Movie"
else if selectedItemType = "Movie"
' open movie detail page
group = CreateMovieDetailsGroup(selectedItem)
else if selectedItem.type = "Person"
else if selectedItemType = "Person"
CreatePersonView(selectedItem)
else if selectedItem.type = "TvChannel" or selectedItem.type = "Video" or selectedItem.type = "Program"
' play channel feed
video_id = selectedItem.id
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
if LCase(selectedItem.subtype()) = "extrasdata"
video = CreateVideoPlayerGroup(video_id, invalid, 1, false, true, false)
else
video = CreateVideoPlayerGroup(video_id)
' 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
if video <> invalid and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
m.global.queueManager.callFunc("hold", selectedItem)
playbackOptionDialog(selectedItem.json.userdata.PlaybackPositionTicks, selectedItem.json)
else
dialog = createObject("roSGNode", "Dialog")
dialog.id = "OKDialog"
dialog.title = tr("Error loading Channel Data")
dialog.message = tr("Unable to load Channel Data from the server")
dialog.buttons = [tr("OK")]
m.scene.dialog = dialog
m.scene.dialog.observeField("buttonSelected", m.port)
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", selectedItem)
m.global.queueManager.callFunc("playQueue")
dialog.close = true
end if
else if selectedItem.type = "Photo"
else if selectedItemType = "Photo"
' Nothing to do here, handled in ItemGrid
else if selectedItem.type = "MusicArtist"
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 selectedItem.type = "MusicAlbum"
else if selectedItemType = "MusicAlbum"
group = CreateAlbumView(selectedItem.json)
else if selectedItem.type = "Playlist"
else if selectedItemType = "Playlist"
group = CreatePlaylistView(selectedItem.json)
else if selectedItem.type = "Audio"
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: " + selectedItem.type + ".")
message_dialog("This type is not yet supported: " + selectedItemType + ".")
end if
end if
else if isNodeEvent(msg, "movieSelected")
' 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")
' If you select a TV Series from ANYWHERE, follow this flow
node = getMsgPicker(msg, "picker")
group = CreateSeriesDetailsGroup(node)
group = CreateSeriesDetailsGroup(node.id)
else if isNodeEvent(msg, "seasonSelected")
' If you select a TV Season from ANYWHERE, follow this flow
ptr = msg.getData()
@ -332,19 +366,6 @@ sub Main (args as dynamic) as void
m.global.queueManager.callFunc("playQueue")
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")
query = msg.getRoSGNode().search_value
group.findNode("SearchBox").visible = false
@ -364,9 +385,8 @@ sub Main (args as dynamic) as void
node = getMsgPicker(msg)
' TODO - swap this based on target.mediatype
' types: [ Series (Show), Episode, Movie, Audio, Person, Studio, MusicArtist ]
m.selectedItemType = node.type
if node.type = "Series"
group = CreateSeriesDetailsGroup(node)
group = CreateSeriesDetailsGroup(node.id)
else if node.type = "Movie"
group = CreateMovieDetailsGroup(node)
else if node.type = "MusicArtist"
@ -402,22 +422,25 @@ sub Main (args as dynamic) as void
btn = getButton(msg)
group = sceneManager.callFunc("getActiveScene")
if isValid(btn) and btn.id = "play-button"
' User chose Play button from movie detail view
' Check if a specific Audio Stream was selected
audio_stream_idx = 1
audio_stream_idx = 0
if isValid(group) and isValid(group.selectedAudioStreamIndex)
audio_stream_idx = group.selectedAudioStreamIndex
end if
' Check to see if a specific video "version" was selected
mediaSourceId = invalid
if isValid(group) and isValid(group.selectedVideoStreamId)
mediaSourceId = group.selectedVideoStreamId
end if
video_id = group.id
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx)
if isValid(video) and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
group.itemContent.selectedAudioStreamIndex = audio_stream_idx
group.itemContent.id = group.selectedVideoStreamId
' Display playback options dialog
if group.itemContent.json.userdata.PlaybackPositionTicks > 0
m.global.queueManager.callFunc("hold", group.itemContent)
playbackOptionDialog(group.itemContent.json.userdata.PlaybackPositionTicks, group.itemContent.json)
else
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("push", group.itemContent)
m.global.queueManager.callFunc("playQueue")
end if
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
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.title = tr("Loading trailer")
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)
video = invalid
if isValid(trailerData) and isValid(trailerData[0]) and isValid(trailerData[0].id)
video_id = trailerData[0].id
video = CreateVideoPlayerGroup(video_id, mediaSourceId, audio_stream_idx, false, false)
end if
if isValid(video) and video.errorMsg <> "introaborted"
sceneManager.callFunc("pushScene", video)
m.global.queueManager.callFunc("clear")
m.global.queueManager.callFunc("set", trailerData)
m.global.queueManager.callFunc("playQueue")
dialog.close = true
end if
@ -535,7 +552,7 @@ sub Main (args as dynamic) as void
else if isNodeEvent(msg, "state")
node = msg.getRoSGNode()
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)
m.global.sceneManager.callFunc("pushScene", video)
m.global.sceneManager.callFunc("deleteSceneAtIndex", 2)
@ -590,7 +607,7 @@ sub Main (args as dynamic) as void
info = msg.GetInfo()
if info.DoesExist("mediatype") and info.DoesExist("contentid")
video = CreateVideoPlayerGroup(info.contentId)
if video <> invalid and video.errorMsg <> "introaborted"
if video <> invalid
sceneManager.callFunc("pushScene", video)
else
dialog = createObject("roSGNode", "Dialog")
@ -603,6 +620,45 @@ sub Main (args as dynamic) as void
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
print "Unhandled " type(msg)
print msg

View File

@ -515,24 +515,24 @@ function CreateMovieDetailsGroup(movie as object) as dynamic
return group
end function
function CreateSeriesDetailsGroup(series as object) as dynamic
function CreateSeriesDetailsGroup(seriesID as string) as dynamic
' 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()
' get series meta data
seriesMetaData = ItemMetaData(series.id)
seriesMetaData = ItemMetaData(seriesID)
' validate series meta data
if not isValid(seriesMetaData)
stopLoadingSpinner()
return invalid
end if
' 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.
if get_user_setting("ui.tvshows.goStraightToEpisodeListing") = "true" and seasonData.Items.Count() = 1
stopLoadingSpinner()
return CreateSeasonDetailsGroupByID(series.id, seasonData.Items[0].id)
return CreateSeasonDetailsGroupByID(seriesID, seasonData.Items[0].id)
end if
' start building SeriesDetails view
group = CreateObject("roSGNode", "TVShowDetails")
@ -814,3 +814,24 @@ sub UpdateSavedServerList()
end if
end if
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