8ce89bec75
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.
192 lines
4.3 KiB
Go
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)
|
|
}
|