// Package viewmap converts a UniFi Protect live view into an rtsp-streamer // layout. It is shared by the `views import` CLI command and the daemon's // periodic re-sync so both produce identical layouts. package viewmap import ( "fmt" "strings" "github.com/lwoodard/rtsp-streamer/internal/config" "github.com/lwoodard/rtsp-streamer/internal/protect" ) // GridForSlots picks a near-square grid that fits n slots (capped at 16). func GridForSlots(n int) (cols, rows int) { switch { case n <= 1: return 1, 1 case n <= 2: return 2, 1 case n <= 4: return 2, 2 case n <= 6: return 3, 2 case n <= 9: return 3, 3 case n <= 12: return 4, 3 default: return 4, 4 } } // LayoutFromView maps a Protect live view to a layout. Cameras are placed // row-major, one per slot (the first camera of a cycling slot). Sizing uses a // slot-count grid for now; asymmetric Protect presets need the `layout` int // mapping. The returned layout is linked back to the view (ProtectView) so the // daemon can re-sync it. Returns warnings for unmappable cameras. func LayoutFromView(v protect.LiveView, idToName map[string]string) (config.Layout, []string) { cols, rows := GridForSlots(len(v.Slots)) var tiles []config.Tile var warns []string for i, slot := range v.Slots { if i >= cols*rows { warns = append(warns, fmt.Sprintf("more slots than the %dx%d grid holds; extra dropped", cols, rows)) break } if len(slot.Cameras) == 0 { continue } name := idToName[slot.Cameras[0]] if name == "" { warns = append(warns, fmt.Sprintf("slot %d camera %s not in config", i, slot.Cameras[0])) continue } tiles = append(tiles, config.Tile{Camera: name, Col: i % cols, Row: i / cols, ColSpan: 1, RowSpan: 1}) } return config.Layout{ Name: Sanitize(v.Name), Grid: fmt.Sprintf("%dx%d", cols, rows), Tiles: tiles, ProtectView: v.Name, }, warns } // Sanitize turns a view name into a layout name (lowercase, dashed). func Sanitize(s string) string { s = strings.ToLower(strings.TrimSpace(s)) s = strings.Map(func(r rune) rune { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9': return r default: return '-' } }, s) for strings.Contains(s, "--") { s = strings.ReplaceAll(s, "--", "-") } if s = strings.Trim(s, "-"); s == "" { return "imported" } return s }