mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 11:23:43 +08:00
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/coder/websocket"
|
|
|
|
"vocat/internal/store"
|
|
)
|
|
|
|
const maxCallMediaMessage = 16 << 10
|
|
|
|
// handleCallMedia upgrades an authenticated same-origin request to a binary
|
|
// PCM bridge. Each WebSocket message contains little-endian signed 16-bit,
|
|
// 8 kHz, mono samples. RTP and codec details remain inside the IMS provider.
|
|
func (s *Server) handleCallMedia(w http.ResponseWriter, r *http.Request, config store.Device) bool {
|
|
if !requireMethod(w, r, http.MethodGet) {
|
|
return true
|
|
}
|
|
if s.callTransport(config.ID) != "vowifi" {
|
|
writeError(w, http.StatusNotImplemented, "call_media_unavailable", "browser audio is only available for an active VoWiFi IMS call")
|
|
return true
|
|
}
|
|
callID := strings.TrimSpace(r.URL.Query().Get("call_id"))
|
|
if callID == "" || len(callID) > 256 {
|
|
writeError(w, http.StatusBadRequest, "invalid_call_id", "call_id is required")
|
|
return true
|
|
}
|
|
controller, ok := s.vowifi.(VoWiFiCallMediaController)
|
|
if !ok {
|
|
writeError(w, http.StatusNotImplemented, "call_media_unavailable", "the active IMS session does not expose RTP media")
|
|
return true
|
|
}
|
|
media, err := controller.CallMedia(r.Context(), config.ID, callID)
|
|
if err != nil {
|
|
writeError(w, http.StatusConflict, "call_media_unavailable", err.Error())
|
|
return true
|
|
}
|
|
connection, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
|
CompressionMode: websocket.CompressionDisabled,
|
|
})
|
|
if err != nil {
|
|
return true
|
|
}
|
|
connection.SetReadLimit(maxCallMediaMessage)
|
|
ctx, cancel := context.WithCancel(r.Context())
|
|
defer cancel()
|
|
defer connection.Close(websocket.StatusNormalClosure, "call media closed")
|
|
|
|
downlink := make(chan error, 1)
|
|
go func() {
|
|
defer cancel()
|
|
for {
|
|
samples, readErr := media.ReadPCM(ctx)
|
|
if readErr != nil {
|
|
downlink <- readErr
|
|
return
|
|
}
|
|
payload := make([]byte, len(samples)*2)
|
|
for index, sample := range samples {
|
|
binary.LittleEndian.PutUint16(payload[index*2:], uint16(sample))
|
|
}
|
|
if writeErr := connection.Write(ctx, websocket.MessageBinary, payload); writeErr != nil {
|
|
downlink <- writeErr
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case err := <-downlink:
|
|
if !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
|
|
s.logger.Debug("call media downlink closed", "device_id", config.ID, "call_id", callID, "error", err)
|
|
}
|
|
return true
|
|
default:
|
|
}
|
|
messageType, payload, readErr := connection.Read(ctx)
|
|
if readErr != nil {
|
|
return true
|
|
}
|
|
if messageType != websocket.MessageBinary || len(payload) == 0 || len(payload)%2 != 0 {
|
|
continue
|
|
}
|
|
samples := make([]int16, len(payload)/2)
|
|
for index := range samples {
|
|
samples[index] = int16(binary.LittleEndian.Uint16(payload[index*2:]))
|
|
}
|
|
if err := media.WritePCM(samples); err != nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|