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