Add unified port configuration system

- Unified API and WebUI on single configurable port (default: 4567)
- Added strix.yaml configuration file support (go2rtc-style format)
- Environment variable STRIX_API_LISTEN overrides config file
- Port validation and source logging
- Relative URLs in frontend for automatic port detection
- Removed separate server instances
- Cleaned up temporary files and updated .gitignore
- Updated documentation with configuration examples
This commit is contained in:
eduard256
2025-11-12 10:20:55 +03:00
parent 627409cf56
commit 3d5a4927a6
13 changed files with 242 additions and 155 deletions
+7 -1
View File
@@ -1,6 +1,7 @@
# Binaries
bin/
strix
main
*.exe
*.exe~
*.dll
@@ -37,4 +38,9 @@ Thumbs.db
# Temporary files
tmp/
temp/
temp/
*.dump
*_output.txt
# Configuration (user-specific)
strix.yaml
+45 -3
View File
@@ -39,6 +39,9 @@ make build
# Run the application
make run
# The server will start on http://localhost:4567
# Open your browser and navigate to http://localhost:4567
```
## 📡 API Endpoints
@@ -105,15 +108,54 @@ strix/
## 🛠️ Configuration
Environment variables:
Strix can be configured via `strix.yaml` file or environment variables.
### Configuration File (strix.yaml)
Create a `strix.yaml` file in the same directory as the binary:
```yaml
# API Server Configuration
api:
listen: ":4567" # Format: ":port" or "host:port"
```
Examples:
```yaml
api:
listen: ":4567" # All interfaces, port 4567 (default)
# listen: "127.0.0.1:4567" # Localhost only
# listen: ":8080" # Custom port
```
### Environment Variables
Environment variables override config file values:
```bash
STRIX_HOST=0.0.0.0 # Server host (default: 0.0.0.0)
STRIX_PORT=8080 # Server port (default: 8080)
STRIX_API_LISTEN=":4567" # Server listen address (overrides strix.yaml)
STRIX_LOG_LEVEL=info # Log level: debug, info, warn, error
STRIX_LOG_FORMAT=json # Log format: json, text
```
### Configuration Priority
1. **Environment variable** `STRIX_API_LISTEN` (highest priority)
2. **Config file** `strix.yaml`
3. **Default value** `:4567` (lowest priority)
### Quick Start with Custom Port
```bash
# Using environment variable
STRIX_API_LISTEN=":8080" ./strix
# Or using config file
cp strix.yaml.example strix.yaml
# Edit strix.yaml, then:
./strix
```
## 📊 Camera Database
The system includes a comprehensive database of camera models:
-57
View File
@@ -1,57 +0,0 @@
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 324 0 157 100 167 209 222 --:--:-- --:--:-- --:--:-- 431
100 324 0 157 100 167 89 95 0:00:01 0:00:01 --:--:-- 184
100 324 0 157 100 167 57 60 0:00:02 0:00:02 --:--:-- 117
100 1075 0 908 100 167 242 44 0:00:03 0:00:03 --:--:-- 286
100 1752 0 1585 100 167 296 31 0:00:05 0:00:05 --:--:-- 327
100 1752 0 1585 100 167 249 26 0:00:06 0:00:06 --:--:-- 255
100 1816 0 1649 100 167 244 24 0:00:06 0:00:06 --:--:-- 298
100 1816 0 1649 100 167 212 21 0:00:07 0:00:07 --:--:-- 298
100 2163 0 1996 100 167 228 19 0:00:08 0:00:08 --:--:-- 218
100 2163 0 1996 100 167 204 17 0:00:09 0:00:09 --:--:-- 93
100 2227 0 2060 100 167 191 15 0:00:11 0:00:10 0:00:01 107
100 2227 0 2060 100 167 175 14 0:00:11 0:00:11 --:--:-- 82
100 2291 0 2124 100 167 166 13 0:00:12 0:00:12 --:--:-- 95
100 2291 0 2124 100 167 154 12 0:00:13 0:00:13 --:--:-- 25
100 2291 0 2124 100 167 143 11 0:00:15 0:00:14 0:00:01 25
100 2353 0 2186 100 167 138 10 0:00:16 0:00:15 0:00:01 25
100 2353 0 2186 100 167 130 9 0:00:18 0:00:16 0:00:02 25
100 2353 0 2186 100 167 123 9 0:00:18 0:00:17 0:00:01 12
100 2353 0 2186 100 167 116 8 0:00:20 0:00:18 0:00:02 12
100 2353 0 2186 100 167 110 8 0:00:20 0:00:19 0:00:01 12
100 2353 0 2186 100 167 105 8 0:00:20 0:00:20 --:--:-- 0
100 2353 0 2186 100 167 100 7 0:00:23 0:00:21 0:00:02 0
100 2353 0 2186 100 167 96 7 0:00:23 0:00:22 0:00:01 0
100 2353 0 2186 100 167 92 7 0:00:23 0:00:23 --:--:-- 0
100 2353 0 2186 100 167 88 6 0:00:27 0:00:24 0:00:03 0
100 2353 0 2186 100 167 84 6 0:00:27 0:00:25 0:00:02 0
100 2353 0 2186 100 167 81 6 0:00:27 0:00:26 0:00:01 0
100 2353 0 2186 100 167 78 6 0:00:27 0:00:27 --:--:-- 0
100 2353 0 2186 100 167 76 5 0:00:33 0:00:28 0:00:05 0
100 2353 0 2186 100 167 73 5 0:00:33 0:00:29 0:00:04 0
100 2353 0 2186 100 167 71 5 0:00:33 0:00:30 0:00:03 0
100 2353 0 2186 100 167 68 5 0:00:33 0:00:31 0:00:02 0
100 2353 0 2186 100 167 66 5 0:00:33 0:00:32 0:00:01 0
100 2353 0 2186 100 167 64 4 0:00:41 0:00:33 0:00:08 0
100 2353 0 2186 100 167 64 4 0:00:41 0:00:33 0:00:08 0
curl: (18) transfer closed with outstanding read data remaining
event: scan_started
data: {"max_streams":5,"model":"NVR","target":"10.0.20.110","timeout":60}
event: progress
data: {"tested":0,"found":0,"remaining":959}
event: stream_found
data: {"stream":{"url":"http://admin:5f8a5b7s9m@10.0.20.110/bubble/live?ch=0\u0026stream=1","type":"BUBBLE","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":11294107,"metadata":{"content_type":"video/bubble","stream_type":"main"}}}
event: progress
data: {"tested":226,"found":1,"remaining":733}
event: stream_found
data: {"stream":{"url":"http://admin:5f8a5b7s9m@10.0.20.110/bubble/live?ch=0\u0026stream=0","type":"BUBBLE","protocol":"http","port":0,"working":true,"has_audio":false,"test_time_ms":212128072,"metadata":{"content_type":"video/bubble","stream_type":"main"}}}
event: progress
data: {"tested":232,"found":2,"remaining":727}
+48 -47
View File
@@ -14,6 +14,7 @@ import (
"github.com/eduard256/Strix/internal/config"
"github.com/eduard256/Strix/internal/utils/logger"
"github.com/eduard256/Strix/webui"
"github.com/go-chi/chi/v5"
)
const (
@@ -52,8 +53,7 @@ func main() {
log.Info("starting Strix",
slog.String("version", Version),
slog.String("go_version", os.Getenv("GO_VERSION")),
slog.String("host", cfg.Server.Host),
slog.String("port", cfg.Server.Port),
slog.String("listen", cfg.Server.Listen),
)
// Check if ffprobe is available
@@ -71,51 +71,39 @@ func main() {
// Create Web UI server
webuiServer := webui.NewServer(log)
// Create API HTTP server
// Create unified router combining API and WebUI
unifiedRouter := chi.NewRouter()
// Mount API routes at /api/v1/*
unifiedRouter.Mount("/api/v1", apiServer.GetRouter())
// Mount WebUI routes at /* (serves everything else including root)
unifiedRouter.Mount("/", webuiServer.GetRouter())
// Create unified HTTP server
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port),
Handler: apiServer,
Addr: cfg.Server.Listen,
Handler: unifiedRouter,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: 120 * time.Second,
}
// Create Web UI HTTP server
webuiHTTPServer := &http.Server{
Addr: fmt.Sprintf("%s:4567", cfg.Server.Host),
Handler: webuiServer,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: 120 * time.Second,
}
// Start API server in goroutine
// Start server in goroutine
go func() {
log.Info("API server starting",
log.Info("server starting",
slog.String("address", httpServer.Addr),
slog.String("api_version", "v1"),
)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error("API server failed", err)
os.Exit(1)
}
}()
// Start Web UI server in goroutine
go func() {
log.Info("Web UI server starting",
slog.String("address", webuiHTTPServer.Addr),
)
if err := webuiHTTPServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error("Web UI server failed", err)
log.Error("server failed", err)
os.Exit(1)
}
}()
// Print endpoints
printEndpoints(cfg.Server.Host, cfg.Server.Port)
printEndpoints(cfg.Server.Listen)
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
@@ -128,19 +116,13 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown API server
// Shutdown server
if err := httpServer.Shutdown(ctx); err != nil {
log.Error("API server shutdown failed", err)
log.Error("server shutdown failed", err)
os.Exit(1)
}
// Shutdown Web UI server
if err := webuiHTTPServer.Shutdown(ctx); err != nil {
log.Error("Web UI server shutdown failed", err)
os.Exit(1)
}
log.Info("servers stopped gracefully")
log.Info("server stopped gracefully")
}
// checkFFProbe checks if ffprobe is available
@@ -167,19 +149,38 @@ func checkFFProbe() error {
return fmt.Errorf("ffprobe not found in common locations")
}
// printEndpoints prints available API endpoints
func printEndpoints(host, port string) {
if host == "0.0.0.0" || host == "" {
host = "localhost"
// printEndpoints prints available endpoints
func printEndpoints(listen string) {
// Parse listen address to get host and port
host := "localhost"
port := "4567"
// Extract port from listen address
if len(listen) > 0 {
if listen[0] == ':' {
port = listen[1:]
} else {
// Parse host:port format
for i := len(listen) - 1; i >= 0; i-- {
if listen[i] == ':' {
port = listen[i+1:]
if i > 0 {
host = listen[:i]
if host == "0.0.0.0" || host == "" {
host = "localhost"
}
}
break
}
}
}
}
baseURL := fmt.Sprintf("http://%s:%s", host, port)
webuiURL := fmt.Sprintf("http://%s:4567", host)
fmt.Println("\n🌐 Web Interface:")
fmt.Println("────────────────────────────────────────────────")
fmt.Printf(" Open in browser: %s\n", webuiURL)
fmt.Printf(" Open in browser: %s\n", baseURL)
fmt.Println("────────────────────────────────────────────────")
fmt.Println("\n🚀 API Endpoints:")
@@ -215,4 +216,4 @@ func printEndpoints(host, port string) {
fmt.Println("\n────────────────────────────────────────────────")
fmt.Println("📚 Documentation: https://github.com/eduard256/Strix")
fmt.Println("────────────────────────────────────────────────\n")
}
}
+1
View File
@@ -23,4 +23,5 @@ require (
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+1
View File
@@ -67,5 +67,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+7 -23
View File
@@ -118,31 +118,15 @@ func (s *Server) setupRoutes() {
})
})
// API version 1 routes
s.router.Route("/api/v1", func(r chi.Router) {
// Health check
r.Get("/health", handlers.NewHealthHandler("1.0.0", s.logger).ServeHTTP)
// API routes (mounted at /api/v1 in main.go)
// Health check
s.router.Get("/health", handlers.NewHealthHandler("1.0.0", s.logger).ServeHTTP)
// Camera search
r.Post("/cameras/search", handlers.NewSearchHandler(s.searchEngine, s.logger).ServeHTTP)
// Camera search
s.router.Post("/cameras/search", handlers.NewSearchHandler(s.searchEngine, s.logger).ServeHTTP)
// Stream discovery (SSE)
r.Post("/streams/discover", handlers.NewDiscoverHandler(s.scanner, s.sseServer, s.logger).ServeHTTP)
})
// Root health check
s.router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"name":"Strix","version":"1.0.0","api":"v1"}`))
})
// 404 handler
s.router.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error":"Not found"}`))
})
// Stream discovery (SSE)
s.router.Post("/streams/discover", handlers.NewDiscoverHandler(s.scanner, s.sseServer, s.logger).ServeHTTP)
}
// ServeHTTP implements http.Handler
+104 -18
View File
@@ -1,10 +1,15 @@
package config
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
)
// Config holds application configuration
@@ -17,8 +22,7 @@ type Config struct {
// ServerConfig contains HTTP server settings
type ServerConfig struct {
Host string
Port string
Listen string // Address to listen on (e.g., ":4567" or "0.0.0.0:4567")
ReadTimeout time.Duration
WriteTimeout time.Duration
}
@@ -35,17 +39,17 @@ type DatabaseConfig struct {
// ScannerConfig contains stream scanner settings
type ScannerConfig struct {
DefaultTimeout time.Duration
MaxStreams int
ModelSearchLimit int
WorkerPoolSize int
FFProbeTimeout time.Duration
RetryAttempts int
RetryDelay time.Duration
DefaultTimeout time.Duration
MaxStreams int
ModelSearchLimit int
WorkerPoolSize int
FFProbeTimeout time.Duration
RetryAttempts int
RetryDelay time.Duration
// Validation settings
StrictValidation bool // Enable strict validation mode
MinImageSize int // Minimum bytes for valid image (JPEG/PNG)
MinVideoStreams int // Minimum video streams required
StrictValidation bool // Enable strict validation mode
MinImageSize int // Minimum bytes for valid image (JPEG/PNG)
MinVideoStreams int // Minimum video streams required
}
// LoggerConfig contains logging settings
@@ -54,14 +58,20 @@ type LoggerConfig struct {
Format string // "text" or "json"
}
// yamlConfig represents the structure of strix.yaml
type yamlConfig struct {
API struct {
Listen string `yaml:"listen"`
} `yaml:"api"`
}
// Load returns configuration with defaults
func Load() *Config {
dataPath := getEnv("STRIX_DATA_PATH", "./data")
return &Config{
cfg := &Config{
Server: ServerConfig{
Host: getEnv("STRIX_HOST", "0.0.0.0"),
Port: getEnv("STRIX_PORT", "8080"),
Listen: ":4567", // Default listen address
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
},
@@ -83,14 +93,90 @@ func Load() *Config {
RetryDelay: 500 * time.Millisecond,
// Strict validation enabled by default
StrictValidation: true,
MinImageSize: 5120, // 5KB minimum for valid images
MinVideoStreams: 1, // At least 1 video stream required
MinImageSize: 5120, // 5KB minimum for valid images
MinVideoStreams: 1, // At least 1 video stream required
},
Logger: LoggerConfig{
Level: getEnv("STRIX_LOG_LEVEL", "info"),
Format: getEnv("STRIX_LOG_FORMAT", "json"),
},
}
// Load from strix.yaml if exists
configSource := "default"
if err := loadYAML(cfg); err == nil {
configSource = "strix.yaml"
}
// Environment variable overrides everything
if envListen := os.Getenv("STRIX_API_LISTEN"); envListen != "" {
cfg.Server.Listen = envListen
configSource = "environment variable STRIX_API_LISTEN"
}
// Validate listen address
if err := validateListen(cfg.Server.Listen); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: Invalid listen address '%s': %v\n", cfg.Server.Listen, err)
fmt.Fprintf(os.Stderr, "Using default: :4567\n")
cfg.Server.Listen = ":4567"
configSource = "default (validation failed)"
}
// Log configuration source
fmt.Printf("INFO: API listen address '%s' loaded from %s\n", cfg.Server.Listen, configSource)
return cfg
}
// loadYAML attempts to load configuration from strix.yaml
func loadYAML(cfg *Config) error {
data, err := os.ReadFile("./strix.yaml")
if err != nil {
return err
}
var yamlCfg yamlConfig
if err := yaml.Unmarshal(data, &yamlCfg); err != nil {
return fmt.Errorf("failed to parse strix.yaml: %w", err)
}
// Apply yaml configuration
if yamlCfg.API.Listen != "" {
cfg.Server.Listen = yamlCfg.API.Listen
}
return nil
}
// validateListen validates the listen address format and port range
func validateListen(listen string) error {
if listen == "" {
return fmt.Errorf("listen address cannot be empty")
}
// Parse the listen address
parts := strings.Split(listen, ":")
if len(parts) < 2 {
return fmt.Errorf("invalid format, expected ':port' or 'host:port', got '%s'", listen)
}
// Get port (last part)
portStr := parts[len(parts)-1]
if portStr == "" {
return fmt.Errorf("port cannot be empty")
}
// Validate port number
port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("invalid port number '%s': %w", portStr, err)
}
if port < 1 || port > 65535 {
return fmt.Errorf("port %d out of valid range (1-65535)", port)
}
return nil
}
// SetupLogger configures the global logger
@@ -126,4 +212,4 @@ func getEnv(key, defaultValue string) string {
return value
}
return defaultValue
}
}
BIN
View File
Binary file not shown.
View File
+25
View File
@@ -0,0 +1,25 @@
# Strix Configuration Example
# Copy this file to strix.yaml and modify as needed
# API Server Configuration
api:
# Listen address in format ":port" or "host:port"
# Default: ":4567"
listen: ":4567"
# Examples:
# listen: ":4567" # Listen on all interfaces, port 4567 (default)
# listen: "0.0.0.0:4567" # Explicitly listen on all interfaces
# listen: "127.0.0.1:4567" # Listen only on localhost (secure local-only access)
# listen: ":8080" # Custom port on all interfaces
# Configuration Priority (highest to lowest):
# 1. Environment variable: STRIX_API_LISTEN
# 2. This file: strix.yaml
# 3. Default value: :4567
# Quick Start:
# 1. Copy this file: cp strix.yaml.example strix.yaml
# 2. Edit the listen address if needed
# 3. Run strix: ./strix
# 4. Or set via environment: STRIX_API_LISTEN=":8080" ./strix
+2 -3
View File
@@ -1,9 +1,8 @@
export class CameraSearchAPI {
constructor(baseURL = null) {
// Auto-detect API URL based on current host
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
const currentHost = window.location.hostname;
this.baseURL = `http://${currentHost}:8080`;
this.baseURL = '';
} else {
this.baseURL = baseURL;
}
+2 -3
View File
@@ -1,9 +1,8 @@
export class StreamDiscoveryAPI {
constructor(baseURL = null) {
// Auto-detect API URL based on current host
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
const currentHost = window.location.hostname;
this.baseURL = `http://${currentHost}:8080`;
this.baseURL = '';
} else {
this.baseURL = baseURL;
}