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

@@ -80,6 +80,9 @@ type Player struct {
Profile string `yaml:"profile"`
// ExtraArgs are appended verbatim to every mpv invocation.
ExtraArgs []string `yaml:"extra_args,omitempty"`
// MaxFPS caps the rendered frame rate (mpv --vf=fps). 0 = uncapped. Trims
// render/scale load; the bigger decode lever is using substreams.
MaxFPS int `yaml:"max_fps,omitempty"`
// RestartBackoffSeconds is how long to wait before relaunching a stream
// that exited or stalled.
RestartBackoffSeconds int `yaml:"restart_backoff_seconds,omitempty"`
@@ -92,12 +95,62 @@ type Camera struct {
ID string `yaml:"id,omitempty"`
// Name is the human label and the key layouts reference. Must be unique.
Name string `yaml:"name"`
// RTSP is the fully-resolved stream URL.
RTSP string `yaml:"rtsp"`
// RTSP is a single fully-resolved stream URL. Kept for backward
// compatibility and manual entries; Streams takes precedence when present.
RTSP string `yaml:"rtsp,omitempty"`
// Streams maps a quality ("high"|"medium"|"low") to its stream URL, so a
// tile can choose per-tile which to pull. Populated by discovery.
Streams map[string]string `yaml:"streams,omitempty"`
// Disabled hides the camera from selection without deleting it.
Disabled bool `yaml:"disabled,omitempty"`
}
// Qualities in preference order, high to low.
var Qualities = []string{"high", "medium", "low"}
// StreamURL returns the URL for the requested quality, falling back sensibly:
// the exact quality, then any lower quality, then any stream at all, then the
// legacy single RTSP field.
func (c Camera) StreamURL(quality string) string {
if len(c.Streams) > 0 {
if quality != "" {
if u := c.Streams[quality]; u != "" {
return u
}
}
// Fall back down the preference list from the requested quality.
start := 0
for i, q := range Qualities {
if q == quality {
start = i
break
}
}
for _, q := range Qualities[start:] {
if u := c.Streams[q]; u != "" {
return u
}
}
for _, q := range Qualities {
if u := c.Streams[q]; u != "" {
return u
}
}
}
return c.RTSP
}
// AvailableQualities lists the qualities this camera actually has, high to low.
func (c Camera) AvailableQualities() []string {
var out []string
for _, q := range Qualities {
if c.Streams[q] != "" {
out = append(out, q)
}
}
return out
}
// MaxTiles caps how many simultaneous streams a layout may show. Decoding
// more than this on a Pi 4 is impractical even with substreams.
const MaxTiles = 16
@@ -125,6 +178,9 @@ type Tile struct {
Row int `yaml:"row"`
ColSpan int `yaml:"colspan,omitempty"` // defaults to 1
RowSpan int `yaml:"rowspan,omitempty"` // defaults to 1
// Quality selects which stream to pull for this tile: "high"|"medium"|
// "low". Empty means the camera's best available (see Camera.StreamURL).
Quality string `yaml:"quality,omitempty"`
}
// Span returns the tile's spans with zero values normalized to 1.