Per-tile stream quality (high/low), fps cap, latency flags

Cameras carry multiple stream URLs (streams: high/medium/low); each
tile picks one via Quality (auto/high/low). Small tiles can pull the
low substream to cut Pi decode load. discover --enable-rtsp takes a
comma list (high,low), enables each channel, records all enabled ones.
TUI: q cycles a tile quality (shown in the tile); discover enables
high+low. Player: --framedrop + low-delay demuxer flags; player.max_fps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Woodard
2026-07-01 21:55:25 -05:00
parent 780396076d
commit 8d95c944bd
10 changed files with 287 additions and 70 deletions

View File

@@ -7,6 +7,7 @@ package tui
import (
"context"
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -95,8 +96,8 @@ type savedMsg struct {
reloaded bool
}
// discoverCmd fetches cameras. When enable is non-empty (e.g. "high"), it also
// turns on RTSP in Protect for any camera that lacks an enabled channel.
// discoverCmd fetches cameras. `enable` is a comma-separated list of qualities
// (e.g. "high,low") to ensure RTSP-enabled in Protect and record per camera.
func (m *model) discoverCmd(enable string) tea.Cmd {
cfg := m.cfg
return func() tea.Msg {
@@ -119,10 +120,25 @@ func (m *model) discoverCmd(enable string) tea.Cmd {
if err != nil {
return discoverMsg{err: err}
}
var want []string
for _, q := range strings.Split(enable, ",") {
if q = strings.TrimSpace(strings.ToLower(q)); q != "" {
want = append(want, q)
}
}
enabled := 0
if enable != "" {
names, _ := cl.EnableMissing(ctx, cams, enable)
enabled = len(names)
for i := range cams {
cam := &cams[i]
for _, q := range want {
target := cam.ChannelByPreference(q)
if target == nil || (target.RTSPEnabled && target.RTSPAlias != "") {
continue
}
if alias, err := cl.EnableRTSP(ctx, cam.ID, target.ID); err == nil {
target.RTSPEnabled, target.RTSPAlias = true, alias
enabled++
}
}
}
return discoverMsg{cams: cams, enabled: enabled}
}
@@ -161,28 +177,35 @@ func (m *model) handleDiscover(msg discoverMsg) (tea.Model, tea.Cmd) {
}
cl, _ := protect.New(m.cfg.Controller.Host, m.cfg.Controller.RTSPPort, m.cfg.Controller.VerifyTLS)
added, updated, skipped := 0, 0, 0
for _, cam := range msg.cams {
ch := cam.BestEnabledChannel()
if ch == nil {
for i := range msg.cams {
cam := &msg.cams[i]
streams := map[string]string{}
for _, ch := range cam.Channels {
if ch.RTSPEnabled && ch.RTSPAlias != "" {
streams[strings.ToLower(ch.Name)] = cl.StreamURL(ch.RTSPAlias)
}
}
if len(streams) == 0 {
skipped++
continue
}
url := cl.StreamURL(ch.RTSPAlias)
if ex := m.cameraByID(cam.ID); ex != nil {
ex.Name, ex.RTSP = cam.Name, url
updated++
} else if ex := m.cfg.CameraByName(cam.Name); ex != nil {
ex.ID, ex.RTSP = cam.ID, url
updated++
} else {
m.cfg.Cameras = append(m.cfg.Cameras, config.Camera{ID: cam.ID, Name: cam.Name, RTSP: url})
added++
entry := m.cameraByID(cam.ID)
if entry == nil {
entry = m.cfg.CameraByName(cam.Name)
}
if entry == nil {
m.cfg.Cameras = append(m.cfg.Cameras, config.Camera{})
entry = &m.cfg.Cameras[len(m.cfg.Cameras)-1]
added++
} else {
updated++
}
entry.ID, entry.Name, entry.Streams, entry.RTSP = cam.ID, cam.Name, streams, ""
}
m.dirty = true
enabledNote := ""
if msg.enabled > 0 {
enabledNote = fmt.Sprintf(", enabled RTSP on %d", msg.enabled)
enabledNote = fmt.Sprintf(", enabled %d channels", msg.enabled)
}
m.setStatus(fmt.Sprintf("discovered %d (%d new, %d updated, %d without RTSP%s)", len(msg.cams), added, updated, skipped, enabledNote), false)
m.screen = screenCameras
@@ -229,7 +252,7 @@ var menuItems = []string{
menuLayouts: "Layouts",
menuSetActive: "Set active layout",
menuDiscover: "Discover from UniFi Protect",
menuDiscoverEnable: "Discover + enable RTSP (high)",
menuDiscoverEnable: "Discover + enable RTSP (hi+lo)",
menuSave: "Save",
menuQuit: "Quit",
}
@@ -252,8 +275,8 @@ func (m *model) updateMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.setStatus("discovering…", false)
return m, m.discoverCmd("")
case menuDiscoverEnable:
m.setStatus("discovering and enabling RTSP…", false)
return m, m.discoverCmd("high")
m.setStatus("discovering and enabling RTSP (high+low)…", false)
return m, m.discoverCmd("high,low")
case menuSave:
return m, m.save()
case menuQuit: