Files
Strix/pkg/camdb/streams.go
T
eduard256 27117900eb Rewrite Strix from scratch as single binary
Complete architecture rewrite following go2rtc patterns:
- pkg/ for pure logic (camdb, tester, probe, generate)
- internal/ for application glue with Init() modules
- Single HTTP server on :4567 with all endpoints
- zerolog with password masking and memory ring buffer
- Environment-based config only (no YAML files)

API endpoints: /api/search, /api/streams, /api/test,
/api/probe, /api/generate, /api/health, /api/log

Dependencies: go2rtc v1.9.14, go-sqlite3, miekg/dns, zerolog
2026-03-25 10:38:46 +00:00

203 lines
4.4 KiB
Go

package camdb
import (
"database/sql"
"encoding/base64"
"errors"
"fmt"
"strconv"
"strings"
)
var defaultPorts = map[string]int{
"rtsp": 554, "rtsps": 322, "http": 80, "https": 443,
"rtmp": 1935, "mms": 554, "rtp": 5004,
}
type StreamParams struct {
IDs string
IP string
User string
Pass string
Channel int
Ports map[int]bool // nil = no filter
}
type raw struct {
url, protocol string
port int
}
// BuildStreams resolves IDs to full stream URLs with credentials and placeholders substituted
func BuildStreams(db *sql.DB, p *StreamParams) ([]string, error) {
var raws []raw
for _, id := range strings.Split(p.IDs, ",") {
id = strings.TrimSpace(id)
if id == "" {
continue
}
var rows *sql.Rows
var err error
switch {
case strings.HasPrefix(id, "b:"):
brandID := id[2:]
rows, err = db.Query(
"SELECT url, protocol, port FROM streams WHERE brand_id = ?", brandID,
)
case strings.HasPrefix(id, "m:"):
parts := strings.SplitN(id[2:], ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("camdb: invalid model id: %s", id)
}
rows, err = db.Query(
`SELECT s.url, s.protocol, s.port
FROM stream_models sm
JOIN streams s ON s.id = sm.stream_id
WHERE s.brand_id = ? AND sm.model = ?`,
parts[0], parts[1],
)
case strings.HasPrefix(id, "p:"):
presetID := id[2:]
rows, err = db.Query(
"SELECT url, protocol, port FROM preset_streams WHERE preset_id = ?", presetID,
)
default:
return nil, fmt.Errorf("camdb: unknown id prefix: %s", id)
}
if err != nil {
return nil, err
}
found := false
for rows.Next() {
var r raw
if err = rows.Scan(&r.url, &r.protocol, &r.port); err != nil {
rows.Close()
return nil, err
}
raws = append(raws, r)
found = true
}
rows.Close()
if !found {
return nil, fmt.Errorf("camdb: not found: %s", id)
}
}
// build full URLs, deduplicate
seen := map[string]bool{}
var streams []string
for _, r := range raws {
if len(streams) >= 20000 {
break
}
port := r.port
if port == 0 {
if p, ok := defaultPorts[r.protocol]; ok {
port = p
} else {
port = 80
}
}
if p.Ports != nil && !p.Ports[port] {
continue
}
u := buildURL(r.protocol, r.url, p.IP, port, p.User, p.Pass, p.Channel)
if seen[u] {
continue
}
seen[u] = true
streams = append(streams, u)
}
return streams, nil
}
// ValidateID checks if id format is valid
func ValidateID(id string) error {
switch {
case strings.HasPrefix(id, "b:"):
if len(id) < 3 {
return errors.New("camdb: empty brand id")
}
case strings.HasPrefix(id, "m:"):
if strings.Count(id, ":") < 2 {
return fmt.Errorf("camdb: invalid model id: %s", id)
}
case strings.HasPrefix(id, "p:"):
if len(id) < 3 {
return errors.New("camdb: empty preset id")
}
default:
return fmt.Errorf("camdb: unknown prefix: %s", id)
}
return nil
}
// internals
func buildURL(protocol, path, ip string, port int, user, pass string, channel int) string {
path = replacePlaceholders(path, ip, port, user, pass, channel)
var auth string
if user != "" {
auth = user + ":" + pass + "@"
}
host := ip
if p, ok := defaultPorts[protocol]; !ok || p != port {
host = ip + ":" + strconv.Itoa(port)
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return protocol + "://" + auth + host + path
}
func replacePlaceholders(s, ip string, port int, user, pass string, channel int) string {
auth := ""
if user != "" && pass != "" {
auth = base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
}
pairs := []string{
"[CHANNEL]", strconv.Itoa(channel),
"[channel]", strconv.Itoa(channel),
"{CHANNEL}", strconv.Itoa(channel),
"{channel}", strconv.Itoa(channel),
"[CHANNEL+1]", strconv.Itoa(channel + 1),
"[channel+1]", strconv.Itoa(channel + 1),
"{CHANNEL+1}", strconv.Itoa(channel + 1),
"{channel+1}", strconv.Itoa(channel + 1),
"[USERNAME]", user, "[username]", user,
"[USER]", user, "[user]", user,
"[PASSWORD]", pass, "[password]", pass,
"[PASWORD]", pass, "[pasword]", pass,
"[PASS]", pass, "[pass]", pass,
"[PWD]", pass, "[pwd]", pass,
"[WIDTH]", "640", "[width]", "640",
"[HEIGHT]", "480", "[height]", "480",
"[IP]", ip, "[ip]", ip,
"[PORT]", strconv.Itoa(port), "[port]", strconv.Itoa(port),
"[AUTH]", auth, "[auth]", auth,
"[TOKEN]", "", "[token]", "",
}
r := strings.NewReplacer(pairs...)
return r.Replace(s)
}