sub init() m.top.setFocus(true) m.backend = m.top.findNode("backend") m.status = m.top.findNode("status") m.help = m.top.findNode("help") m.refreshTimer = m.top.findNode("refreshTimer") m.liveVideo = m.top.findNode("liveVideo") m.cameraPosters = [ m.top.findNode("poster1"), m.top.findNode("poster2"), m.top.findNode("poster3"), m.top.findNode("poster4") ] m.cameraFrames = [ m.top.findNode("frame1"), m.top.findNode("frame2"), m.top.findNode("frame3"), m.top.findNode("frame4") ] m.cameraNodes = [ m.top.findNode("cam1"), m.top.findNode("cam2"), m.top.findNode("cam3"), m.top.findNode("cam4") ] m.refreshToken = 0 m.selectedIndex = 0 m.isLiveView = false loadConfig() end sub sub loadConfig() raw = ReadAsciiFile("pkg:/config/cameras.json") config = ParseJson(raw) if type(config) <> "roAssociativeArray" then m.status.text = "Unable to parse cameras.json" return end if m.config = config if config.doesExist("backend") and config.backend.doesExist("baseUrl") then m.backend.text = "Direct Frigate" end if if not config.doesExist("cameras") then m.status.text = "No cameras found in config" return end if if config.doesExist("backend") and config.backend.doesExist("refreshSeconds") then m.refreshTimer.duration = config.backend.refreshSeconds end if m.refreshTimer.observeField("fire", "onRefreshTimerFire") m.refreshTimer.control = "start" renderCameras() updateSelection() end sub sub renderCameras() if m.config = invalid or not m.config.doesExist("cameras") then return end if cameras = m.config.cameras m.status.text = "Refreshing every " + m.refreshTimer.duration.toStr() + "s" for i = 0 to m.cameraNodes.count() - 1 poster = m.cameraPosters[i] node = m.cameraNodes[i] if i < cameras.count() then camera = cameras[i] poster.uri = addCacheBust(buildSnapshotUrl(m.config, camera)) node.text = camera.name else poster.uri = "" node.text = "Empty slot" end if end for end sub sub onRefreshTimerFire() if m.isLiveView then return end if m.refreshToken = m.refreshToken + 1 renderCameras() end sub function onKeyEvent(key as String, press as Boolean) as Boolean if not press then return false end if lower = LCase(key) if m.isLiveView then if lower = "back" then closeLiveView() return true end if return false end if if lower = "right" and m.selectedIndex mod 2 = 0 and m.selectedIndex + 1 < m.config.cameras.count() then m.selectedIndex = m.selectedIndex + 1 updateSelection() return true else if lower = "left" and m.selectedIndex mod 2 = 1 then m.selectedIndex = m.selectedIndex - 1 updateSelection() return true else if lower = "down" and m.selectedIndex < 2 and m.selectedIndex + 2 < m.config.cameras.count() then m.selectedIndex = m.selectedIndex + 2 updateSelection() return true else if lower = "up" and m.selectedIndex >= 2 then m.selectedIndex = m.selectedIndex - 2 updateSelection() return true else if lower = "ok" then openLiveView() return true end if return false end function sub updateSelection() for i = 0 to m.cameraFrames.count() - 1 if i = m.selectedIndex then m.cameraFrames[i].color = "0x2EA043FF" else m.cameraFrames[i].color = "0x1F262DFF" end if end for end sub sub openLiveView() if m.config = invalid or not m.config.doesExist("cameras") or m.selectedIndex >= m.config.cameras.count() then return end if camera = m.config.cameras[m.selectedIndex] liveUrl = buildLiveUrl(m.config, camera) content = CreateObject("roSGNode", "ContentNode") content.url = liveUrl content.streamformat = "hls" content.title = camera.name m.liveVideo.content = content m.liveVideo.visible = true m.liveVideo.control = "play" m.isLiveView = true m.status.text = "Live view: " + camera.name m.help.text = "Press Back to return to the snapshot wall." end sub sub closeLiveView() m.liveVideo.control = "stop" m.liveVideo.visible = false m.isLiveView = false m.status.text = "Refreshing every " + m.refreshTimer.duration.toStr() + "s" m.help.text = "Use arrows to choose a camera. Press OK for live view. Press Back to return." m.top.setFocus(true) end sub function buildSnapshotUrl(config as Object, camera as Object) as String if not config.doesExist("backend") or not config.backend.doesExist("baseUrl") then return "" end if baseUrl = config.backend.baseUrl path = "" if camera.doesExist("snapshotPath") then path = camera.snapshotPath else if camera.doesExist("frigateCamera") then path = "/api/" + camera.frigateCamera + "/latest.jpg" else return "" end if if right(baseUrl, 1) = "/" and left(path, 1) = "/" then return left(baseUrl, len(baseUrl) - 1) + path else if right(baseUrl, 1) <> "/" and left(path, 1) <> "/" then return baseUrl + "/" + path end if return baseUrl + path end function function addCacheBust(url as String) as String token = m.refreshToken.toStr() if instr(1, url, "?") > 0 then return url + "&cb=" + token end if return url + "?cb=" + token end function function buildLiveUrl(config as Object, camera as Object) as String if not config.doesExist("live") or not config.live.doesExist("baseUrl") or not camera.doesExist("frigateCamera") then return "" end if baseUrl = config.live.baseUrl path = "/api/stream.m3u8?src=" + camera.frigateCamera if right(baseUrl, 1) = "/" and left(path, 1) = "/" then return left(baseUrl, len(baseUrl) - 1) + path else if right(baseUrl, 1) <> "/" and left(path, 1) <> "/" then return baseUrl + "/" + path end if return baseUrl + path end function