feat(call): integrate call handling and WebSocket support
- 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:
60
internal/handler/call_handler.go
Normal file
60
internal/handler/call_handler.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"carrot_bbs/internal/pkg/response"
|
||||
"carrot_bbs/internal/service"
|
||||
)
|
||||
|
||||
// CallHandler 通话记录处理器
|
||||
type CallHandler struct {
|
||||
callService service.CallService
|
||||
}
|
||||
|
||||
// NewCallHandler 创建通话处理器
|
||||
func NewCallHandler(callService service.CallService) *CallHandler {
|
||||
return &CallHandler{callService: callService}
|
||||
}
|
||||
|
||||
// GetCallHistory 获取通话记录
|
||||
// GET /api/v1/calls/history?page=1&page_size=20
|
||||
func (h *CallHandler) GetCallHistory(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Unauthorized(c, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 50 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
calls, total, err := h.callService.GetCallHistory(c.Request.Context(), userID.(string), page, pageSize)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, "failed to get call history")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"items": calls,
|
||||
"total": total,
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
|
||||
// GetICEServers 获取 ICE 服务器配置
|
||||
// GET /api/v1/calls/ice-servers
|
||||
func (h *CallHandler) GetICEServers(c *gin.Context) {
|
||||
servers := h.callService.GetICEServers()
|
||||
response.Success(c, gin.H{
|
||||
"ice_servers": servers,
|
||||
})
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user