package main import ( "encoding/json" "log" "net/http" "strconv" "time" "github.com/gorilla/websocket" "sdr-visual-suite/internal/recorder" ) func registerWSHandlers(mux *http.ServeMux, h *hub, recMgr *recorder.Manager) { upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { origin := r.Header.Get("Origin") if origin == "" || origin == "null" { return true } return true }} mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("ws upgrade failed: %v (origin: %s)", err, r.Header.Get("Origin")) return } c := &client{conn: conn, send: make(chan []byte, 32), done: make(chan struct{})} h.add(c) defer func() { h.remove(c) _ = conn.Close() }() conn.SetReadDeadline(time.Now().Add(60 * time.Second)) conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(60 * time.Second)) return nil }) go func() { ping := time.NewTicker(30 * time.Second) defer ping.Stop() for { select { case msg, ok := <-c.send: if !ok { return } _ = conn.SetWriteDeadline(time.Now().Add(200 * time.Millisecond)) if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil { return } case <-ping.C: _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { log.Printf("ws ping error: %v", err) return } } } }() for { _, _, err := conn.ReadMessage() if err != nil { return } } }) // /ws/audio — WebSocket endpoint for continuous live-listen audio streaming. // Client connects with query params: freq, bw, mode // Server sends binary frames of PCM s16le audio at 48kHz. mux.HandleFunc("/ws/audio", func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() freq, _ := strconv.ParseFloat(q.Get("freq"), 64) bw, _ := strconv.ParseFloat(q.Get("bw"), 64) mode := q.Get("mode") if freq <= 0 { http.Error(w, "freq required", http.StatusBadRequest) return } if bw <= 0 { bw = 12000 } streamer := recMgr.StreamerRef() if streamer == nil { http.Error(w, "streamer not available", http.StatusServiceUnavailable) return } subID, ch := streamer.SubscribeAudio(freq, bw, mode) if ch == nil { http.Error(w, "no active stream for this frequency", http.StatusNotFound) return } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { streamer.UnsubscribeAudio(subID) log.Printf("ws/audio upgrade failed: %v", err) return } defer func() { streamer.UnsubscribeAudio(subID) _ = conn.Close() }() log.Printf("ws/audio: client connected freq=%.1fMHz mode=%s", freq/1e6, mode) // Send audio stream info as first text message info := map[string]any{ "type": "audio_info", "sample_rate": 48000, "channels": 1, "format": "s16le", "freq": freq, "mode": mode, } if infoBytes, err := json.Marshal(info); err == nil { _ = conn.WriteMessage(websocket.TextMessage, infoBytes) } // Read goroutine (to detect disconnect) done := make(chan struct{}) go func() { defer close(done) for { _, _, err := conn.ReadMessage() if err != nil { return } } }() ping := time.NewTicker(30 * time.Second) defer ping.Stop() for { select { case pcm, ok := <-ch: if !ok { log.Printf("ws/audio: stream ended freq=%.1fMHz", freq/1e6) return } _ = conn.SetWriteDeadline(time.Now().Add(500 * time.Millisecond)) if err := conn.WriteMessage(websocket.BinaryMessage, pcm); err != nil { log.Printf("ws/audio: write error: %v", err) return } case <-ping.C: _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } case <-done: log.Printf("ws/audio: client disconnected freq=%.1fMHz", freq/1e6) return } } }) }