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.
155 lines
4.0 KiB
Go
155 lines
4.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/livekit/protocol/livekit"
|
|
"go.uber.org/zap"
|
|
|
|
"with_you/internal/config"
|
|
"with_you/internal/pkg/response"
|
|
"with_you/internal/service"
|
|
)
|
|
|
|
// LiveKitHandler LiveKit 令牌和 Webhook 处理器
|
|
type LiveKitHandler struct {
|
|
liveKitService service.LiveKitService
|
|
callService service.CallService
|
|
config *config.LiveKitConfig
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewLiveKitHandler 创建 LiveKit 处理器
|
|
func NewLiveKitHandler(
|
|
liveKitService service.LiveKitService,
|
|
callService service.CallService,
|
|
cfg *config.Config,
|
|
logger *zap.Logger,
|
|
) *LiveKitHandler {
|
|
return &LiveKitHandler{
|
|
liveKitService: liveKitService,
|
|
callService: callService,
|
|
config: &cfg.LiveKit,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetToken 生成 LiveKit 访问令牌
|
|
// GET /api/v1/calls/token?room=<callID>
|
|
func (h *LiveKitHandler) GetToken(c *gin.Context) {
|
|
if !h.config.Enabled {
|
|
response.Forbidden(c, "livekit is not enabled")
|
|
return
|
|
}
|
|
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
response.Unauthorized(c, "unauthorized")
|
|
return
|
|
}
|
|
|
|
room := c.Query("room")
|
|
if room == "" {
|
|
response.BadRequest(c, "room parameter is required")
|
|
return
|
|
}
|
|
|
|
// 验证用户是该通话的参与者
|
|
activeCall := h.callService.GetActiveCall(c.Request.Context(), room)
|
|
if activeCall == nil {
|
|
response.NotFound(c, "call not found or already ended")
|
|
return
|
|
}
|
|
|
|
isParticipant := false
|
|
for _, p := range activeCall.Participants {
|
|
if p.UserID == userID.(string) {
|
|
isParticipant = true
|
|
break
|
|
}
|
|
}
|
|
if !isParticipant && activeCall.CallerID != userID.(string) && activeCall.CalleeID != userID.(string) {
|
|
response.Forbidden(c, "not a participant of this call")
|
|
return
|
|
}
|
|
|
|
canPublish := true
|
|
canSubscribe := true
|
|
if cp := c.Query("can_publish"); cp != "" {
|
|
canPublish, _ = strconv.ParseBool(cp)
|
|
}
|
|
if cs := c.Query("can_subscribe"); cs != "" {
|
|
canSubscribe, _ = strconv.ParseBool(cs)
|
|
}
|
|
|
|
token, err := h.liveKitService.GenerateToken(room, userID.(string), canPublish, canSubscribe)
|
|
if err != nil {
|
|
h.logger.Error("failed to generate livekit token", zap.Error(err))
|
|
response.InternalServerError(c, "failed to generate token")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"token": token,
|
|
"url": h.config.URL,
|
|
})
|
|
}
|
|
|
|
// HandleWebhook 处理 LiveKit Webhook 回调
|
|
// POST /api/v1/calls/webhook
|
|
func (h *LiveKitHandler) HandleWebhook(c *gin.Context) {
|
|
if !h.config.Enabled {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "livekit is not enabled"})
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
h.logger.Error("failed to read webhook body", zap.Error(err))
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"})
|
|
return
|
|
}
|
|
|
|
authHeader := c.GetHeader("Authorization")
|
|
event, err := h.liveKitService.ProcessWebhook(c.Request.Context(), body, authHeader)
|
|
if err != nil {
|
|
h.logger.Error("failed to process webhook", zap.Error(err))
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid webhook"})
|
|
return
|
|
}
|
|
|
|
// 根据 webhook 事件类型处理
|
|
switch event.Event {
|
|
case "room_finished":
|
|
h.handleRoomFinished(c.Request.Context(), event)
|
|
case "participant_left":
|
|
h.logger.Info("participant left room",
|
|
zap.String("room", event.Room.GetName()),
|
|
zap.String("identity", event.Participant.GetIdentity()),
|
|
)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// handleRoomFinished 处理 room_finished 事件,作为通话历史持久化的安全兜底
|
|
func (h *LiveKitHandler) handleRoomFinished(ctx context.Context, event *livekit.WebhookEvent) {
|
|
roomName := event.Room.GetName()
|
|
if roomName == "" {
|
|
return
|
|
}
|
|
|
|
// 尝试结束通话(如果尚未结束)
|
|
// callService.End 会检查调用是否仍然活跃,如果已经结束则返回 nil
|
|
_, err := h.callService.End(ctx, roomName, "", "room_finished")
|
|
if err != nil {
|
|
h.logger.Debug("room_finished: call end returned error (may already be ended)",
|
|
zap.String("room", roomName),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
} |