package service import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "time" "with_you/internal/config" "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" "go.uber.org/zap" "google.golang.org/protobuf/encoding/protojson" ) // RoomConfig 房间配置选项 type RoomConfig struct { MaxParticipants uint32 EmptyTimeout uint32 // 秒 } // LiveKitService LiveKit 令牌生成和 Webhook 处理服务 type LiveKitService interface { GenerateToken(roomName, userID string, canPublish, canSubscribe bool, roomCfg ...RoomConfig) (string, error) ProcessWebhook(ctx context.Context, body []byte, authHeader string) (*livekit.WebhookEvent, error) } type liveKitService struct { config *config.LiveKitConfig logger *zap.Logger } // NewLiveKitService 创建 LiveKit 服务 func NewLiveKitService(cfg *config.Config, logger *zap.Logger) LiveKitService { return &liveKitService{ config: &cfg.LiveKit, logger: logger, } } // GenerateToken 为指定 room 和 user 生成 LiveKit 访问令牌 func (s *liveKitService) GenerateToken(roomName, userID string, canPublish, canSubscribe bool, roomCfg ...RoomConfig) (string, error) { if !s.config.Enabled { return "", fmt.Errorf("livekit is not enabled") } ttl := s.config.TokenTTL if ttl <= 0 { ttl = 600 // 默认 10 分钟 } at := auth.NewAccessToken(s.config.APIKey, s.config.APISecret) at.SetName(userID) at.SetIdentity(userID) at.SetValidFor(time.Duration(ttl) * time.Second) grant := &auth.VideoGrant{ RoomJoin: true, Room: roomName, CanPublish: &canPublish, CanSubscribe: &canSubscribe, CanPublishData: &canPublish, } at.SetVideoGrant(grant) token, err := at.ToJWT() if err != nil { s.logger.Error("failed to generate livekit token", zap.Error(err)) return "", fmt.Errorf("generate token: %w", err) } return token, nil } // ProcessWebhook 验证并解析 LiveKit Webhook 事件 func (s *liveKitService) ProcessWebhook(ctx context.Context, body []byte, authHeader string) (*livekit.WebhookEvent, error) { if s.config.WebhookSecret != "" { if err := s.verifyWebhookSignature(body, authHeader); err != nil { return nil, fmt.Errorf("webhook signature verification failed: %w", err) } } var event livekit.WebhookEvent if err := protojson.Unmarshal(body, &event); err != nil { return nil, fmt.Errorf("failed to unmarshal webhook event: %w", err) } s.logger.Info("received livekit webhook", zap.String("event", event.Event), zap.String("room_name", event.Room.GetName()), ) return &event, nil } // verifyWebhookSignature 验证 Webhook 签名 func (s *liveKitService) verifyWebhookSignature(body []byte, authHeader string) error { // LiveKit webhook 使用 SHA256 HMAC 签名 // Authorization header 格式: "sha256=" if authHeader == "" { return fmt.Errorf("missing authorization header") } expectedPrefix := "sha256=" if len(authHeader) < len(expectedPrefix) || authHeader[:len(expectedPrefix)] != expectedPrefix { return fmt.Errorf("invalid authorization header format") } expectedSig, err := base64.StdEncoding.DecodeString(authHeader[len(expectedPrefix):]) if err != nil { return fmt.Errorf("failed to decode signature: %w", err) } mac := hmac.New(sha256.New, []byte(s.config.WebhookSecret)) mac.Write(body) actualSig := mac.Sum(nil) if !hmac.Equal(actualSig, expectedSig) { return fmt.Errorf("signature mismatch") } return nil }