HMS

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

MainScene.brs (14276B)


      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 
    181     ' Try to get the last selected poster for this category
    182     GetKeystoreValue(m.metadataTask.category, "JumpToPoster")
    183 end sub
    184 
    185 ' OnPosterSelected it called when OK is hit on the selected poster
    186 ' It starts the video player
    187 sub OnPosterSelected()
    188     print "MainScene->OnPosterSelected()"
    189     print m.posterGrid.itemSelected
    190     StartVideoPlayer(m.posterGrid.itemSelected)
    191 
    192     ' Store the new selection for this category
    193     SetKeystoreValue(m.metadataTask.category, m.posterGrid.itemSelected.ToStr(), "ResetKeystoreTask")
    194 end sub
    195 
    196 ' OnPosterFocused updates the information at the top of the screen with the
    197 ' category name and the name of the selected video
    198 sub OnPosterFocused()
    199     print "MainScene->OnPosterFocused()"
    200     print m.posterGrid.itemFocused
    201     print m.metadata[m.posterGrid.itemFocused].ShortDescriptionLine1
    202     m.details.text = m.categories[m.listPanel.createNextPanelIndex] + " | " + m.metadata[m.posterGrid.itemFocused].ShortDescriptionLine1
    203 end sub
    204 
    205 
    206 ' JumpToPoster moves the selection to the last played video if there is one
    207 sub JumpToPoster()
    208     ResetKeystoreTask()
    209 
    210     ' Was there a result?
    211     if m.keystoreTask.value <> ""
    212         item =  m.keystoreTask.value.ToInt()
    213         if item < m.metadata.Count()
    214             ' If the animation will be short, animate, otherwise jump
    215             if item < 42
    216                 m.posterGrid.animateToItem = item
    217             else
    218                 m.posterGrid.jumpToItem = item
    219             end if
    220         end if
    221     end if
    222 end sub
    223 
    224 '***********************
    225 ' Video player functions
    226 '***********************
    227 
    228 ' SetupVideoPlayer sets up the observers for the video node
    229 ' and how often it will report the playback position
    230 sub SetupVideoPlayer()
    231     ' Setup the video player
    232     m.video = m.top.FindNode("player")
    233     m.video.observeField("state", "OnVideoStateChange")
    234     m.video.observeField("position", "OnVideoPositionChange")
    235     m.video.notificationInterval = 5
    236     ' map of events that should be handled on state change
    237     m.statesToHandle = {
    238         finished: ""
    239         error:    ""
    240     }
    241 end sub
    242 
    243 ' StartVideoPlayer is called with the index of the video to play
    244 ' It runs a keystore task to retrieve the last playback position for the
    245 ' selected video and then calls StartPlayback
    246 sub StartVideoPlayer(index as integer)
    247     print "MainScene->StartVideoPlayer()"
    248     print m.metadata[index].ShortDescriptionLine1
    249     m.video.content = m.metadata[index]
    250 
    251     ' Get the previous playback position, if any, and start playing
    252     GetKeystoreValue(m.video.content.Title, "StartPlayback")
    253 end sub
    254 
    255 ' StartPlayback is called by GetKeystoreValue which may have a starting
    256 ' position. If so, it is set, and playback is started.
    257 sub StartPlayback()
    258     print "MainScene->StartPlayback()"
    259     ResetKeystoreTask()
    260 
    261     ' Was there a result?
    262     if m.keystoreTask.value <> ""
    263         m.video.seek = m.keystoreTask.value.ToInt()
    264     end if
    265     ' Play the selected video
    266     m.video.visible = true
    267     m.video.SetFocus(true)
    268     m.video.control = "play"
    269 end sub
    270 
    271 ' OnVideoStateChanged is called when the playback is finished or there is an error
    272 ' it will save the last playback position and close the video player
    273 sub OnVideoStateChange()
    274     print "MainScene->OnVideoStateChange()"
    275     ? "video state: " + m.video.state
    276     if m.video.state = "finished"
    277         ' Set the playback position back to 0 if it played all the way
    278         SetKeystoreValue(m.video.content.Title, "0", "ResetKeystoreTask")
    279     end if
    280     if m.video.content <> invalid AND m.statesToHandle[m.video.state] <> invalid
    281         m.timer = CreateObject("roSgnode", "Timer")
    282         m.timer.observeField("fire", "CloseVideoPlayer")
    283         m.timer.duration = 0.3
    284         m.timer.control = "start"
    285     end if
    286 end sub
    287 
    288 ' CloseVideoPlayer coses the player and stops playback, returning focus to the
    289 ' poster grid.
    290 sub CloseVideoPlayer()
    291     print "MainScene->CloseVideoPlayer()"
    292     m.video.visible = false
    293     m.video.control = "stop"
    294     m.posterGrid.SetFocus(true)
    295 end sub
    296 
    297 ' OnVideoPositionChange is called every 5 seconds and it sends the position
    298 ' to the keystore server
    299 sub OnVideoPositionChange()
    300     print "MainScene->OnVideoPositionChange()"
    301     if m.video.positionInfo = invalid
    302         return
    303     end if
    304     print "position = "; m.video.positionInfo.video
    305     SetKeystoreValue(m.video.content.Title, m.video.positionInfo.video.ToStr(), "ResetKeystoreTask")
    306 end sub
    307 
    308 ' onKeyEvent handles hitting 'back' during playback and play when selecting a poster grid
    309 ' which normally doesn't start playback.
    310 function onKeyEvent(key as String, press as Boolean) as Boolean
    311     if press
    312         if key = "back"  'If the back button is pressed
    313             if m.video.visible
    314                 CloseVideoPlayer()
    315                 return true
    316             else
    317                 return false
    318             end if
    319         else if key = "play"
    320             StartVideoPlayer(m.posterGrid.itemFocused)
    321         end if
    322     end if
    323 end Function
    324 
    325 
    326 '***********************
    327 ' Server setup functions
    328 '***********************
    329 
    330 ' RunSetupServerDialog runs the dialog prompting the user for the server url
    331 sub RunSetupServerDialog(url as string)
    332     print "MainScene->RunSetupServerDialog()"
    333     m.serverDialog = createObject("roSGNode", "SetupServerDialog")
    334     m.serverDialog.ObserveField("serverurl", "OnSetupServerURL")
    335     m.serverDialog.text = url
    336     m.top.dialog = m.serverDialog
    337 end sub
    338 
    339 ' OnSetupServerURL is called when the user has entered a url, it then validates it
    340 ' by calling RunValidateURLTask
    341 sub OnSetupServerURL()
    342     print "MainScene->OnSetupServerURL()"
    343     print m.serverDialog.serverurl
    344 
    345     RunValidateURLTask(m.serverDialog.serverurl)
    346 end sub
    347 
    348 ' RunValidateURLTask is called to validate the url that the user entered in the dialog
    349 ' it starts a task and calls OnValidateChanged when done.
    350 sub RunValidateURLTask(url as string)
    351     print "MainScene->RunValidateURLTask()"
    352 
    353     m.validateTask = CreateObject("roSGNode", "ValidateURLTask")
    354     m.validateTask.serverurl = url
    355     m.validateTask.ObserveField("valid", "OnValidateChanged")
    356     m.validateTask.control = "run"
    357 end sub
    358 
    359 ' OnValidateChanged checks the result of validating the URL and either runs the setup
    360 ' dialog again, or sets the serverurl which triggers loading the categories and the
    361 ' rest of the screen.
    362 sub OnValidateChanged()
    363     print "MainScene->OnValidateChanged"
    364     print "server url = "; m.validateTask.serverurl
    365     print "valid? "; m.validateTask.valid
    366     print "keystore? "; m.validateTask.keystore
    367     if not m.validateTask.valid then
    368         ' Still invalid, run it again
    369         RunSetupServerDialog(m.validateTask.serverurl)
    370     else
    371         ' Valid url, trigger the content load
    372         m.top.serverurl = m.validateTask.serverurl
    373         ' And save it for next time
    374         RegWrite("ServerURL", m.validateTask.serverurl)
    375         m.keystoreTask.has_keystore = m.validateTask.keystore
    376     end if
    377 end sub
    378 
    379 
    380 ' ******************
    381 ' Keystore functions
    382 ' ******************
    383 
    384 ' GetKeystoreValue retrieves a string from the keystore server
    385 ' It calls the callback when it is done (or has failed)
    386 ' The callback needs to call ResetKeystoreTask to clear the
    387 ' done field.
    388 sub GetKeystoreValue(key as string, callback as string)
    389     m.keystoreTask.serverurl = m.top.serverurl
    390     m.keystoreTask.key = key
    391     m.keystoreTask.value = ""
    392     m.keystoreTask.command = "get"
    393     if callback <> ""
    394         m.keystoreTask.ObserveField("done", callback)
    395     end if
    396     m.keystoreTask.control = "run"
    397 end sub
    398 
    399 ' SetKeystoreValue sets a key to a string on the keystore server
    400 ' It calls the callback when it is done (or has failed)
    401 ' The callback needs to call ResetKeystoreTask to clear the
    402 ' done field.
    403 sub SetKeystoreValue(key as string, value as string, callback as string)
    404     m.keystoreTask.serverurl = m.top.serverurl
    405     m.keystoreTask.key = key
    406     m.keystoreTask.value = value
    407     m.keystoreTask.command = "set"
    408     if callback <> ""
    409         m.keystoreTask.ObserveField("done", callback)
    410     end if
    411     m.keystoreTask.control = "run"
    412 end sub
    413 
    414 ' ResetKeystoreTask clears the observer and sets done back to false
    415 sub ResetKeystoreTask()
    416     m.keystoreTask.UNObserveField("done")
    417     m.keystoreTask.done = false
    418 end sub