Files
Strix/pkg/camdb/streams.go
T
eduard256 8ce89bec75 Always include port in URL for protocols with raw TCP dial
Bubble protocol uses net.DialTimeout with u.Host directly,
which requires explicit port. Add portRequired set to force
port in generated URLs for such protocols.
2026-04-01 17:53:00 +00:00

192 lines
4.3 KiB
Go

package camdb
import (
"database/sql"
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
)
var defaultPorts = map[string]int{
"rtsp": 554, "rtsps": 322, "http": 80, "https": 443,
"rtmp": 1935, "mms": 554, "rtp": 5004, "bubble": 80,
}
// protocols where port must always be explicit in URL (raw TCP dial without default port logic)
var portRequired = map[string]bool{
"bubble": true,
}
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
}
// 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 = url.PathEscape(user) + ":" + url.PathEscape(pass) + "@"
}
host := ip
if p, ok := defaultPorts[protocol]; (!ok || p != port) || portRequired[protocol] {
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))
}
// URL-encode credentials for safe use in query parameters
encUser := url.QueryEscape(user)
encPass := url.QueryEscape(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]", encUser, "[username]", encUser,
"[USER]", encUser, "[user]", encUser,
"[PASSWORD]", encPass, "[password]", encPass,
"[PASWORD]", encPass, "[pasword]", encPass,
"[PASS]", encPass, "[pass]", encPass,
"[PWD]", encPass, "[pwd]", encPass,
"[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)
}