import "pkg:/source/utils/misc.bs" import "pkg:/source/utils/config.bs" sub init() ' Hide the overhang on init to prevent showing 2 clocks m.top.getScene().findNode("overhang").visible = false m.currentItem = m.global.queueManager.callFunc("getCurrentItem") m.top.id = m.currentItem.id m.top.seekMode = "accurate" m.playbackEnum = { null: -10 } ' Load meta data m.LoadMetaDataTask = CreateObject("roSGNode", "LoadVideoContentTask") 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" m.chapterList = m.top.findNode("chapterList") m.chapterMenu = m.top.findNode("chapterMenu") m.chapterContent = m.top.findNode("chapterContent") m.osd = m.top.findNode("osd") m.osd.observeField("action", "onOSDAction") m.playbackTimer = m.top.findNode("playbackTimer") m.bufferCheckTimer = m.top.findNode("bufferCheckTimer") m.top.observeField("state", "onState") m.top.observeField("content", "onContentChange") m.top.observeField("selectedSubtitle", "onSubtitleChange") m.top.observeField("audioIndex", "onAudioIndexChange") ' Custom Caption Function m.top.observeField("allowCaptions", "onAllowCaptionsChange") m.playbackTimer.observeField("fire", "ReportPlayback") m.bufferPercentage = 0 ' Track whether content is being loaded m.playReported = false m.top.transcodeReasons = [] m.bufferCheckTimer.duration = 30 if m.global.session.user.settings["ui.design.hideclock"] = true clockNode = findNodeBySubtype(m.top, "clock") if clockNode[0] <> invalid then clockNode[0].parent.removeChild(clockNode[0].node) end if 'Play Next Episode button m.nextEpisodeButton = m.top.findNode("nextEpisode") m.nextEpisodeButton.text = tr("Next Episode") m.nextEpisodeButton.setFocus(false) m.nextupbuttonseconds = m.global.session.user.settings["playback.nextupbuttonseconds"].ToInt() m.showNextEpisodeButtonAnimation = m.top.findNode("showNextEpisodeButton") m.hideNextEpisodeButtonAnimation = m.top.findNode("hideNextEpisodeButton") m.checkedForNextEpisode = false m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask") m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded") m.top.retrievingBar.filledBarBlendColor = m.global.constants.colors.blue m.top.bufferingBar.filledBarBlendColor = m.global.constants.colors.blue m.top.trickPlayBar.filledBarBlendColor = m.global.constants.colors.blue end sub ' handleChapterSkipAction: Handles user command to skip chapters in playing video ' sub handleChapterSkipAction(action as string) if not isValidAndNotEmpty(m.chapters) then return currentChapter = getCurrentChapterIndex() if action = "chapternext" gotoChapter = currentChapter + 1 ' If there is no next chapter, exit if gotoChapter > m.chapters.count() - 1 then return m.top.seek = m.chapters[gotoChapter].StartPositionTicks / 10000000# return end if if action = "chapterback" gotoChapter = currentChapter - 1 ' If there is no previous chapter, restart current chapter if gotoChapter < 0 then gotoChapter = 0 m.top.seek = m.chapters[gotoChapter].StartPositionTicks / 10000000# return end if end sub ' handleItemSkipAction: Handles user command to skip items ' ' @param {string} action - skip action to take sub handleItemSkipAction(action as string) if action = "itemnext" ' If there is something next in the queue, play it if m.global.queueManager.callFunc("getPosition") < m.global.queueManager.callFunc("getCount") - 1 m.top.control = "stop" m.global.sceneManager.callFunc("clearPreviousScene") m.global.queueManager.callFunc("moveForward") m.global.queueManager.callFunc("playQueue") end if return end if if action = "itemback" ' If there is something previous in the queue, play it if m.global.queueManager.callFunc("getPosition") > 0 m.top.control = "stop" m.global.sceneManager.callFunc("clearPreviousScene") m.global.queueManager.callFunc("moveBack") m.global.queueManager.callFunc("playQueue") end if return end if end sub ' handleHideAction: Handles action to hide OSD menu ' ' @param {boolean} resume - controls whether or not to resume video playback when sub is called ' sub handleHideAction(resume as boolean) m.osd.visible = false m.chapterList.visible = false m.osd.showChapterList = false m.chapterList.setFocus(false) m.osd.hasFocus = false m.osd.setFocus(false) m.top.setFocus(true) if resume m.top.control = "resume" end if end sub ' handleChapterListAction: Handles action to show chapter list ' sub handleChapterListAction() m.chapterList.visible = m.osd.showChapterList if not m.chapterList.visible then return m.chapterMenu.jumpToItem = getCurrentChapterIndex() m.osd.hasFocus = false m.osd.setFocus(false) m.chapterMenu.setFocus(true) end sub ' getCurrentChapterIndex: Finds current chapter index ' ' @return {integer} indicating index of current chapter within chapter data or 0 if chapter lookup fails ' function getCurrentChapterIndex() as integer if not isValidAndNotEmpty(m.chapters) then return 0 ' Give a 15 second buffer to compensate for user expectation and roku video position inaccuracy ' Web client uses 10 seconds, but this wasn't enough for Roku in testing currentPosition = m.top.position + 15 currentChapter = 0 for i = m.chapters.count() - 1 to 0 step -1 if currentPosition >= (m.chapters[i].StartPositionTicks / 10000000#) currentChapter = i exit for end if end for return currentChapter end function ' handleVideoPlayPauseAction: Handles action to either play or pause the video content ' sub handleVideoPlayPauseAction() ' If video is paused, resume it if m.top.state = "paused" handleHideAction(true) return end if ' Pause video m.top.control = "pause" end sub ' handleShowSubtitleMenuAction: Handles action to show subtitle selection menu ' sub handleShowSubtitleMenuAction() m.top.selectSubtitlePressed = true end sub ' handleShowAudioMenuAction: Handles action to show audio selection menu ' sub handleShowAudioMenuAction() m.top.selectAudioPressed = true end sub ' handleShowVideoInfoPopupAction: Handles action to show video info popup ' sub handleShowVideoInfoPopupAction() m.top.selectPlaybackInfoPressed = true end sub ' onOSDAction: Process action events from OSD to their respective handlers ' sub onOSDAction() action = LCase(m.osd.action) if action = "hide" handleHideAction(false) return end if if action = "play" handleHideAction(true) return end if if action = "chapterback" or action = "chapternext" handleChapterSkipAction(action) return end if if action = "chapterlist" handleChapterListAction() return end if if action = "videoplaypause" handleVideoPlayPauseAction() return end if if action = "showsubtitlemenu" handleShowSubtitleMenuAction() return end if if action = "showaudiomenu" handleShowAudioMenuAction() return end if if action = "showvideoinfopopup" handleShowVideoInfoPopupAction() return end if if action = "itemback" or action = "itemnext" handleItemSkipAction(action) return end if end sub ' Only setup caption items if captions are allowed sub onAllowCaptionsChange() if not m.top.allowCaptions then return m.captionGroup = m.top.findNode("captionGroup") m.captionGroup.createchildren(9, "LayoutGroup") m.captionTask = createObject("roSGNode", "captionTask") m.captionTask.observeField("currentCaption", "updateCaption") m.captionTask.observeField("useThis", "checkCaptionMode") m.top.observeField("subtitleTrack", "loadCaption") m.top.observeField("globalCaptionMode", "toggleCaption") if m.global.session.user.settings["playback.subs.custom"] m.top.suppressCaptions = true toggleCaption() else m.top.suppressCaptions = false end if end sub ' Set caption url to server subtitle track sub loadCaption() if m.top.suppressCaptions m.captionTask.url = m.top.subtitleTrack end if end sub ' Toggles visibility of custom subtitles and sets captionTask's player state sub toggleCaption() m.captionTask.playerState = m.top.state + m.top.globalCaptionMode if LCase(m.top.globalCaptionMode) = "on" m.captionTask.playerState = m.top.state + m.top.globalCaptionMode + "w" m.captionGroup.visible = true else m.captionGroup.visible = false end if end sub ' Removes old subtitle lines and adds new subtitle lines sub updateCaption() m.captionGroup.removeChildrenIndex(m.captionGroup.getChildCount(), 0) m.captionGroup.appendChildren(m.captionTask.currentCaption) end sub ' Event handler for when selectedSubtitle changes sub onSubtitleChange() switchWithoutRefresh = true if m.top.SelectedSubtitle <> -1 ' If the global caption mode is off, then Roku can't display the subtitles natively and needs a video stop/start if LCase(m.top.globalCaptionMode) <> "on" then switchWithoutRefresh = false end if ' If previous sustitle was encoded, then we need to a video stop/start to change subtitle content if m.top.previousSubtitleWasEncoded then switchWithoutRefresh = false if switchWithoutRefresh then return ' 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.selectedAudioStreamIndex = m.top.audioIndex m.LoadMetaDataTask.itemId = m.currentItem.id m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded") m.LoadMetaDataTask.control = "RUN" end sub ' Event handler for when audioIndex changes sub onAudioIndexChange() ' Skip initial audio index setting if m.top.position = 0 then return ' 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.selectedAudioStreamIndex = m.top.audioIndex m.LoadMetaDataTask.itemId = m.currentItem.id m.LoadMetaDataTask.observeField("content", "onVideoContentLoaded") m.LoadMetaDataTask.control = "RUN" end sub sub onPlaybackErrorDialogClosed(msg) sourceNode = msg.getRoSGNode() sourceNode.unobserveField("buttonSelected") sourceNode.unobserveField("wasClosed") m.global.sceneManager.callFunc("popScene") end sub sub onPlaybackErrorButtonSelected(msg) sourceNode = msg.getRoSGNode() sourceNode.close = true end sub sub showPlaybackErrorDialog(errorMessage as string) dialog = createObject("roSGNode", "Dialog") dialog.title = tr("Error During Playback") dialog.buttons = [tr("OK")] dialog.message = errorMessage dialog.observeField("buttonSelected", "onPlaybackErrorButtonSelected") dialog.observeField("wasClosed", "onPlaybackErrorDialogClosed") m.top.getScene().dialog = dialog end sub sub onVideoContentLoaded() m.LoadMetaDataTask.unobserveField("content") m.LoadMetaDataTask.control = "STOP" videoContent = m.LoadMetaDataTask.content m.LoadMetaDataTask.content = [] stopLoadingSpinner() ' If we have nothing to play, return to previous screen if not isValid(videoContent) showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server.")) return end if if not isValid(videoContent[0]) showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server.")) return end if m.top.content = videoContent[0].content m.top.PlaySessionId = videoContent[0].PlaySessionId m.top.videoId = videoContent[0].id m.top.container = videoContent[0].container m.top.mediaSourceId = videoContent[0].mediaSourceId m.top.fullSubtitleData = videoContent[0].fullSubtitleData m.top.fullAudioData = videoContent[0].fullAudioData m.top.audioIndex = videoContent[0].audioIndex m.top.transcodeParams = videoContent[0].transcodeparams m.chapters = videoContent[0].chapters m.top.showID = videoContent[0].showID m.osd.itemTitleText = m.top.content.title populateChapterMenu() if m.LoadMetaDataTask.isIntro ' Disable trackplay bar for intro videos m.top.enableTrickPlay = false else ' Allow custom captions for non intro videos m.top.allowCaptions = true end if ' Allow default subtitles m.top.unobserveField("selectedSubtitle") ' Set subtitleTrack property if subs are natively supported by Roku selectedSubtitle = invalid for each subtitle in m.top.fullSubtitleData if subtitle.Index = videoContent[0].selectedSubtitle selectedSubtitle = subtitle exit for end if end for if isValid(selectedSubtitle) availableSubtitleTrackIndex = availSubtitleTrackIdx(selectedSubtitle.Track.TrackName) if availableSubtitleTrackIndex <> -1 if not selectedSubtitle.IsEncoded m.top.globalCaptionMode = "On" m.top.subtitleTrack = m.top.availableSubtitleTracks[availableSubtitleTrackIndex].TrackName end if end if end if m.top.selectedSubtitle = videoContent[0].selectedSubtitle m.top.observeField("selectedSubtitle", "onSubtitleChange") if isValid(m.top.audioIndex) m.top.audioTrack = (m.top.audioIndex + 1).toStr() else m.top.audioTrack = "2" end if m.top.setFocus(true) m.top.control = "play" end sub ' populateChapterMenu: ' Parse chapter data from API and appeand to chapter list menu ' sub populateChapterMenu() ' Clear any existing chapter list data m.chapterContent.clear() if not isValidAndNotEmpty(m.chapters) chapterItem = CreateObject("roSGNode", "ContentNode") chapterItem.title = tr("No Chapter Data Found") chapterItem.playstart = m.playbackEnum.null m.chapterContent.appendChild(chapterItem) return end if for each chapter in m.chapters chapterItem = CreateObject("roSGNode", "ContentNode") chapterItem.title = chapter.Name chapterItem.playstart = chapter.StartPositionTicks / 10000000# m.chapterContent.appendChild(chapterItem) end for end sub ' Event handler for when video content field changes sub onContentChange() if not isValid(m.top.content) then return m.top.observeField("position", "onPositionChanged") end sub sub onNextEpisodeDataLoaded() m.checkedForNextEpisode = true m.top.observeField("position", "onPositionChanged") ' If there is no next episode, disable next episode button if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2 m.nextupbuttonseconds = 0 end if end sub ' ' Runs Next Episode button animation and sets focus to button sub showNextEpisodeButton() if m.osd.visible then return if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode" if m.nextupbuttonseconds = 0 then return ' is the button disabled? if m.nextEpisodeButton.opacity = 0 and m.global.session.user.configuration.EnableNextEpisodeAutoPlay m.nextEpisodeButton.visible = true m.showNextEpisodeButtonAnimation.control = "start" m.nextEpisodeButton.setFocus(true) end if end sub ' 'Update count down text sub updateCount() nextEpisodeCountdown = Int(m.top.duration - m.top.position) if nextEpisodeCountdown < 0 nextEpisodeCountdown = 0 end if m.nextEpisodeButton.text = tr("Next Episode") + " " + nextEpisodeCountdown.toStr() end sub ' ' Runs hide Next Episode button animation and sets focus back to video sub hideNextEpisodeButton() m.hideNextEpisodeButtonAnimation.control = "start" m.nextEpisodeButton.setFocus(false) m.top.setFocus(true) end sub ' Checks if we need to display the Next Episode button sub checkTimeToDisplayNextEpisode() if m.top.content.contenttype <> 4 then return ' only display when content is type "Episode" if m.nextupbuttonseconds = 0 then return ' is the button disabled? ' Don't show Next Episode button if trickPlayBar is visible if m.top.trickPlayBar.visible then return if isValid(m.top.duration) and isValid(m.top.position) nextEpisodeCountdown = Int(m.top.duration - m.top.position) if nextEpisodeCountdown < 0 and m.nextEpisodeButton.opacity = 0.9 hideNextEpisodeButton() return else if nextEpisodeCountdown > 1 and int(m.top.position) >= (m.top.duration - m.nextupbuttonseconds - 1) updateCount() if m.nextEpisodeButton.opacity = 0 showNextEpisodeButton() end if return end if end if if m.nextEpisodeButton.visible or m.nextEpisodeButton.hasFocus() m.nextEpisodeButton.visible = false m.nextEpisodeButton.setFocus(false) end if end sub ' When Video Player state changes sub onPositionChanged() ' Pass video position data into OSD m.osd.progressPercentage = m.top.position / m.top.duration m.osd.positionTime = m.top.position m.osd.remainingPositionTime = m.top.duration - m.top.position if isValid(m.captionTask) m.captionTask.currentPos = Int(m.top.position * 1000) end if ' Check if dialog is open m.dialog = m.top.getScene().findNode("dialogBackground") if not isValid(m.dialog) ' Do not show Next Episode button for intro videos if not m.LoadMetaDataTask.isIntro checkTimeToDisplayNextEpisode() end if end if end sub ' ' When Video Player state changes sub onState(msg) if isValid(m.captionTask) m.captionTask.playerState = m.top.state + m.top.globalCaptionMode end if ' Pass video state into OSD m.osd.playbackState = m.top.state ' When buffering, start timer to monitor buffering process if m.top.state = "buffering" and m.bufferCheckTimer <> invalid ' start timer m.bufferCheckTimer.control = "start" m.bufferCheckTimer.ObserveField("fire", "bufferCheck") else if m.top.state = "error" if not m.playReported and m.top.transcodeAvailable m.top.retryWithTranscoding = true ' If playback was not reported, retry with transcoding else ' If an error was encountered, Display dialog showPlaybackErrorDialog(tr("Error During Playback")) end if ' Stop playback and exit player m.top.control = "stop" m.top.backPressed = true else if m.top.state = "playing" ' Check if next episode is available if isValid(m.top.showID) if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4 m.getNextEpisodeTask.showID = m.top.showID m.getNextEpisodeTask.videoID = m.top.id m.getNextEpisodeTask.control = "RUN" end if end if if m.playReported = false ReportPlayback("start") m.playReported = true else ReportPlayback() end if m.playbackTimer.control = "start" else if m.top.state = "paused" m.playbackTimer.control = "stop" ReportPlayback() else if m.top.state = "stopped" m.playbackTimer.control = "stop" ReportPlayback("stop") m.playReported = false end if end sub ' ' Report playback to server sub ReportPlayback(state = "update" as string) if m.top.position = invalid then return params = { "ItemId": m.top.id, "PlaySessionId": m.top.PlaySessionId, "PositionTicks": int(m.top.position) * 10000000&, 'Ensure a LongInteger is used "IsPaused": (m.top.state = "paused") } if m.top.content.live params.append({ "MediaSourceId": m.top.transcodeParams.MediaSourceId, "LiveStreamId": m.top.transcodeParams.LiveStreamId }) m.bufferCheckTimer.duration = 30 end if ' Report playstate via worker task playstateTask = m.global.playstateTask playstateTask.setFields({ status: state, params: params }) playstateTask.control = "RUN" end sub ' ' Check the the buffering has not hung sub bufferCheck(msg) if m.top.state <> "buffering" ' If video is not buffering, stop timer m.bufferCheckTimer.control = "stop" m.bufferCheckTimer.unobserveField("fire") return end if if m.top.bufferingStatus <> invalid ' Check that the buffering percentage is increasing if m.top.bufferingStatus["percentage"] > m.bufferPercentage m.bufferPercentage = m.top.bufferingStatus["percentage"] else if m.top.content.live = true m.top.callFunc("refresh") else ' If buffering has stopped Display dialog showPlaybackErrorDialog(tr("There was an error retrieving the data for this item from the server.")) ' Stop playback and exit player m.top.control = "stop" m.top.backPressed = true end if end if end sub ' stateAllowsOSD: Check if current video state allows showing the OSD ' ' @return {boolean} indicating if video state allows the OSD to show function stateAllowsOSD() as boolean validStates = ["playing", "paused", "stopped"] return inArray(validStates, m.top.state) end function ' availSubtitleTrackIdx: Returns Roku's index for requested subtitle track ' ' @param {string} tracknameToFind - TrackName for subtitle we're looking to match ' @return {integer} indicating Roku's index for requested subtitle track. Returns -1 if not found function availSubtitleTrackIdx(tracknameToFind as string) as integer idx = 0 for each availTrack in m.top.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 function onKeyEvent(key as string, press as boolean) as boolean ' Keypress handler while user is inside the chapter menu if m.chapterMenu.hasFocus() if not press then return false if key = "OK" focusedChapter = m.chapterMenu.itemFocused selectedChapter = m.chapterMenu.content.getChild(focusedChapter) seekTime = selectedChapter.playstart ' Don't seek if user clicked on No Chapter Data if seekTime = m.playbackEnum.null then return true m.top.seek = seekTime return true end if if key = "back" or key = "replay" m.chapterList.visible = false m.osd.showChapterList = false m.chapterMenu.setFocus(false) m.osd.hasFocus = true m.osd.setFocus(true) return true end if if key = "play" handleVideoPlayPauseAction() end if return true end if if key = "OK" and m.nextEpisodeButton.hasfocus() and not m.top.trickPlayBar.visible m.top.control = "stop" m.top.state = "finished" hideNextEpisodeButton() return true else 'Hide Next Episode Button if m.nextEpisodeButton.opacity > 0 or m.nextEpisodeButton.hasFocus() m.nextEpisodeButton.opacity = 0 m.nextEpisodeButton.setFocus(false) m.top.setFocus(true) end if end if if not press then return false if key = "down" and not m.top.trickPlayBar.visible if not m.LoadMetaDataTask.isIntro ' Don't allow user to open menu prior to video loading if not stateAllowsOSD() then return true m.osd.visible = true m.osd.hasFocus = true m.osd.setFocus(true) return true end if else if key = "up" and not m.top.trickPlayBar.visible if not m.LoadMetaDataTask.isIntro ' Don't allow user to open menu prior to video loading if not stateAllowsOSD() then return true m.osd.visible = true m.osd.hasFocus = true m.osd.setFocus(true) return true end if else if key = "OK" and not m.top.trickPlayBar.visible if not m.LoadMetaDataTask.isIntro ' Don't allow user to open menu prior to video loading if not stateAllowsOSD() then return true ' Show OSD, but don't pause video m.osd.visible = true m.osd.hasFocus = true m.osd.setFocus(true) return true end if return false end if ' Disable OSD for intro videos if not m.LoadMetaDataTask.isIntro if key = "play" and not m.top.trickPlayBar.visible ' Don't allow user to open menu prior to video loading if not stateAllowsOSD() then return true ' If video is paused, resume it and don't show OSD if m.top.state = "paused" m.top.control = "resume" return true end if ' Pause video and show OSD m.top.control = "pause" m.osd.visible = true m.osd.hasFocus = true m.osd.setFocus(true) return true end if end if if key = "back" m.top.control = "stop" end if return false end function