Files
Strix/internal/api/api.go
T
eduard256 74b4b61198 Add frontend pages, move static to www/, add resolution to tester
Frontend:
- index.html: probe device, navigate to standard/homekit by type
- standard.html: camera config, model search with multi-select
- create.html: stream URL list, custom URL input, create test session
- homekit.html: HomeKit device info, contact links, fallback to standard

Backend:
- Move static files to www/ package with embed (go2rtc pattern)
- Add initStatic() in api with FileServer
- Add width/height to test results from H264 SPS parsing
- Contribute links to gostrix.github.io with auto-filled params
2026-03-26 10:18:40 +00:00

119 lines
2.8 KiB
Go

package api
import (
"encoding/json"
"net"
"net/http"
"time"
"github.com/eduard256/strix/internal/app"
"github.com/rs/zerolog"
)
var log zerolog.Logger
var Handler http.Handler
func Init() {
listen := app.Env("STRIX_LISTEN", ":4567")
log = app.GetLogger("api")
HandleFunc("api", apiHandler)
HandleFunc("api/health", apiHealth)
HandleFunc("api/log", apiLog)
initStatic()
Handler = middlewareCORS(http.DefaultServeMux)
if log.Trace().Enabled() {
Handler = middlewareLog(Handler)
}
go listen_serve("tcp", listen)
}
func listen_serve(network, address string) {
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", address).Msg("[api] listen")
server := http.Server{
Handler: Handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Minute, // long for test sessions
}
if err = server.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}
// HandleFunc registers handler on http.DefaultServeMux with "/" prefix
func HandleFunc(pattern string, handler http.HandlerFunc) {
if len(pattern) == 0 || pattern[0] != '/' {
pattern = "/" + pattern
}
log.Trace().Str("path", pattern).Msg("[api] register")
http.HandleFunc(pattern, handler)
}
// ResponseJSON writes JSON response with Content-Type header
func ResponseJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
// Error logs error and writes HTTP error response
func Error(w http.ResponseWriter, err error, code int) {
log.Error().Err(err).Caller(1).Send()
http.Error(w, err.Error(), code)
}
func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
return
}
next.ServeHTTP(w, r)
})
}
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
ResponseJSON(w, app.Info)
}
func apiHealth(w http.ResponseWriter, r *http.Request) {
ResponseJSON(w, map[string]any{
"version": app.Version,
"uptime": time.Since(app.StartTime).Truncate(time.Second).String(),
})
}
func apiLog(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/jsonlines")
app.MemoryLog.WriteTo(w)
case "DELETE":
app.MemoryLog.Reset()
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}