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