Merge pull request #1253 from 1hitsong/GlobalVideoPlayer-Part1
This commit is contained in:
commit
4f79597166
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
<component name="ChannelData" extends="JFContentItem">
|
||||
<interface>
|
||||
<field id="channelID" type="string" />
|
||||
<field id="selectedAudioStreamIndex" type="integer" value="1" />
|
||||
</interface>
|
||||
</component>
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
232
source/Main.brs
232
source/Main.brs
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user