MainScene.brs (14488B)
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 print "Seeking to "; m.keystoreTask.value.ToInt() 264 m.video.seek = m.keystoreTask.value.ToInt() 265 end if 266 ' Play the selected video 267 m.video.visible = true 268 m.video.SetFocus(true) 269 m.video.control = "play" 270 271 end sub 272 273 ' OnVideoStateChanged is called when the playback is finished or there is an error 274 ' it will save the last playback position and close the video player 275 sub OnVideoStateChange() 276 print "MainScene->OnVideoStateChange()" 277 ? "video state: " + m.video.state 278 if m.video.state = "finished" 279 ' Set the playback position back to 0 if it played all the way 280 SetKeystoreValue(m.video.content.Title, "0", "ResetKeystoreTask") 281 end if 282 if m.video.content <> invalid AND m.statesToHandle[m.video.state] <> invalid 283 m.timer = CreateObject("roSgnode", "Timer") 284 m.timer.observeField("fire", "CloseVideoPlayer") 285 m.timer.duration = 0.3 286 m.timer.control = "start" 287 end if 288 end sub 289 290 ' CloseVideoPlayer coses the player and stops playback, returning focus to the 291 ' poster grid. 292 sub CloseVideoPlayer() 293 print "MainScene->CloseVideoPlayer()" 294 m.video.visible = false 295 m.video.control = "stop" 296 m.posterGrid.SetFocus(true) 297 298 ' Store the new selection for this category 299 SetKeystoreValue(m.metadataTask.category, m.posterGrid.itemFocused.ToStr(), "ResetKeystoreTask") 300 end sub 301 302 ' OnVideoPositionChange is called every 5 seconds and it sends the position 303 ' to the keystore server 304 sub OnVideoPositionChange() 305 print "MainScene->OnVideoPositionChange()" 306 if m.video.positionInfo = invalid 307 return 308 end if 309 print "position = "; m.video.positionInfo.video 310 SetKeystoreValue(m.video.content.Title, m.video.positionInfo.video.ToStr(), "ResetKeystoreTask") 311 end sub 312 313 ' onKeyEvent handles hitting 'back' during playback and play when selecting a poster grid 314 ' which normally doesn't start playback. 315 function onKeyEvent(key as String, press as Boolean) as Boolean 316 if press 317 if key = "back" 'If the back button is pressed 318 if m.video.visible 319 CloseVideoPlayer() 320 return true 321 else 322 return false 323 end if 324 else if key = "play" 325 StartVideoPlayer(m.posterGrid.itemFocused) 326 end if 327 end if 328 end Function 329 330 331 '*********************** 332 ' Server setup functions 333 '*********************** 334 335 ' RunSetupServerDialog runs the dialog prompting the user for the server url 336 sub RunSetupServerDialog(url as string) 337 print "MainScene->RunSetupServerDialog()" 338 m.serverDialog = createObject("roSGNode", "SetupServerDialog") 339 m.serverDialog.ObserveField("serverurl", "OnSetupServerURL") 340 m.serverDialog.text = url 341 m.top.dialog = m.serverDialog 342 end sub 343 344 ' OnSetupServerURL is called when the user has entered a url, it then validates it 345 ' by calling RunValidateURLTask 346 sub OnSetupServerURL() 347 print "MainScene->OnSetupServerURL()" 348 print m.serverDialog.serverurl 349 350 RunValidateURLTask(m.serverDialog.serverurl) 351 end sub 352 353 ' RunValidateURLTask is called to validate the url that the user entered in the dialog 354 ' it starts a task and calls OnValidateChanged when done. 355 sub RunValidateURLTask(url as string) 356 print "MainScene->RunValidateURLTask()" 357 358 m.validateTask = CreateObject("roSGNode", "ValidateURLTask") 359 m.validateTask.serverurl = url 360 m.validateTask.ObserveField("valid", "OnValidateChanged") 361 m.validateTask.control = "run" 362 end sub 363 364 ' OnValidateChanged checks the result of validating the URL and either runs the setup 365 ' dialog again, or sets the serverurl which triggers loading the categories and the 366 ' rest of the screen. 367 sub OnValidateChanged() 368 print "MainScene->OnValidateChanged" 369 print "server url = "; m.validateTask.serverurl 370 print "valid? "; m.validateTask.valid 371 print "keystore? "; m.validateTask.keystore 372 if not m.validateTask.valid then 373 ' Still invalid, run it again 374 RunSetupServerDialog(m.validateTask.serverurl) 375 else 376 ' Valid url, trigger the content load 377 m.top.serverurl = m.validateTask.serverurl 378 ' And save it for next time 379 RegWrite("ServerURL", m.validateTask.serverurl) 380 m.keystoreTask.has_keystore = m.validateTask.keystore 381 end if 382 end sub 383 384 385 ' ****************** 386 ' Keystore functions 387 ' ****************** 388 389 ' GetKeystoreValue retrieves a string from the keystore server 390 ' It calls the callback when it is done (or has failed) 391 ' The callback needs to call ResetKeystoreTask to clear the 392 ' done field. 393 sub GetKeystoreValue(key as string, callback as string) 394 m.keystoreTask.serverurl = m.top.serverurl 395 m.keystoreTask.key = key 396 m.keystoreTask.value = "" 397 m.keystoreTask.command = "get" 398 if callback <> "" 399 m.keystoreTask.ObserveField("done", callback) 400 end if 401 m.keystoreTask.control = "run" 402 end sub 403 404 ' SetKeystoreValue sets a key to a string on the keystore server 405 ' It calls the callback when it is done (or has failed) 406 ' The callback needs to call ResetKeystoreTask to clear the 407 ' done field. 408 sub SetKeystoreValue(key as string, value as string, callback as string) 409 m.keystoreTask.serverurl = m.top.serverurl 410 m.keystoreTask.key = key 411 m.keystoreTask.value = value 412 m.keystoreTask.command = "set" 413 if callback <> "" 414 m.keystoreTask.ObserveField("done", callback) 415 end if 416 m.keystoreTask.control = "run" 417 end sub 418 419 ' ResetKeystoreTask clears the observer and sets done back to false 420 sub ResetKeystoreTask() 421 m.keystoreTask.UNObserveField("done") 422 m.keystoreTask.done = false 423 end sub