feat(call): integrate call handling and WebSocket support
All checks were successful
Build Backend / build (push) Successful in 13m26s
Build Backend / build-docker (push) Successful in 1m22s

- Added CallHandler and related services for managing call sessions and participants.
- Enhanced WSHandler to support call signaling, including invite, answer, reject, and end functionalities.
- Updated router to include call-related routes for history and ICE server retrieval.
- Introduced WebRTC configuration in the application settings for call management.
- Refactored wire generation to include CallService and CallRepository for improved dependency injection.
This commit is contained in:
lafay
2026-03-27 01:54:34 +08:00
parent 9ecb29225a
commit 7e6a65d29d
16 changed files with 1002 additions and 7 deletions

View File

@@ -36,6 +36,7 @@ type WSHandler struct {
chatService service.ChatService
groupService service.GroupService
jwtService *service.JWTService
callService service.CallService
clientSeq uint64
}
@@ -45,12 +46,14 @@ func NewWSHandler(
chatService service.ChatService,
groupService service.GroupService,
jwtService *service.JWTService,
callService service.CallService,
) *WSHandler {
return &WSHandler{
wsHub: wsHub,
chatService: chatService,
groupService: groupService,
jwtService: jwtService,
callService: callService,
}
}
@@ -151,7 +154,7 @@ func (h *WSHandler) readPump(conn *websocket.Conn, client *ws.Client) {
conn.Close()
}()
conn.SetReadLimit(64 * 1024) // 64KB
conn.SetReadLimit(256 * 1024) // 256KB (SDP/ICE candidates can be large)
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
zap.L().Debug("WebSocket pong received",
@@ -260,6 +263,23 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
h.handleRead(ctx, client, msg.Payload)
case "recall":
h.handleRecall(ctx, client, msg.Payload)
// 通话信令
case "call_invite":
h.handleCallInvite(ctx, client, msg.Payload)
case "call_answer":
h.handleCallAnswer(ctx, client, msg.Payload)
case "call_reject":
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_end":
h.handleCallEnd(ctx, client, msg.Payload)
case "call_mute":
h.handleCallMute(ctx, client, msg.Payload)
default:
h.wsHub.SendError(client, "unknown_type", fmt.Sprintf("unknown message type: %s", msg.Type))
}
@@ -421,3 +441,199 @@ func (h *WSHandler) handleRecall(ctx context.Context, client *ws.Client, payload
}
const defaultUserBufferSize = 128
// ==================== 通话信令处理 ====================
// handleCallInvite 处理呼叫邀请
func (h *WSHandler) handleCallInvite(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CalleeID string `json:"callee_id"`
ConversationID string `json:"conversation_id"`
}
if err := json.Unmarshal(payload, &req); err != nil {
h.wsHub.SendError(client, "parse_error", "invalid call_invite format")
return
}
if req.CalleeID == "" || req.ConversationID == "" {
h.wsHub.SendError(client, "invalid_params", "callee_id and conversation_id are required")
return
}
call, err := h.callService.Invite(ctx, client.UserID, req.CalleeID, req.ConversationID)
if err != nil {
zap.L().Warn("Failed to invite call",
zap.String("caller_id", client.UserID),
zap.String("callee_id", req.CalleeID),
zap.Error(err),
)
h.wsHub.SendError(client, "call_invite_failed", err.Error())
return
}
resp := ws.ResponseMessage{
EventID: h.wsHub.NextID(),
Type: "call_invited",
TS: time.Now().UnixMilli(),
Payload: map[string]interface{}{
"call_id": call.ID,
"conversation_id": req.ConversationID,
"callee_id": req.CalleeID,
},
}
data, _ := json.Marshal(resp)
select {
case <-client.Quit:
case client.Send <- data:
}
}
// handleCallAnswer 处理接听
func (h *WSHandler) handleCallAnswer(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.wsHub.SendError(client, "invalid_params", "call_id is required")
return
}
call, err := h.callService.Accept(ctx, req.CallID, client.UserID)
if err != nil {
h.wsHub.SendError(client, "call_accept_failed", err.Error())
return
}
resp := ws.ResponseMessage{
EventID: h.wsHub.NextID(),
Type: "call_accepted",
TS: time.Now().UnixMilli(),
Payload: map[string]interface{}{
"call_id": call.ID,
"started_at": call.StartedAt,
},
}
data, _ := json.Marshal(resp)
select {
case <-client.Quit:
case client.Send <- data:
}
}
// handleCallReject 处理拒绝
func (h *WSHandler) handleCallReject(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.wsHub.SendError(client, "invalid_params", "call_id is required")
return
}
if err := h.callService.Reject(ctx, req.CallID, client.UserID); err != nil {
h.wsHub.SendError(client, "call_reject_failed", err.Error())
}
}
// handleCallBusy 处理忙线
func (h *WSHandler) handleCallBusy(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.wsHub.SendError(client, "invalid_params", "call_id is required")
return
}
if err := h.callService.Busy(ctx, req.CallID, client.UserID); err != nil {
h.wsHub.SendError(client, "call_busy_failed", err.Error())
}
}
// handleCallSDP 转发 SDP
func (h *WSHandler) handleCallSDP(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.wsHub.SendError(client, "invalid_params", "call_id and sdp_type are required")
return
}
signalPayload, _ := json.Marshal(map[string]interface{}{
"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.wsHub.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"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.wsHub.SendError(client, "invalid_params", "call_id is required")
return
}
signalPayload, _ := json.Marshal(map[string]interface{}{
"candidate": json.RawMessage(req.Candidate),
})
if err := h.callService.RelaySignal(ctx, req.CallID, client.UserID, "call_ice", signalPayload); err != nil {
h.wsHub.SendError(client, "call_ice_failed", err.Error())
}
}
// handleCallEnd 处理结束通话
func (h *WSHandler) handleCallEnd(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
Reason string `json:"reason"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.wsHub.SendError(client, "invalid_params", "call_id is required")
return
}
call, err := h.callService.End(ctx, req.CallID, client.UserID, req.Reason)
if err != nil {
h.wsHub.SendError(client, "call_end_failed", err.Error())
return
}
resp := ws.ResponseMessage{
EventID: h.wsHub.NextID(),
Type: "call_ended",
TS: time.Now().UnixMilli(),
Payload: map[string]interface{}{
"call_id": call.ID,
"duration": call.Duration,
},
}
data, _ := json.Marshal(resp)
select {
case <-client.Quit:
case client.Send <- data:
}
}
// handleCallMute 处理静音
func (h *WSHandler) handleCallMute(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
Muted bool `json:"muted"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.wsHub.SendError(client, "invalid_params", "call_id is required")
return
}
if err := h.callService.SetMuted(ctx, req.CallID, client.UserID, req.Muted); err != nil {
h.wsHub.SendError(client, "call_mute_failed", err.Error())
}
}