mirror of
https://github.com/jellyfin/jellyfin-roku.git
synced 2024-11-30 09:50:46 +00:00
905 lines
29 KiB
Plaintext
905 lines
29 KiB
Plaintext
import "pkg:/source/utils/misc.bs"
|
|
import "pkg:/source/utils/config.bs"
|
|
import "pkg:/source/utils/session.bs"
|
|
import "pkg:/source/roku_modules/log/LogMixin.brs"
|
|
|
|
sub init()
|
|
m.log = log.Logger("VideoPlayerView")
|
|
' Hide the overhang on init to prevent showing 2 clocks
|
|
m.top.getScene().findNode("overhang").visible = false
|
|
userSettings = m.global.session.user.settings
|
|
m.currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
|
m.originalClosedCaptionState = invalid
|
|
|
|
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("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 userSettings["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 = userSettings["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"
|
|
queueManager = m.global.queueManager
|
|
|
|
' If there is something next in the queue, play it
|
|
if queueManager.callFunc("getPosition") < queueManager.callFunc("getCount") - 1
|
|
m.top.control = "stop"
|
|
session.video.Delete()
|
|
m.global.sceneManager.callFunc("clearPreviousScene")
|
|
queueManager.callFunc("moveForward")
|
|
queueManager.callFunc("playQueue")
|
|
end if
|
|
|
|
return
|
|
end if
|
|
|
|
if action = "itemback"
|
|
queueManager = m.global.queueManager
|
|
|
|
' If there is something previous in the queue, play it
|
|
if queueManager.callFunc("getPosition") > 0
|
|
m.top.control = "stop"
|
|
session.video.Delete()
|
|
m.global.sceneManager.callFunc("clearPreviousScene")
|
|
queueManager.callFunc("moveBack")
|
|
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.observeField("state", "onState")
|
|
|
|
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
|
|
|
|
' If video is an episode, attempt to add season and episode numbers to OSD
|
|
if m.top.content.contenttype = 4
|
|
if isValid(videoContent[0].seasonNumber)
|
|
m.osd.seasonNumber = videoContent[0].seasonNumber
|
|
end if
|
|
|
|
if isValid(videoContent[0].episodeNumber)
|
|
m.osd.episodeNumber = videoContent[0].episodeNumber
|
|
end if
|
|
|
|
if isValid(videoContent[0].episodeNumberEnd)
|
|
m.osd.episodeNumberEnd = videoContent[0].episodeNumberEnd
|
|
end if
|
|
end if
|
|
|
|
' Attempt to add logo to OSD
|
|
if isValidAndNotEmpty(videoContent[0].logoImage)
|
|
m.osd.logoImage = videoContent[0].logoImage
|
|
|
|
' Don't show both the logo and the video title if this isn't an episode
|
|
if m.top.content.contenttype <> 4
|
|
m.osd.itemTitleText = ""
|
|
end if
|
|
end if
|
|
|
|
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
|
|
if selectedSubtitle.IsForced
|
|
' If IsForced, make sure to remember the Roku global setting so we
|
|
' can set it back when the video is done playing.
|
|
m.originalClosedCaptionState = m.top.globalCaptionMode
|
|
end if
|
|
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 then return
|
|
userSettings = m.global.session.user.settings
|
|
if userSettings["playback.playnextepisode"] = "disabled" then return
|
|
if userSettings["playback.playnextepisode"] = "webclient" and not m.global.session.user.Configuration.EnableNextEpisodeAutoPlay then return
|
|
|
|
m.nextEpisodeButton.visible = true
|
|
m.showNextEpisodeButtonAnimation.control = "start"
|
|
m.nextEpisodeButton.setFocus(true)
|
|
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
|
|
if m.top.duration = 0
|
|
m.osd.progressPercentage = 0
|
|
else
|
|
m.osd.progressPercentage = m.top.position / m.top.duration
|
|
end if
|
|
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)
|
|
m.log.debug("start onState()", m.top.state)
|
|
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
|
|
|
|
if m.top.state = "buffering"
|
|
' When buffering, start timer to monitor buffering process
|
|
if isValid(m.bufferCheckTimer)
|
|
m.bufferCheckTimer.control = "start"
|
|
m.bufferCheckTimer.ObserveField("fire", "bufferCheck")
|
|
end if
|
|
else if m.top.state = "error"
|
|
m.log.error(m.top.errorCode, m.top.errorMsg, m.top.errorStr, m.top.errorCode)
|
|
|
|
print m.top.errorInfo
|
|
|
|
if not m.playReported and m.top.transcodeAvailable
|
|
m.top.retryWithTranscoding = true ' If playback was not reported, retry with transcoding
|
|
else if m.top.errorStr = "decoder:pump:Unsupported AAC stream."
|
|
m.log.info("retrying video with mp3 audio stream", m.currentItem.id, m.top.SelectedSubtitle, m.top.audioIndex)
|
|
|
|
m.top.unobserveField("state")
|
|
m.LoadMetaDataTask.forceMp3 = true
|
|
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"
|
|
else
|
|
' If an error was encountered, Display dialog
|
|
showPlaybackErrorDialog(tr("Error During Playback"))
|
|
session.video.Delete()
|
|
end if
|
|
|
|
|
|
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
|
|
session.video.Delete()
|
|
else if m.top.state = "finished"
|
|
m.playbackTimer.control = "stop"
|
|
ReportPlayback("finished")
|
|
session.video.Delete()
|
|
else
|
|
m.log.warning("Unhandled state", m.top.state, m.playReported, m.playFinished)
|
|
end if
|
|
m.log.debug("end onState()", m.top.state)
|
|
end sub
|
|
|
|
'
|
|
' Report playback to server
|
|
sub ReportPlayback(state = "update" as string)
|
|
if m.top.position = invalid then return
|
|
|
|
m.log.debug("start ReportPlayback()", state, int(m.top.position))
|
|
|
|
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 isValid(m.top.content) and isValid(m.top.content.live) and m.top.content.live
|
|
params.append({
|
|
"MediaSourceId": m.top.transcodeParams.MediaSourceId,
|
|
"LiveStreamId": m.top.transcodeParams.LiveStreamId
|
|
})
|
|
m.bufferCheckTimer.duration = 30
|
|
end if
|
|
|
|
if (state = "stop" or state = "finished") and m.originalClosedCaptionState <> invalid
|
|
m.log.debug("ReportPlayback() setting", m.top.globalCaptionMode, "back to", m.originalClosedCaptionState)
|
|
m.top.globalCaptionMode = m.originalClosedCaptionState
|
|
m.originalClosedCaptionState = invalid
|
|
end if
|
|
|
|
' Report playstate via worker task
|
|
playstateTask = m.global.playstateTask
|
|
playstateTask.setFields({ status: state, params: params })
|
|
playstateTask.control = "RUN"
|
|
|
|
m.log.debug("end ReportPlayback()", state, int(m.top.position))
|
|
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"
|
|
session.video.Delete()
|
|
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"
|
|
session.video.Delete()
|
|
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"
|
|
session.video.Delete()
|
|
end if
|
|
|
|
return false
|
|
end function
|