feat(call): integrate LiveKit for voice and video calling
All checks were successful
Build Backend / build (push) Successful in 3m47s
Build Backend / build-docker (push) Successful in 5m9s

Replace the manual WebRTC signaling implementation with LiveKit SFU. This includes:
- Adding LiveKit service, handler, and configuration.
- Updating Docker Compose to include LiveKit server, Redis, and PostgreSQL.
- Refactoring `CallService` and `WSHandler` to support LiveKit room readiness instead of raw SDP/ICE relaying.
- Adding new API endpoints for LiveKit token generation and webhooks.
- Removing deprecated WebRTC configuration and manual signaling DTOs.
This commit is contained in:
2026-06-01 13:41:02 +08:00
parent 9356cb6876
commit 14114db68d
22 changed files with 731 additions and 452 deletions

View File

@@ -212,7 +212,7 @@ func (h *WSHandler) readPump(conn *websocket.Conn, client *ws.Client) {
conn.Close()
}()
conn.SetReadLimit(256 * 1024) // 256KB (SDP/ICE candidates can be large)
conn.SetReadLimit(256 * 1024) // 256KB
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
zap.L().Debug("WebSocket pong received",
@@ -383,10 +383,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
h.handleCallReject(ctx, client, msg.Payload)
case "call_busy":
h.handleCallBusy(ctx, client, msg.Payload)
case "call_sdp":
h.handleCallSDP(ctx, client, msg.Payload)
case "call_ice":
h.handleCallICE(ctx, client, msg.Payload)
case "call_ready":
h.handleCallReady(ctx, client, msg.Payload)
case "call_end":
h.handleCallEnd(ctx, client, msg.Payload)
case "call_mute":
@@ -717,43 +715,18 @@ func (h *WSHandler) handleCallBusy(ctx context.Context, client *ws.Client, paylo
}
}
// handleCallSDP 转发 SDP
func (h *WSHandler) handleCallSDP(ctx context.Context, client *ws.Client, payload json.RawMessage) {
// handleCallReady 处理通话就绪LiveKit 房间连接成功)
func (h *WSHandler) handleCallReady(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
SDPType string `json:"sdp_type"`
SDP json.RawMessage `json:"sdp"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" || req.SDPType == "" {
h.publisher.SendError(client, "invalid_params", "call_id and sdp_type are required")
return
}
signalPayload, _ := json.Marshal(map[string]any{
"sdp_type": req.SDPType,
"sdp": json.RawMessage(req.SDP),
})
if err := h.callService.RelaySignal(ctx, req.CallID, client.UserID, "call_sdp", signalPayload); err != nil {
h.publisher.SendError(client, "call_sdp_failed", err.Error())
}
}
// handleCallICE 转发 ICE candidate
func (h *WSHandler) handleCallICE(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
Candidate json.RawMessage `json:"candidate"`
CallID string `json:"call_id"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.publisher.SendError(client, "invalid_params", "call_id is required")
return
}
signalPayload, _ := json.Marshal(map[string]any{
"candidate": json.RawMessage(req.Candidate),
})
if err := h.callService.RelaySignal(ctx, req.CallID, client.UserID, "call_ice", signalPayload); err != nil {
h.publisher.SendError(client, "call_ice_failed", err.Error())
if err := h.callService.Ready(ctx, req.CallID, client.UserID); err != nil {
h.publisher.SendError(client, "call_ready_failed", err.Error())
}
}