|
- package aoiprxkit
-
- import (
- "bufio"
- "context"
- "crypto/sha1"
- "encoding/base64"
- "encoding/json"
- "io"
- "net"
- "net/http"
- "strings"
- "time"
- )
-
- type MeterServer struct {
- meter *LiveMeter
- srv *http.Server
- }
-
- func NewMeterServer(listenAddress string, meter *LiveMeter) *MeterServer {
- if meter == nil {
- meter = NewLiveMeter()
- }
- ms := &MeterServer{meter: meter}
- mux := http.NewServeMux()
- mux.HandleFunc("/", ms.handleIndex)
- mux.HandleFunc("/healthz", ms.handleHealth)
- mux.HandleFunc("/api/meter", ms.handleSnapshot)
- mux.HandleFunc("/ws/live", ms.handleWS)
- ms.srv = &http.Server{
- Addr: listenAddress,
- Handler: mux,
- ReadHeaderTimeout: 5 * time.Second,
- ReadTimeout: 10 * time.Second,
- WriteTimeout: 30 * time.Second,
- IdleTimeout: 60 * time.Second,
- }
- return ms
- }
-
- func (m *MeterServer) Meter() *LiveMeter { return m.meter }
-
- func (m *MeterServer) Start() error {
- go func() {
- _ = m.srv.ListenAndServe()
- }()
- return nil
- }
-
- func (m *MeterServer) Shutdown(ctx context.Context) error {
- return m.srv.Shutdown(ctx)
- }
-
- func (m *MeterServer) handleHealth(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
- }
-
- func (m *MeterServer) handleSnapshot(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(m.meter.Snapshot())
- }
-
- func (m *MeterServer) handleIndex(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- _, _ = io.WriteString(w, meterIndexHTML)
- }
-
- func (m *MeterServer) handleWS(w http.ResponseWriter, r *http.Request) {
- if !headerContainsToken(r.Header, "Connection", "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
- http.Error(w, "upgrade required", http.StatusUpgradeRequired)
- return
- }
- key := strings.TrimSpace(r.Header.Get("Sec-WebSocket-Key"))
- if key == "" {
- http.Error(w, "missing Sec-WebSocket-Key", http.StatusBadRequest)
- return
- }
- hj, ok := w.(http.Hijacker)
- if !ok {
- http.Error(w, "hijacking not supported", http.StatusInternalServerError)
- return
- }
- conn, rw, err := hj.Hijack()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- accept := computeWebSocketAccept(key)
- _, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
- _, _ = rw.WriteString("Upgrade: websocket\r\n")
- _, _ = rw.WriteString("Connection: Upgrade\r\n")
- _, _ = rw.WriteString("Sec-WebSocket-Accept: " + accept + "\r\n\r\n")
- _ = rw.Flush()
-
- ch, unsubscribe := m.meter.Subscribe()
- defer unsubscribe()
- defer conn.Close()
-
- _ = conn.SetDeadline(time.Time{})
- for snap := range ch {
- payload, err := json.Marshal(snap)
- if err != nil {
- return
- }
- if err := writeWebSocketTextFrame(conn, payload); err != nil {
- return
- }
- }
- }
-
- func headerContainsToken(h http.Header, key, token string) bool {
- for _, v := range h.Values(key) {
- parts := strings.Split(v, ",")
- for _, part := range parts {
- if strings.EqualFold(strings.TrimSpace(part), token) {
- return true
- }
- }
- }
- return false
- }
-
- func computeWebSocketAccept(key string) string {
- const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- sum := sha1.Sum([]byte(key + magic))
- return base64.StdEncoding.EncodeToString(sum[:])
- }
-
- func writeWebSocketTextFrame(conn net.Conn, payload []byte) error {
- bw := bufio.NewWriter(conn)
- header := []byte{0x81}
- switch {
- case len(payload) < 126:
- header = append(header, byte(len(payload)))
- case len(payload) <= 65535:
- header = append(header, 126, byte(len(payload)>>8), byte(len(payload)))
- default:
- header = append(header, 127,
- byte(uint64(len(payload))>>56), byte(uint64(len(payload))>>48), byte(uint64(len(payload))>>40), byte(uint64(len(payload))>>32),
- byte(uint64(len(payload))>>24), byte(uint64(len(payload))>>16), byte(uint64(len(payload))>>8), byte(uint64(len(payload))),
- )
- }
- if _, err := bw.Write(header); err != nil {
- return err
- }
- if _, err := bw.Write(payload); err != nil {
- return err
- }
- return bw.Flush()
- }
-
- const meterIndexHTML = `<!doctype html>
- <html>
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>aoiprxkit meter</title>
- <style>
- body { font-family: system-ui, sans-serif; margin: 20px; background: #111; color: #eee; }
- .meta { margin-bottom: 16px; color: #bbb; }
- .row { margin: 12px 0; }
- .label { margin-bottom: 4px; }
- .bar { width: 100%; height: 22px; background: #222; border-radius: 6px; overflow: hidden; }
- .fill { height: 100%; background: linear-gradient(90deg, #2ecc71, #f1c40f, #e74c3c); width: 0%; }
- .nums { font-size: 12px; color: #bbb; margin-top: 4px; }
- </style>
- </head>
- <body>
- <h1>aoiprxkit live meter</h1>
- <div id="meta" class="meta">waiting for frames…</div>
- <div id="meters"></div>
- <script>
- const meta = document.getElementById('meta');
- const root = document.getElementById('meters');
- const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/live');
- ws.onmessage = (ev) => {
- const snap = JSON.parse(ev.data);
- meta.textContent = (snap.source || 'unknown') + ' · ' + snap.sampleRateHz + ' Hz · ' + snap.channels + ' ch · ' + snap.updatedAt;
- root.innerHTML = '';
- (snap.meters || []).forEach((m, idx) => {
- const row = document.createElement('div');
- row.className = 'row';
- const label = document.createElement('div');
- label.className = 'label';
- label.textContent = 'Channel ' + (idx + 1);
- const bar = document.createElement('div');
- bar.className = 'bar';
- const fill = document.createElement('div');
- fill.className = 'fill';
- fill.style.width = Math.max(0, Math.min(100, m.peak * 100)).toFixed(1) + '%';
- bar.appendChild(fill);
- const nums = document.createElement('div');
- nums.className = 'nums';
- nums.textContent = 'RMS ' + m.rms.toFixed(4) + ' · Peak ' + m.peak.toFixed(4) + ' · Latest ' + m.latest.toFixed(4);
- row.appendChild(label);
- row.appendChild(bar);
- row.appendChild(nums);
- root.appendChild(row);
- });
- };
- </script>
- </body>
- </html>`
|