Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

206 рядки
6.2KB

  1. package aoiprxkit
  2. import (
  3. "bufio"
  4. "context"
  5. "crypto/sha1"
  6. "encoding/base64"
  7. "encoding/json"
  8. "io"
  9. "net"
  10. "net/http"
  11. "strings"
  12. "time"
  13. )
  14. type MeterServer struct {
  15. meter *LiveMeter
  16. srv *http.Server
  17. }
  18. func NewMeterServer(listenAddress string, meter *LiveMeter) *MeterServer {
  19. if meter == nil {
  20. meter = NewLiveMeter()
  21. }
  22. ms := &MeterServer{meter: meter}
  23. mux := http.NewServeMux()
  24. mux.HandleFunc("/", ms.handleIndex)
  25. mux.HandleFunc("/healthz", ms.handleHealth)
  26. mux.HandleFunc("/api/meter", ms.handleSnapshot)
  27. mux.HandleFunc("/ws/live", ms.handleWS)
  28. ms.srv = &http.Server{
  29. Addr: listenAddress,
  30. Handler: mux,
  31. ReadHeaderTimeout: 5 * time.Second,
  32. ReadTimeout: 10 * time.Second,
  33. WriteTimeout: 30 * time.Second,
  34. IdleTimeout: 60 * time.Second,
  35. }
  36. return ms
  37. }
  38. func (m *MeterServer) Meter() *LiveMeter { return m.meter }
  39. func (m *MeterServer) Start() error {
  40. go func() {
  41. _ = m.srv.ListenAndServe()
  42. }()
  43. return nil
  44. }
  45. func (m *MeterServer) Shutdown(ctx context.Context) error {
  46. return m.srv.Shutdown(ctx)
  47. }
  48. func (m *MeterServer) handleHealth(w http.ResponseWriter, _ *http.Request) {
  49. w.Header().Set("Content-Type", "application/json")
  50. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
  51. }
  52. func (m *MeterServer) handleSnapshot(w http.ResponseWriter, _ *http.Request) {
  53. w.Header().Set("Content-Type", "application/json")
  54. _ = json.NewEncoder(w).Encode(m.meter.Snapshot())
  55. }
  56. func (m *MeterServer) handleIndex(w http.ResponseWriter, _ *http.Request) {
  57. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  58. _, _ = io.WriteString(w, meterIndexHTML)
  59. }
  60. func (m *MeterServer) handleWS(w http.ResponseWriter, r *http.Request) {
  61. if !headerContainsToken(r.Header, "Connection", "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
  62. http.Error(w, "upgrade required", http.StatusUpgradeRequired)
  63. return
  64. }
  65. key := strings.TrimSpace(r.Header.Get("Sec-WebSocket-Key"))
  66. if key == "" {
  67. http.Error(w, "missing Sec-WebSocket-Key", http.StatusBadRequest)
  68. return
  69. }
  70. hj, ok := w.(http.Hijacker)
  71. if !ok {
  72. http.Error(w, "hijacking not supported", http.StatusInternalServerError)
  73. return
  74. }
  75. conn, rw, err := hj.Hijack()
  76. if err != nil {
  77. http.Error(w, err.Error(), http.StatusInternalServerError)
  78. return
  79. }
  80. accept := computeWebSocketAccept(key)
  81. _, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
  82. _, _ = rw.WriteString("Upgrade: websocket\r\n")
  83. _, _ = rw.WriteString("Connection: Upgrade\r\n")
  84. _, _ = rw.WriteString("Sec-WebSocket-Accept: " + accept + "\r\n\r\n")
  85. _ = rw.Flush()
  86. ch, unsubscribe := m.meter.Subscribe()
  87. defer unsubscribe()
  88. defer conn.Close()
  89. _ = conn.SetDeadline(time.Time{})
  90. for snap := range ch {
  91. payload, err := json.Marshal(snap)
  92. if err != nil {
  93. return
  94. }
  95. if err := writeWebSocketTextFrame(conn, payload); err != nil {
  96. return
  97. }
  98. }
  99. }
  100. func headerContainsToken(h http.Header, key, token string) bool {
  101. for _, v := range h.Values(key) {
  102. parts := strings.Split(v, ",")
  103. for _, part := range parts {
  104. if strings.EqualFold(strings.TrimSpace(part), token) {
  105. return true
  106. }
  107. }
  108. }
  109. return false
  110. }
  111. func computeWebSocketAccept(key string) string {
  112. const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  113. sum := sha1.Sum([]byte(key + magic))
  114. return base64.StdEncoding.EncodeToString(sum[:])
  115. }
  116. func writeWebSocketTextFrame(conn net.Conn, payload []byte) error {
  117. bw := bufio.NewWriter(conn)
  118. header := []byte{0x81}
  119. switch {
  120. case len(payload) < 126:
  121. header = append(header, byte(len(payload)))
  122. case len(payload) <= 65535:
  123. header = append(header, 126, byte(len(payload)>>8), byte(len(payload)))
  124. default:
  125. header = append(header, 127,
  126. byte(uint64(len(payload))>>56), byte(uint64(len(payload))>>48), byte(uint64(len(payload))>>40), byte(uint64(len(payload))>>32),
  127. byte(uint64(len(payload))>>24), byte(uint64(len(payload))>>16), byte(uint64(len(payload))>>8), byte(uint64(len(payload))),
  128. )
  129. }
  130. if _, err := bw.Write(header); err != nil {
  131. return err
  132. }
  133. if _, err := bw.Write(payload); err != nil {
  134. return err
  135. }
  136. return bw.Flush()
  137. }
  138. const meterIndexHTML = `<!doctype html>
  139. <html>
  140. <head>
  141. <meta charset="utf-8" />
  142. <meta name="viewport" content="width=device-width, initial-scale=1" />
  143. <title>aoiprxkit meter</title>
  144. <style>
  145. body { font-family: system-ui, sans-serif; margin: 20px; background: #111; color: #eee; }
  146. .meta { margin-bottom: 16px; color: #bbb; }
  147. .row { margin: 12px 0; }
  148. .label { margin-bottom: 4px; }
  149. .bar { width: 100%; height: 22px; background: #222; border-radius: 6px; overflow: hidden; }
  150. .fill { height: 100%; background: linear-gradient(90deg, #2ecc71, #f1c40f, #e74c3c); width: 0%; }
  151. .nums { font-size: 12px; color: #bbb; margin-top: 4px; }
  152. </style>
  153. </head>
  154. <body>
  155. <h1>aoiprxkit live meter</h1>
  156. <div id="meta" class="meta">waiting for frames…</div>
  157. <div id="meters"></div>
  158. <script>
  159. const meta = document.getElementById('meta');
  160. const root = document.getElementById('meters');
  161. const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/live');
  162. ws.onmessage = (ev) => {
  163. const snap = JSON.parse(ev.data);
  164. meta.textContent = (snap.source || 'unknown') + ' · ' + snap.sampleRateHz + ' Hz · ' + snap.channels + ' ch · ' + snap.updatedAt;
  165. root.innerHTML = '';
  166. (snap.meters || []).forEach((m, idx) => {
  167. const row = document.createElement('div');
  168. row.className = 'row';
  169. const label = document.createElement('div');
  170. label.className = 'label';
  171. label.textContent = 'Channel ' + (idx + 1);
  172. const bar = document.createElement('div');
  173. bar.className = 'bar';
  174. const fill = document.createElement('div');
  175. fill.className = 'fill';
  176. fill.style.width = Math.max(0, Math.min(100, m.peak * 100)).toFixed(1) + '%';
  177. bar.appendChild(fill);
  178. const nums = document.createElement('div');
  179. nums.className = 'nums';
  180. nums.textContent = 'RMS ' + m.rms.toFixed(4) + ' · Peak ' + m.peak.toFixed(4) + ' · Latest ' + m.latest.toFixed(4);
  181. row.appendChild(label);
  182. row.appendChild(bar);
  183. row.appendChild(nums);
  184. root.appendChild(row);
  185. });
  186. };
  187. </script>
  188. </body>
  189. </html>`