diff --git a/internal/frigate/frigate.go b/internal/frigate/frigate.go index d5eda6a..58cda49 100644 --- a/internal/frigate/frigate.go +++ b/internal/frigate/frigate.go @@ -1,6 +1,7 @@ package frigate import ( + "encoding/json" "io" "net/http" "sync" @@ -99,10 +100,17 @@ func apiConfig(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(resp.Body) + // Frigate /api/config/raw returns JSON-encoded string, unquote it + config := string(body) + var unquoted string + if err := json.Unmarshal(body, &unquoted); err == nil { + config = unquoted + } + api.ResponseJSON(w, map[string]any{ "connected": true, "url": url, - "config": string(body), + "config": config, }) } diff --git a/internal/go2rtc/go2rtc.go b/internal/go2rtc/go2rtc.go new file mode 100644 index 0000000..75a3abb --- /dev/null +++ b/internal/go2rtc/go2rtc.go @@ -0,0 +1,108 @@ +package go2rtc + +import ( + "io" + "net/http" + "sync" + "time" + + "github.com/eduard256/strix/internal/api" + "github.com/eduard256/strix/internal/app" + "github.com/rs/zerolog" +) + +var log zerolog.Logger + +var go2rtcURL string +var go2rtcOnce sync.Once + +var candidates = []string{ + "http://localhost:1984", + "http://localhost:11984", +} + +const probeTimeout = 50 * time.Millisecond +const requestTimeout = 5 * time.Second + +func Init() { + log = app.GetLogger("go2rtc") + + if url := app.Env("STRIX_GO2RTC_URL", ""); url != "" { + go2rtcURL = url + log.Info().Str("url", go2rtcURL).Msg("[go2rtc] using STRIX_GO2RTC_URL") + } + + api.HandleFunc("api/go2rtc/streams", apiStreams) +} + +func getURL() string { + if go2rtcURL != "" { + return go2rtcURL + } + + go2rtcOnce.Do(func() { + go2rtcURL = probe() + if go2rtcURL != "" { + log.Info().Str("url", go2rtcURL).Msg("[go2rtc] discovered") + } + }) + + return go2rtcURL +} + +func probe() string { + client := &http.Client{Timeout: probeTimeout} + + for _, url := range candidates { + resp, err := client.Get(url + "/api") + if err != nil { + continue + } + resp.Body.Close() + if resp.StatusCode == 200 { + return url + } + } + + return "" +} + +// PUT /api/go2rtc/streams?name=...&src=... -- proxy to go2rtc +func apiStreams(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + url := getURL() + if url == "" { + api.ResponseJSON(w, map[string]any{"success": false, "error": "go2rtc not found"}) + return + } + + // forward query params as-is + target := url + "/api/streams?" + r.URL.RawQuery + + client := &http.Client{Timeout: requestTimeout} + req, err := http.NewRequest("PUT", target, nil) + if err != nil { + api.ResponseJSON(w, map[string]any{"success": false, "error": err.Error()}) + return + } + + resp, err := client.Do(req) + if err != nil { + api.ResponseJSON(w, map[string]any{"success": false, "error": err.Error()}) + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + w.Header().Set("Content-Type", "application/json") + if resp.StatusCode == 200 { + api.ResponseJSON(w, map[string]any{"success": true}) + } else { + api.ResponseJSON(w, map[string]any{"success": false, "error": string(body)}) + } +} diff --git a/main.go b/main.go index eb097da..0b17abe 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "github.com/eduard256/strix/internal/app" "github.com/eduard256/strix/internal/frigate" "github.com/eduard256/strix/internal/generate" + "github.com/eduard256/strix/internal/go2rtc" "github.com/eduard256/strix/internal/probe" "github.com/eduard256/strix/internal/search" "github.com/eduard256/strix/internal/test" @@ -26,6 +27,7 @@ func main() { {"probe", probe.Init}, {"generate", generate.Init}, {"frigate", frigate.Init}, + {"go2rtc", go2rtc.Init}, } for _, m := range modules { diff --git a/pkg/generate/config.go b/pkg/generate/config.go index 1296a87..eb7e623 100644 --- a/pkg/generate/config.go +++ b/pkg/generate/config.go @@ -26,9 +26,17 @@ func Generate(req *Request) (*Response, error) { } } - if strings.TrimSpace(req.ExistingConfig) == "" { + existing := strings.TrimSpace(req.ExistingConfig) + + // generate from scratch if no config or config has no go2rtc streams section + if existing == "" || !strings.Contains(existing, "go2rtc:") { config := newConfig(info, req) - return &Response{Config: config, Diff: fullDiff(config)}, nil + lines := strings.Count(config, "\n") + 1 + added := make([]int, lines) + for i := range added { + added[i] = i + 1 + } + return &Response{Config: config, Added: added}, nil } return addToConfig(req.ExistingConfig, info, req) @@ -124,7 +132,7 @@ func newConfig(info *cameraInfo, req *Request) string { var b strings.Builder b.WriteString("mqtt:\n enabled: false\n\n") - b.WriteString("record:\n enabled: true\n retain:\n days: 7\n mode: motion\n\n") + b.WriteString("record:\n enabled: true\n\n") b.WriteString("go2rtc:\n streams:\n") writeStreamLines(&b, info) @@ -132,7 +140,7 @@ func newConfig(info *cameraInfo, req *Request) string { b.WriteString("cameras:\n") writeCameraBlock(&b, info, req) - b.WriteString("version: 0.18-0\n") + b.WriteString("version: 0.17-0\n") return b.String() } diff --git a/pkg/generate/diff.go b/pkg/generate/diff.go deleted file mode 100644 index 0750d4e..0000000 --- a/pkg/generate/diff.go +++ /dev/null @@ -1,36 +0,0 @@ -package generate - -import "strings" - -func fullDiff(config string) []DiffLine { - lines := strings.Split(config, "\n") - diff := make([]DiffLine, len(lines)) - for i, line := range lines { - diff[i] = DiffLine{Line: i + 1, Text: line, Type: "added"} - } - return diff -} - -func diffWithContext(lines []string, added map[int]bool, ctx int) []DiffLine { - visible := make(map[int]bool) - for idx := range added { - for c := -ctx; c <= ctx; c++ { - if j := idx + c; j >= 0 && j < len(lines) { - visible[j] = true - } - } - } - - var diff []DiffLine - for i, line := range lines { - if !visible[i] { - continue - } - t := "context" - if added[i] { - t = "added" - } - diff = append(diff, DiffLine{Line: i + 1, Text: line, Type: t}) - } - return diff -} diff --git a/pkg/generate/insert.go b/pkg/generate/insert.go index d82f6e7..d3f35d6 100644 --- a/pkg/generate/insert.go +++ b/pkg/generate/insert.go @@ -65,8 +65,15 @@ func addToConfig(existing string, info *cameraInfo, req *Request) (*Response, er result = append(result, rest[split:]...) config := strings.Join(result, "\n") - diff := diffWithContext(result, added, 3) - return &Response{Config: config, Diff: diff}, nil + + addedLines := make([]int, 0, len(added)) + for i := range result { + if added[i] { + addedLines = append(addedLines, i+1) + } + } + + return &Response{Config: config, Added: addedLines}, nil } func dedup(info *cameraInfo, cams, streams map[string]bool) *cameraInfo { diff --git a/pkg/generate/models.go b/pkg/generate/models.go index f597a9c..ee61e41 100644 --- a/pkg/generate/models.go +++ b/pkg/generate/models.go @@ -106,12 +106,6 @@ type UIConfig struct { } type Response struct { - Config string `json:"config"` - Diff []DiffLine `json:"diff"` -} - -type DiffLine struct { - Line int `json:"line"` - Text string `json:"text"` - Type string `json:"type"` // context, added, removed + Config string `json:"config"` + Added []int `json:"added"` // 1-based line numbers of added lines } diff --git a/www/config.html b/www/config.html new file mode 100644 index 0000000..e5a71af --- /dev/null +++ b/www/config.html @@ -0,0 +1,820 @@ + + +
+ + + +Copy these URLs to use in your NVR, media player, or streaming software.
+ + + + + + + +