HMS

Home Media Server for Roku Players
git clone https://www.brianlane.com/git/HMS
Log | Files | Refs | README | LICENSE

MainScene.brs (13477B)


      1 '********************************************************************
      2 '**  Home Media Server Application - MainScene
      3 '**  Copyright (c) 2022 Brian C. Lane All Rights Reserved.
      4 '********************************************************************
      5 sub Init()
      6     print "MainScene->Init()"
      7     m.top.ObserveField("serverurl", "RunContentTask")
      8     m.details = m.top.FindNode("details")
      9     m.keystoreTask = CreateObject("roSGNode", "KeystoreTask")
     10 
     11     StartClock()
     12 
     13     ' Get the server URL from the registry or a user dialog
     14     url = RegRead("ServerURL")
     15     if url = invalid then
     16         RunSetupServerDialog("")
     17     else
     18         ' Validate the url
     19         RunValidateURLTask(url)
     20     end if
     21 
     22     ' Setup the video player node
     23     SetupVideoPlayer()
     24 end sub
     25 
     26 
     27 '****************
     28 ' Clock functions
     29 '****************
     30 
     31 ' StartClock starts displaying the clock in the upper right of the screen
     32 ' It calls UpdateClock every 5 seconds
     33 sub StartClock()
     34     m.clock = m.top.FindNode("clock")
     35     m.clockTimer = m.top.FindNode("clockTimer")
     36     m.clockTimer.ObserveField("fire", "UpdateClock")
     37     m.clockTimer.control = "start"
     38     UpdateClock()
     39 end sub
     40 
     41 ' Update the clock, showing HH:MM AM/PM in the upper right of the screen
     42 sub UpdateClock()
     43     now = CreateObject("roDateTime")
     44     now.ToLocalTime()
     45     hour = now.GetHours()
     46     use_ampm = true
     47     if use_ampm then
     48         if hour < 12 then
     49             ampm = " AM"
     50         else
     51             ampm = " PM"
     52             if hour > 12 then
     53                 hour = hour - 12
     54             end if
     55         end if
     56     end if
     57     hour = tostr(hour)
     58     minutes = now.GetMinutes()
     59     if minutes < 10 then
     60         minutes = "0"+tostr(minutes)
     61     else
     62         minutes = tostr(minutes)
     63     end if
     64     m.clock.text = now.GetWeekday()+" "+hour+":"+minutes+ampm
     65 end sub
     66 
     67 
     68 '******************
     69 ' Content functions
     70 '******************
     71 '
     72 ' RunContentTask is called when the server url has been set from the registry or
     73 ' entered by the user, and verified to be valid.
     74 ' It then starts the task to load the list of categories
     75 sub RunContentTask()
     76     print "MainScene->RunContentTask()"
     77 
     78     m.contentTask = CreateObject("roSGNode", "MainLoaderTask")
     79     m.contentTask.serverurl = m.top.serverurl
     80     m.contentTask.ObserveField("categories", "OnCategoriesLoaded")
     81     m.contentTask.control = "run"
     82 end sub
     83 
     84 ' OnCategoriesLoaded is called when the list of categories has been recalled
     85 ' from the server. It is returned as a list of strings and is displayed on
     86 ' the left side of the screen.
     87 sub OnCategoriesLoaded()
     88     print "MainScene->OnCategoriesLoaded()"
     89     print m.contentTask.categories
     90     m.categories = m.contentTask.categories
     91 
     92     ' Add these to the list on the left side of the screen
     93     m.panels = m.top.FindNode("panels")
     94     m.listPanel = m.panels.CreateChild("ListPanel")
     95     m.listPanel.observeField("createNextPanelIndex", "OnCreateNextPanelIndex")
     96 
     97     m.labelList = CreateObject("roSGNode", "LabelList")
     98     m.listPanel.list = m.labelList
     99     m.listPanel.appendChild(m.labelList)
    100     m.listPanel.SetFocus(true)
    101 
    102     ln = CreateObject("roSGNode", "ContentNode")
    103     for each item in m.categories:
    104         n = CreateObject("roSGNode", "ContentNode")
    105         n.title = item
    106         ln.appendChild(n)
    107     end for
    108     m.labelList.content = ln
    109 end sub
    110 
    111 ' OnCreateNextPanelIndex is called when a new category is selected (up/down)
    112 ' It populates the poster grid on the right of the screen
    113 sub OnCreateNextPanelIndex()
    114     print "MainScene->OnCreateNextPanelIndex()"
    115     print m.listPanel.createNextPanelIndex
    116     print m.categories[m.listPanel.createNextPanelIndex]
    117     m.details.text = ""
    118     RunCategoryLoadTask(m.categories[m.listPanel.createNextPanelIndex])
    119 end sub
    120 
    121 ' RunCategoryLoadTask runs a task to get the metadata for the selected category
    122 ' It calls OnMetadataLoaded when it is done
    123 sub RunCategoryLoadTask(category as string)
    124     print "MainScene->RunCategoryLoadTask()"
    125     print category
    126 
    127     m.metadataTask = CreateObject("roSGNode", "CategoryLoaderTask")
    128     m.metadataTask.serverurl = m.top.serverurl
    129     m.metadataTask.category = category
    130     m.metadataTask.ObserveField("metadata", "OnMetadataLoaded")
    131     m.metadataTask.control = "run"
    132 end sub
    133 
    134 ' OnMetadataLoaded is called when it has retrieved the metadata for the category
    135 ' It creates one GridPanel and one PosterGrid then re-populates them with each
    136 ' new batch of metadata.
    137 sub OnMetadataLoaded()
    138     print "MainScene->OnMetadataLoaded()"
    139     m.metadata = m.metadataTask.metadata
    140     if m.metadata = invalid then
    141         return
    142     end if
    143     print "Got "; m.metadataTask.metadata.Count(); " items."
    144 
    145     ' Create one GridPanel and one PosterGrid, then reuse them for each category
    146     ' This may not be quite right, but it works for now.
    147     if m.gridPanel = invalid then
    148         print "Creating new GridPanel"
    149         m.gridPanel = m.panels.CreateChild("GridPanel")
    150         m.gridPanel.panelSize = "full"
    151         m.gridPanel.isFullScreen = true
    152         m.gridPanel.focusable = true
    153         m.gridPanel.hasNextPanel = false
    154         m.gridPanel.createNextPanelOnItemFocus = false
    155 
    156         m.posterGrid = CreateObject("roSGNode", "PosterGrid")
    157         m.posterGrid.basePosterSize = "[222, 330]"
    158         m.posterGrid.itemSpacing = "[6, 9]"
    159         m.posterGrid.posterDisplayMode = "scaleToZoom"
    160         m.posterGrid.caption1NumLines = "1"
    161         m.posterGrid.numColumns = "7"
    162         m.posterGrid.numRows = "3"
    163         m.posterGrid.ObserveField("itemSelected", "OnPosterSelected")
    164         m.posterGrid.ObserveField("itemFocused", "OnPosterFocused")
    165 
    166         m.gridPanel.appendChild(m.PosterGrid)
    167         m.gridPanel.grid = m.posterGrid
    168         m.listPanel.nextPanel = m.gridPanel
    169     end if
    170 
    171     cn = CreateObject("roSGNode", "ContentNode")
    172     for each item in m.metadata
    173         n = CreateObject("roSGNode", "ContentNode")
    174         n.HDPosterUrl = item.HDPosterUrl
    175         n.SDPosterUrl = item.SDPosterUrl
    176         n.ShortDescriptionLine1 = item.ShortDescriptionLine1
    177         cn.appendChild(n)
    178     end for
    179     m.posterGrid.content = cn
    180 end sub
    181 
    182 ' OnPosterSelected it called when OK is hit on the selected poster
    183 ' It starts the video player
    184 sub OnPosterSelected()
    185     print "MainScene->OnPosterSelected()"
    186     print m.posterGrid.itemSelected
    187     StartVideoPlayer(m.posterGrid.itemSelected)
    188 end sub
    189 
    190 ' OnPosterFocused updates the information at the top of the screen with the
    191 ' category name and the name of the selected video
    192 sub OnPosterFocused()
    193     print "MainScene->OnPosterFocused()"
    194     print m.posterGrid.itemFocused
    195     print m.metadata[m.posterGrid.itemFocused].ShortDescriptionLine1
    196     m.details.text = m.categories[m.listPanel.createNextPanelIndex] + " | " + m.metadata[m.posterGrid.itemFocused].ShortDescriptionLine1
    197 end sub
    198 
    199 
    200 '***********************
    201 ' Video player functions
    202 '***********************
    203 
    204 ' SetupVideoPlayer sets up the observers for the video node
    205 ' and how often it will report the playback position
    206 sub SetupVideoPlayer()
    207     ' Setup the video player
    208     m.video = m.top.FindNode("player")
    209     m.video.observeField("state", "OnVideoStateChange")
    210     m.video.observeField("position", "OnVideoPositionChange")
    211     m.video.notificationInterval = 5
    212     ' map of events that should be handled on state change
    213     m.statesToHandle = {
    214         finished: ""
    215         error:    ""
    216     }
    217 end sub
    218 
    219 ' StartVideoPlayer is called with the index of the video to play
    220 ' It runs a keystore task to retrieve the last playback position for the
    221 ' selected video and then calls StartPlayback
    222 sub StartVideoPlayer(index as integer)
    223     print "MainScene->StartVideoPlayer()"
    224     print m.metadata[index].ShortDescriptionLine1
    225     m.video.content = m.metadata[index]
    226 
    227     ' Get the previous playback position, if any, and start playing
    228     GetKeystoreValue(m.video.content.Title, "StartPlayback")
    229 end sub
    230 
    231 ' StartPlayback is called by GetKeystoreValue which may have a starting
    232 ' position. If so, it is set, and playback is started.
    233 sub StartPlayback()
    234     print "MainScene->StartPlayback()"
    235     ResetKeystoreTask()
    236 
    237     ' Was there a result?
    238     if m.keystoreTask.value <> ""
    239         m.video.seek = m.keystoreTask.value.ToInt()
    240     end if
    241     ' Play the selected video
    242     m.video.visible = true
    243     m.video.SetFocus(true)
    244     m.video.control = "play"
    245 end sub
    246 
    247 ' OnVideoStateChanged is called when the playback is finished or there is an error
    248 ' it will save the last playback position and close the video player
    249 sub OnVideoStateChange()
    250     print "MainScene->OnVideoStateChange()"
    251     ? "video state: " + m.video.state
    252     if m.video.state = "finished"
    253         ' Set the playback position back to 0 if it played all the way
    254         SetKeystoreValue(m.video.content.Title, "0", "ResetKeystoreTask")
    255     end if
    256     if m.video.content <> invalid AND m.statesToHandle[m.video.state] <> invalid
    257         m.timer = CreateObject("roSgnode", "Timer")
    258         m.timer.observeField("fire", "CloseVideoPlayer")
    259         m.timer.duration = 0.3
    260         m.timer.control = "start"
    261     end if
    262 end sub
    263 
    264 ' CloseVideoPlayer coses the player and stops playback, returning focus to the
    265 ' poster grid.
    266 sub CloseVideoPlayer()
    267     print "MainScene->CloseVideoPlayer()"
    268     m.video.visible = false
    269     m.video.control = "stop"
    270     m.posterGrid.SetFocus(true)
    271 end sub
    272 
    273 ' OnVideoPositionChange is called every 5 seconds and it sends the position
    274 ' to the keystore server
    275 sub OnVideoPositionChange()
    276     print "MainScene->OnVideoPositionChange()"
    277     if m.video.positionInfo = invalid
    278         return
    279     end if
    280     print "position = "; m.video.positionInfo.video
    281     SetKeystoreValue(m.video.content.Title, m.video.positionInfo.video.ToStr(), "ResetKeystoreTask")
    282 end sub
    283 
    284 ' onKeyEvent handles hitting 'back' during playback and play when selecting a poster grid
    285 ' which normally doesn't start playback.
    286 function onKeyEvent(key as String, press as Boolean) as Boolean
    287     if press
    288         if key = "back"  'If the back button is pressed
    289             if m.video.visible
    290                 CloseVideoPlayer()
    291                 return true
    292             else
    293                 return false
    294             end if
    295         else if key = "play"
    296             StartVideoPlayer(m.posterGrid.itemFocused)
    297         end if
    298     end if
    299 end Function
    300 
    301 
    302 '***********************
    303 ' Server setup functions
    304 '***********************
    305 
    306 ' RunSetupServerDialog runs the dialog prompting the user for the server url
    307 sub RunSetupServerDialog(url as string)
    308     print "MainScene->RunSetupServerDialog()"
    309     m.serverDialog = createObject("roSGNode", "SetupServerDialog")
    310     m.serverDialog.ObserveField("serverurl", "OnSetupServerURL")
    311     m.serverDialog.text = url
    312     m.top.dialog = m.serverDialog
    313 end sub
    314 
    315 ' OnSetupServerURL is called when the user has entered a url, it then validates it
    316 ' by calling RunValidateURLTask
    317 sub OnSetupServerURL()
    318     print "MainScene->OnSetupServerURL()"
    319     print m.serverDialog.serverurl
    320 
    321     RunValidateURLTask(m.serverDialog.serverurl)
    322 end sub
    323 
    324 ' RunValidateURLTask is called to validate the url that the user entered in the dialog
    325 ' it starts a task and calls OnValidateChanged when done.
    326 sub RunValidateURLTask(url as string)
    327     print "MainScene->RunValidateURLTask()"
    328 
    329     m.validateTask = CreateObject("roSGNode", "ValidateURLTask")
    330     m.validateTask.serverurl = url
    331     m.validateTask.ObserveField("valid", "OnValidateChanged")
    332     m.validateTask.control = "run"
    333 end sub
    334 
    335 ' OnValidateChanged checks the result of validating the URL and either runs the setup
    336 ' dialog again, or sets the serverurl which triggers loading the categories and the
    337 ' rest of the screen.
    338 sub OnValidateChanged()
    339     print "MainScene->OnValidateChanged"
    340     print "server url = "; m.validateTask.serverurl
    341     print "valid? "; m.validateTask.valid
    342     print "keystore? "; m.validateTask.keystore
    343     if not m.validateTask.valid then
    344         ' Still invalid, run it again
    345         RunSetupServerDialog(m.validateTask.serverurl)
    346     else
    347         ' Valid url, trigger the content load
    348         m.top.serverurl = m.validateTask.serverurl
    349         ' And save it for next time
    350         RegWrite("ServerURL", m.validateTask.serverurl)
    351         m.keystoreTask.has_keystore = m.validateTask.keystore
    352     end if
    353 end sub
    354 
    355 
    356 ' ******************
    357 ' Keystore functions
    358 ' ******************
    359 
    360 ' GetKeystoreValue retrieves a string from the keystore server
    361 ' It calls the callback when it is done (or has failed)
    362 ' The callback needs to call ResetKeystoreTask to clear the
    363 ' done field.
    364 sub GetKeystoreValue(key as string, callback as string)
    365     m.keystoreTask.serverurl = m.top.serverurl
    366     m.keystoreTask.key = key
    367     m.keystoreTask.value = ""
    368     m.keystoreTask.command = "get"
    369     if callback <> ""
    370         m.keystoreTask.ObserveField("done", callback)
    371     end if
    372     m.keystoreTask.control = "run"
    373 end sub
    374 
    375 ' SetKeystoreValue sets a key to a string on the keystore server
    376 ' It calls the callback when it is done (or has failed)
    377 ' The callback needs to call ResetKeystoreTask to clear the
    378 ' done field.
    379 sub SetKeystoreValue(key as string, value as string, callback as string)
    380     m.keystoreTask.serverurl = m.top.serverurl
    381     m.keystoreTask.key = key
    382     m.keystoreTask.value = value
    383     m.keystoreTask.command = "set"
    384     if callback <> ""
    385         m.keystoreTask.ObserveField("done", callback)
    386     end if
    387     m.keystoreTask.control = "run"
    388 end sub
    389 
    390 ' ResetKeystoreTask clears the observer and sets done back to false
    391 sub ResetKeystoreTask()
    392     m.keystoreTask.UNObserveField("done")
    393     m.keystoreTask.done = false
    394 end sub