Add snapshot-first Frigate camera wall
This commit is contained in:
@@ -4,6 +4,8 @@ visionLabs Roku is a private Roku app scaffold for viewing home security cameras
|
||||
|
||||
The current starter path is Frigate-backed, with Roku reading a static inventory of cameras that are managed by Frigate instead of embedding raw camera credentials in the app.
|
||||
|
||||
The first playback strategy is `snapshotFirst`, which lets the Roku app render a useful camera wall before the direct live stream endpoint shape is finalized.
|
||||
|
||||
## Product direction
|
||||
|
||||
- Brand family follows the user's existing `Labs` naming pattern.
|
||||
@@ -36,6 +38,7 @@ Roku is not a great target for raw RTSP feeds. With Frigate in the middle, the c
|
||||
|
||||
1. Set the real Frigate base URL in `config/cameras.json`
|
||||
2. Map the four starter cameras in Frigate using the same IDs as the Roku config
|
||||
3. Load config into the UI
|
||||
4. Render a static multi-camera grid
|
||||
5. Add edit mode and persistence strategy
|
||||
3. Verify that Frigate snapshots load at `/api/<camera>/latest.jpg`
|
||||
4. Render the snapshot wall on Roku
|
||||
5. Choose the direct live stream path for full playback
|
||||
6. Add edit mode and persistence strategy
|
||||
|
||||
@@ -2,6 +2,12 @@ sub init()
|
||||
m.top.setFocus(true)
|
||||
m.backend = m.top.findNode("backend")
|
||||
m.status = m.top.findNode("status")
|
||||
m.cameraPosters = [
|
||||
m.top.findNode("poster1"),
|
||||
m.top.findNode("poster2"),
|
||||
m.top.findNode("poster3"),
|
||||
m.top.findNode("poster4")
|
||||
]
|
||||
m.cameraNodes = [
|
||||
m.top.findNode("cam1"),
|
||||
m.top.findNode("cam2"),
|
||||
@@ -31,9 +37,10 @@ sub loadConfig()
|
||||
end if
|
||||
|
||||
cameras = config.cameras
|
||||
m.status.text = cameras.count().toStr() + " cameras loaded from static config"
|
||||
m.status.text = cameras.count().toStr() + " cameras loaded, snapshot-first prototype"
|
||||
|
||||
for i = 0 to m.cameraNodes.count() - 1
|
||||
poster = m.cameraPosters[i]
|
||||
node = m.cameraNodes[i]
|
||||
|
||||
if i < cameras.count() then
|
||||
@@ -48,9 +55,36 @@ sub loadConfig()
|
||||
label = label + chr(10) + "Frigate ID: " + camera.frigateCamera
|
||||
end if
|
||||
|
||||
poster.uri = buildSnapshotUrl(config, camera)
|
||||
node.text = label
|
||||
else
|
||||
poster.uri = ""
|
||||
node.text = "Empty slot"
|
||||
end if
|
||||
end for
|
||||
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
|
||||
|
||||
@@ -36,35 +36,59 @@
|
||||
text="Loading camera config..."
|
||||
font="font:MediumSystemFont"
|
||||
color="0x8B949EFF" />
|
||||
<Label
|
||||
id="cam1"
|
||||
<Poster
|
||||
id="poster1"
|
||||
translation="[80, 360]"
|
||||
width="760"
|
||||
height="120"
|
||||
height="300"
|
||||
loadDisplayMode="scaleToFit" />
|
||||
<Label
|
||||
id="cam1"
|
||||
translation="[80, 675]"
|
||||
width="760"
|
||||
height="70"
|
||||
text="Cam slot 1"
|
||||
font="font:MediumSystemFont"
|
||||
color="0xFFFFFFFF" />
|
||||
<Label
|
||||
id="cam2"
|
||||
<Poster
|
||||
id="poster2"
|
||||
translation="[980, 360]"
|
||||
width="760"
|
||||
height="120"
|
||||
height="300"
|
||||
loadDisplayMode="scaleToFit" />
|
||||
<Label
|
||||
id="cam2"
|
||||
translation="[980, 675]"
|
||||
width="760"
|
||||
height="70"
|
||||
text="Cam slot 2"
|
||||
font="font:MediumSystemFont"
|
||||
color="0xFFFFFFFF" />
|
||||
<Poster
|
||||
id="poster3"
|
||||
translation="[80, 760]"
|
||||
width="760"
|
||||
height="300"
|
||||
loadDisplayMode="scaleToFit" />
|
||||
<Label
|
||||
id="cam3"
|
||||
translation="[80, 580]"
|
||||
translation="[80, 1015]"
|
||||
width="760"
|
||||
height="120"
|
||||
height="50"
|
||||
text="Cam slot 3"
|
||||
font="font:MediumSystemFont"
|
||||
color="0xFFFFFFFF" />
|
||||
<Poster
|
||||
id="poster4"
|
||||
translation="[980, 760]"
|
||||
width="760"
|
||||
height="300"
|
||||
loadDisplayMode="scaleToFit" />
|
||||
<Label
|
||||
id="cam4"
|
||||
translation="[980, 580]"
|
||||
translation="[980, 1015]"
|
||||
width="760"
|
||||
height="120"
|
||||
height="50"
|
||||
text="Cam slot 4"
|
||||
font="font:MediumSystemFont"
|
||||
color="0xFFFFFFFF" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"backend": {
|
||||
"type": "frigate",
|
||||
"baseUrl": "https://home.labs.casa/ccab4aaf_frigate",
|
||||
"mode": "snapshotFirst",
|
||||
"notes": "Keep raw camera credentials in Frigate or go2rtc, not in the Roku repo."
|
||||
},
|
||||
"layout": {
|
||||
@@ -16,6 +17,7 @@
|
||||
"name": "Cam 195",
|
||||
"frigateCamera": "cam195",
|
||||
"host": "192.168.0.195",
|
||||
"snapshotPath": "/api/cam195/latest.jpg",
|
||||
"onvif": true
|
||||
},
|
||||
{
|
||||
@@ -23,6 +25,7 @@
|
||||
"name": "Cam 196",
|
||||
"frigateCamera": "cam196",
|
||||
"host": "192.168.0.196",
|
||||
"snapshotPath": "/api/cam196/latest.jpg",
|
||||
"onvif": true
|
||||
},
|
||||
{
|
||||
@@ -30,6 +33,7 @@
|
||||
"name": "Cam 201",
|
||||
"frigateCamera": "cam201",
|
||||
"host": "192.168.0.201",
|
||||
"snapshotPath": "/api/cam201/latest.jpg",
|
||||
"onvif": true
|
||||
},
|
||||
{
|
||||
@@ -37,6 +41,7 @@
|
||||
"name": "Cam 228",
|
||||
"frigateCamera": "cam228",
|
||||
"host": "192.168.0.228",
|
||||
"snapshotPath": "/api/cam228/latest.jpg",
|
||||
"onvif": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -27,11 +27,13 @@ Keep camera credentials inside:
|
||||
|
||||
1. Set the real Frigate base URL in `config/cameras.json`
|
||||
2. Confirm the Frigate camera IDs match `cam195`, `cam196`, `cam201`, and `cam228`
|
||||
3. Decide which Frigate live endpoint format the Roku app should consume first
|
||||
4. Add snapshot and live tile loading in the SceneGraph UI
|
||||
3. Verify that each camera snapshot is reachable at `/api/<camera>/latest.jpg`
|
||||
4. Add snapshot tile loading in the SceneGraph UI
|
||||
5. Decide which direct live endpoint format the Roku app should consume after snapshots are working
|
||||
|
||||
## Why this shape works
|
||||
|
||||
- Roku does not want to manage ONVIF directly
|
||||
- Roku is happier when Frigate handles the camera-side protocol mess
|
||||
- This keeps the app focused on layout, browsing, and playback
|
||||
- Snapshot-first is the lowest-risk way to prove the app shape before live playback is finalized
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
- Static packaged Frigate-backed camera config
|
||||
- Simple Roku SceneGraph shell
|
||||
- One screen with a fixed camera grid
|
||||
- One screen with a fixed snapshot-first camera grid
|
||||
- Frigate-managed live feeds in a Roku-friendly format
|
||||
|
||||
## Phase 2
|
||||
|
||||
- Config loader and camera tile rendering
|
||||
- Config loader and snapshot tile rendering
|
||||
- Snapshot fallback if a stream is unavailable
|
||||
- Focus movement and remote navigation
|
||||
|
||||
@@ -30,4 +30,5 @@
|
||||
- Roku does not directly fit raw RTSP workflows well
|
||||
- Frigate should own camera auth and camera-side protocol details
|
||||
- HLS is the safest first-class playback target
|
||||
- Snapshot-first is the safest prototype path while live endpoint details are still being verified
|
||||
- Packaged JSON is fine for V1 but not enough for runtime editing
|
||||
|
||||
Reference in New Issue
Block a user