feat(call): implement group calling and enhance call management
Introduces group calling capabilities, including group invites and participant lifecycle management. Enhances the existing call system with support for media types (voice/video) and improved participant tracking. - Add `GroupInvite` and `ParticipantJoin/Leave` to `CallService`. - Implement WebSocket handlers for `call_group_invite` and `call_participant_join`. - Update `CallSession` model to support group IDs, media types, and tracking who ended the call. - Integrate `GroupService` into `CallService` via dependency injection. - Add error constants for group call limitations and participant capacity. - Update LiveKit webhook handling to process participant leave events. Refactor grade synchronization to remove semester-based filtering in favor of a more flexible user-based approach and improve GPA summary updates.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
redispkg "with_you/internal/pkg/redis"
|
||||
"with_you/internal/pkg/utils"
|
||||
|
||||
"with_you/internal/config"
|
||||
apperrors "with_you/internal/errors"
|
||||
@@ -34,15 +35,17 @@ const (
|
||||
|
||||
// activeCallRedisData Redis 中存储的通话数据(精简字段)
|
||||
type activeCallRedisData struct {
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
CallerID string `json:"caller_id"`
|
||||
CalleeID string `json:"callee_id"`
|
||||
CallType string `json:"call_type"`
|
||||
Status string `json:"status"`
|
||||
MediaType string `json:"media_type"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
StartedAt int64 `json:"started_at"` // 0 means nil
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
CallerID string `json:"caller_id"`
|
||||
CalleeID string `json:"callee_id,omitempty"` // 兼容旧数据,1v1时仍有值
|
||||
CallType string `json:"call_type"`
|
||||
Status string `json:"status"`
|
||||
MediaType string `json:"media_type"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
StartedAt int64 `json:"started_at"` // 0 means nil
|
||||
ParticipantIDs []string `json:"participant_ids"`
|
||||
}
|
||||
|
||||
// ActiveCall 内存中的活跃通话
|
||||
@@ -50,7 +53,8 @@ type ActiveCall struct {
|
||||
ID string
|
||||
ConversationID string
|
||||
CallerID string
|
||||
CalleeID string
|
||||
CalleeID string // 1v1通话的被叫方ID(兼容)
|
||||
GroupID string // 群组通话的群组ID
|
||||
CallType model.CallType
|
||||
Status model.CallStatus
|
||||
MediaType string // voice 或 video
|
||||
@@ -66,15 +70,19 @@ type ActiveParticipant struct {
|
||||
UserID string
|
||||
Status model.ParticipantStatus
|
||||
JoinedAt *time.Time
|
||||
LeftAt *time.Time
|
||||
}
|
||||
|
||||
// CallService 通话服务接口
|
||||
type CallService interface {
|
||||
Invite(ctx context.Context, callerID, calleeID, conversationID, mediaType string) (*ActiveCall, bool, error)
|
||||
GroupInvite(ctx context.Context, callerID, groupID, conversationID, mediaType string) (*ActiveCall, error)
|
||||
Accept(ctx context.Context, callID, userID string) (*ActiveCall, error)
|
||||
Reject(ctx context.Context, callID, userID string) error
|
||||
Busy(ctx context.Context, callID, userID string) error
|
||||
End(ctx context.Context, callID, userID string, reason string) (*ActiveCall, error)
|
||||
ParticipantJoin(ctx context.Context, callID, userID string) error
|
||||
ParticipantLeave(ctx context.Context, callID, userID string, reason string) error
|
||||
SetMuted(ctx context.Context, callID, userID string, muted bool) error
|
||||
Ready(ctx context.Context, callID, userID string) error
|
||||
GetActiveCall(ctx context.Context, callID string) *ActiveCall
|
||||
@@ -83,11 +91,12 @@ type CallService interface {
|
||||
}
|
||||
|
||||
type callService struct {
|
||||
callRepo repository.CallRepository
|
||||
hub ws.MessagePublisher
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
redis *redispkg.Client
|
||||
callRepo repository.CallRepository
|
||||
hub ws.MessagePublisher
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
redis *redispkg.Client
|
||||
groupService GroupService
|
||||
|
||||
// 内存中的活跃通话状态
|
||||
activeCalls map[string]*ActiveCall // callID -> ActiveCall
|
||||
@@ -103,6 +112,7 @@ func NewCallService(
|
||||
cfg *config.Config,
|
||||
db *gorm.DB,
|
||||
redisClient *redispkg.Client,
|
||||
groupService GroupService,
|
||||
) CallService {
|
||||
svc := &callService{
|
||||
callRepo: callRepo,
|
||||
@@ -110,6 +120,7 @@ func NewCallService(
|
||||
config: cfg,
|
||||
db: db,
|
||||
redis: redisClient,
|
||||
groupService: groupService,
|
||||
activeCalls: make(map[string]*ActiveCall),
|
||||
activeCallsByUser: make(map[string]map[string]bool),
|
||||
activeCallsByConv: make(map[string]string),
|
||||
@@ -124,9 +135,13 @@ func NewCallService(
|
||||
return svc
|
||||
}
|
||||
|
||||
// generateCallID 生成通话ID
|
||||
// generateCallID 生成通话ID(使用雪花算法,与数据库ID一致)
|
||||
func (s *callService) generateCallID() string {
|
||||
id := time.Now().UnixNano()
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
zap.L().Error("Failed to generate snowflake call ID", zap.Error(err))
|
||||
return strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
}
|
||||
return strconv.FormatInt(id, 10)
|
||||
}
|
||||
|
||||
@@ -141,6 +156,12 @@ func (s *callService) redisStoreCall(call *ActiveCall) {
|
||||
if call.StartedAt != nil {
|
||||
startedAt = call.StartedAt.UnixMilli()
|
||||
}
|
||||
|
||||
participantIDs := make([]string, 0, len(call.Participants))
|
||||
for id := range call.Participants {
|
||||
participantIDs = append(participantIDs, id)
|
||||
}
|
||||
|
||||
data := activeCallRedisData{
|
||||
ID: call.ID,
|
||||
ConversationID: call.ConversationID,
|
||||
@@ -149,8 +170,10 @@ func (s *callService) redisStoreCall(call *ActiveCall) {
|
||||
CallType: string(call.CallType),
|
||||
Status: string(call.Status),
|
||||
MediaType: call.MediaType,
|
||||
GroupID: call.GroupID,
|
||||
CreatedAt: call.CreatedAt.UnixMilli(),
|
||||
StartedAt: startedAt,
|
||||
ParticipantIDs: participantIDs,
|
||||
}
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
@@ -159,13 +182,13 @@ func (s *callService) redisStoreCall(call *ActiveCall) {
|
||||
}
|
||||
|
||||
callKey := redisCallKeyPrefix + call.ID
|
||||
callerKey := redisCallByUserPrefix + call.CallerID
|
||||
calleeKey := redisCallByUserPrefix + call.CalleeID
|
||||
|
||||
pipe := s.redis.Pipeline()
|
||||
pipe.Set(ctx, callKey, jsonData, redisCallTTL)
|
||||
pipe.Set(ctx, callerKey, call.ID, redisCallTTL)
|
||||
pipe.Set(ctx, calleeKey, call.ID, redisCallTTL)
|
||||
// 为每个参与者建立索引
|
||||
for userID := range call.Participants {
|
||||
pipe.Set(ctx, redisCallByUserPrefix+userID, call.ID, redisCallTTL)
|
||||
}
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
zap.L().Error("Failed to store call in Redis", zap.String("call_id", call.ID), zap.Error(err))
|
||||
}
|
||||
@@ -180,10 +203,11 @@ func (s *callService) redisRemoveCall(call *ActiveCall) {
|
||||
|
||||
keys := []string{
|
||||
redisCallKeyPrefix + call.ID,
|
||||
redisCallByUserPrefix + call.CallerID,
|
||||
redisCallByUserPrefix + call.CalleeID,
|
||||
redisCallAcceptPrefix + call.ID,
|
||||
}
|
||||
for userID := range call.Participants {
|
||||
keys = append(keys, redisCallByUserPrefix+userID)
|
||||
}
|
||||
if err := s.redis.Del(ctx, keys...); err != nil {
|
||||
zap.L().Error("Failed to remove call from Redis", zap.String("call_id", call.ID), zap.Error(err))
|
||||
}
|
||||
@@ -207,18 +231,38 @@ func (s *callService) redisGetCall(callID string) *ActiveCall {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建参与者列表
|
||||
participants := make(map[string]*ActiveParticipant)
|
||||
for _, pid := range data.ParticipantIDs {
|
||||
status := model.ParticipantStatusInvited
|
||||
if pid == data.CallerID {
|
||||
status = model.ParticipantStatusJoined
|
||||
}
|
||||
participants[pid] = &ActiveParticipant{
|
||||
UserID: pid,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
// 兼容旧数据:无 ParticipantIDs 时使用 CallerID/CalleeID
|
||||
if len(participants) == 0 && data.CalleeID != "" {
|
||||
participants[data.CallerID] = &ActiveParticipant{
|
||||
UserID: data.CallerID, Status: model.ParticipantStatusJoined,
|
||||
}
|
||||
participants[data.CalleeID] = &ActiveParticipant{
|
||||
UserID: data.CalleeID, Status: model.ParticipantStatusInvited,
|
||||
}
|
||||
}
|
||||
|
||||
call := &ActiveCall{
|
||||
ID: data.ID,
|
||||
ConversationID: data.ConversationID,
|
||||
CallerID: data.CallerID,
|
||||
CalleeID: data.CalleeID,
|
||||
GroupID: data.GroupID,
|
||||
CallType: model.CallType(data.CallType),
|
||||
Status: model.CallStatus(data.Status),
|
||||
MediaType: data.MediaType,
|
||||
Participants: map[string]*ActiveParticipant{
|
||||
data.CallerID: {UserID: data.CallerID, Status: model.ParticipantStatusJoined},
|
||||
data.CalleeID: {UserID: data.CalleeID, Status: model.ParticipantStatusInvited},
|
||||
},
|
||||
Participants: participants,
|
||||
}
|
||||
|
||||
call.CreatedAt = time.UnixMilli(data.CreatedAt)
|
||||
@@ -226,10 +270,11 @@ func (s *callService) redisGetCall(callID string) *ActiveCall {
|
||||
t := time.UnixMilli(data.StartedAt)
|
||||
call.StartedAt = &t
|
||||
// 如果已经开始,被叫方也已加入
|
||||
call.Participants[data.CalleeID] = &ActiveParticipant{
|
||||
UserID: data.CalleeID,
|
||||
Status: model.ParticipantStatusJoined,
|
||||
JoinedAt: call.StartedAt,
|
||||
if data.CalleeID != "" {
|
||||
if p, ok := call.Participants[data.CalleeID]; ok {
|
||||
p.Status = model.ParticipantStatusJoined
|
||||
p.JoinedAt = call.StartedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,10 +288,9 @@ func (s *callService) redisRefreshTTL(call *ActiveCall) {
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
keys := []string{
|
||||
redisCallKeyPrefix + call.ID,
|
||||
redisCallByUserPrefix + call.CallerID,
|
||||
redisCallByUserPrefix + call.CalleeID,
|
||||
keys := []string{redisCallKeyPrefix + call.ID}
|
||||
for userID := range call.Participants {
|
||||
keys = append(keys, redisCallByUserPrefix+userID)
|
||||
}
|
||||
for _, key := range keys {
|
||||
_, _ = s.redis.Expire(ctx, key, redisCallTTL)
|
||||
@@ -506,7 +550,7 @@ func (s *callService) Reject(ctx context.Context, callID, userID string) error {
|
||||
s.redisRemoveCall(call)
|
||||
|
||||
// 保存到数据库作为历史记录
|
||||
s.saveCallHistory(call, model.CallStatusRejected, 0)
|
||||
s.saveCallHistory(call, model.CallStatusRejected, 0, userID)
|
||||
|
||||
// 通知拨打方
|
||||
s.hub.PublishToUserOnline(call.CallerID, "call_rejected", map[string]any{
|
||||
@@ -543,7 +587,7 @@ func (s *callService) Busy(ctx context.Context, callID, userID string) error {
|
||||
s.redisRemoveCall(call)
|
||||
|
||||
// 保存到数据库作为历史记录
|
||||
s.saveCallHistory(call, model.CallStatusMissed, 0)
|
||||
s.saveCallHistory(call, model.CallStatusMissed, 0, "")
|
||||
|
||||
// 通知拨打方
|
||||
s.hub.PublishToUserOnline(call.CallerID, "call_busy", map[string]any{
|
||||
@@ -602,7 +646,7 @@ func (s *callService) End(ctx context.Context, callID, userID string, reason str
|
||||
s.redisRemoveCall(call)
|
||||
|
||||
// 保存到数据库作为历史记录
|
||||
s.saveCallHistory(call, endStatus, duration)
|
||||
s.saveCallHistory(call, endStatus, duration, userID)
|
||||
|
||||
// 通知其他参与者
|
||||
for pUserID := range call.Participants {
|
||||
@@ -654,6 +698,174 @@ func (s *callService) GetActiveCall(ctx context.Context, callID string) *ActiveC
|
||||
return s.getActiveCall(callID)
|
||||
}
|
||||
|
||||
// GroupInvite 发起群组通话
|
||||
func (s *callService) GroupInvite(ctx context.Context, callerID, groupID, conversationID, mediaType string) (*ActiveCall, error) {
|
||||
if s.groupService == nil {
|
||||
return nil, apperrors.ErrGroupCallNotSupported
|
||||
}
|
||||
|
||||
// 验证调用者是群组成员
|
||||
member, err := s.groupService.GetMember(groupID, callerID)
|
||||
if err != nil {
|
||||
return nil, apperrors.ErrNotGroupMember
|
||||
}
|
||||
if member == nil {
|
||||
return nil, apperrors.ErrNotGroupMember
|
||||
}
|
||||
|
||||
// 检查会话是否有活跃通话
|
||||
if active := s.getActiveCallByConversation(conversationID); active != nil {
|
||||
return nil, apperrors.Wrap(fmt.Errorf("call %s", active.ID), apperrors.ErrCallInProgress)
|
||||
}
|
||||
|
||||
// 获取群组成员
|
||||
members, _, err := s.groupService.GetMembers(groupID, 1, 50)
|
||||
if err != nil {
|
||||
return nil, apperrors.Wrap(err, apperrors.ErrInternal)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
callID := s.generateCallID()
|
||||
|
||||
// 构建参与者列表
|
||||
participants := map[string]*ActiveParticipant{
|
||||
callerID: {UserID: callerID, Status: model.ParticipantStatusJoined, JoinedAt: &now},
|
||||
}
|
||||
for _, m := range members {
|
||||
if m.UserID != callerID {
|
||||
participants[m.UserID] = &ActiveParticipant{
|
||||
UserID: m.UserID,
|
||||
Status: model.ParticipantStatusInvited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
call := &ActiveCall{
|
||||
ID: callID,
|
||||
ConversationID: conversationID,
|
||||
CallerID: callerID,
|
||||
GroupID: groupID,
|
||||
CallType: model.CallTypeGroup,
|
||||
Status: model.CallStatusCalling,
|
||||
MediaType: mediaType,
|
||||
CreatedAt: now,
|
||||
Participants: participants,
|
||||
}
|
||||
|
||||
s.storeActiveCall(call)
|
||||
s.redisStoreCall(call)
|
||||
|
||||
// 通知所有在线成员
|
||||
payload := map[string]any{
|
||||
"call_id": call.ID,
|
||||
"conversation_id": conversationID,
|
||||
"caller_id": callerID,
|
||||
"call_type": "group",
|
||||
"media_type": mediaType,
|
||||
"group_id": groupID,
|
||||
"created_at": now.UnixMilli(),
|
||||
"lifetime": CallLifetimeMs,
|
||||
}
|
||||
|
||||
for pID := range participants {
|
||||
if pID != callerID {
|
||||
s.hub.PublishToUserOnline(pID, "call_incoming", payload)
|
||||
}
|
||||
}
|
||||
|
||||
return call, nil
|
||||
}
|
||||
|
||||
// ParticipantJoin 参与者加入通话
|
||||
func (s *callService) ParticipantJoin(ctx context.Context, callID, userID string) error {
|
||||
call := s.getActiveCall(callID)
|
||||
if call == nil {
|
||||
return apperrors.ErrCallNotFound
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
p, ok := call.Participants[userID]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return apperrors.ErrNotCallParticipant
|
||||
}
|
||||
now := time.Now()
|
||||
p.Status = model.ParticipantStatusJoined
|
||||
p.JoinedAt = &now
|
||||
|
||||
// 如果是第一个被叫方加入,标记通话已接通
|
||||
if call.Status == model.CallStatusCalling {
|
||||
call.Status = model.CallStatusConnected
|
||||
call.StartedAt = &now
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
s.redisStoreCall(call)
|
||||
|
||||
// 通知其他参与者
|
||||
for pID := range call.Participants {
|
||||
if pID != userID {
|
||||
s.hub.PublishToUserOnline(pID, "call_participant_joined", map[string]any{
|
||||
"call_id": callID,
|
||||
"user_id": userID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParticipantLeave 参与者离开通话
|
||||
func (s *callService) ParticipantLeave(ctx context.Context, callID, userID string, reason string) error {
|
||||
call := s.getActiveCall(callID)
|
||||
if call == nil {
|
||||
return apperrors.ErrCallNotFound
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
p, ok := call.Participants[userID]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return apperrors.ErrNotCallParticipant
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
p.Status = model.ParticipantStatusLeft
|
||||
p.LeftAt = &now
|
||||
|
||||
// 计算剩余活跃参与者数量
|
||||
activeCount := 0
|
||||
for _, pp := range call.Participants {
|
||||
if pp.Status == model.ParticipantStatusJoined {
|
||||
activeCount++
|
||||
}
|
||||
}
|
||||
shouldEnd := false
|
||||
if call.CallType == model.CallTypeGroup && activeCount <= 1 {
|
||||
shouldEnd = true
|
||||
} else if call.CallType == model.CallTypePrivate {
|
||||
shouldEnd = true
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// 通知其他参与者
|
||||
for pID := range call.Participants {
|
||||
if pID != userID {
|
||||
s.hub.PublishToUserOnline(pID, "call_participant_left", map[string]any{
|
||||
"call_id": callID,
|
||||
"user_id": userID,
|
||||
"reason": reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if shouldEnd {
|
||||
_, _ = s.End(ctx, callID, userID, reason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ready 标记参与者已加入 LiveKit 房间
|
||||
func (s *callService) Ready(ctx context.Context, callID, userID string) error {
|
||||
call := s.getActiveCall(callID)
|
||||
@@ -686,16 +898,23 @@ func (s *callService) GetCallHistory(ctx context.Context, userID string, page, p
|
||||
|
||||
|
||||
// saveCallHistory 保存通话历史到数据库
|
||||
func (s *callService) saveCallHistory(call *ActiveCall, status model.CallStatus, duration int64) {
|
||||
func (s *callService) saveCallHistory(call *ActiveCall, status model.CallStatus, duration int64, endedBy string) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
var endedByPtr *string
|
||||
if endedBy != "" {
|
||||
endedByPtr = &endedBy
|
||||
}
|
||||
|
||||
dbCall := &model.CallSession{
|
||||
ID: call.ID,
|
||||
ConversationID: call.ConversationID,
|
||||
CallerID: call.CallerID,
|
||||
CallType: call.CallType,
|
||||
MediaType: call.MediaType,
|
||||
Status: status,
|
||||
EndedBy: endedByPtr,
|
||||
StartedAt: call.StartedAt,
|
||||
EndedAt: &now,
|
||||
Duration: duration,
|
||||
|
||||
@@ -81,7 +81,7 @@ func (s *gradeSyncService) SyncUserGrades(ctx context.Context, userID string, re
|
||||
}
|
||||
|
||||
grades := s.convertGrades(userID, &resultData)
|
||||
if err := s.saveGrades(ctx, userID, resultData.Semester, grades); err != nil {
|
||||
if err := s.saveGrades(ctx, userID, grades); err != nil {
|
||||
s.logger.Error("failed to save grades",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
@@ -89,7 +89,12 @@ func (s *gradeSyncService) SyncUserGrades(ctx context.Context, userID string, re
|
||||
}
|
||||
|
||||
if resultData.GpaSummary != nil {
|
||||
summary := s.convertGpaSummary(userID, resultData.Semester, resultData.GpaSummary)
|
||||
if err := s.gradeRepo.DeleteGpaSummaryByUser(ctx, userID); err != nil {
|
||||
s.logger.Warn("failed to delete old gpa summary",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
summary := s.convertGpaSummary(userID, resultData.GpaSummary)
|
||||
if err := s.gradeRepo.SaveGpaSummary(ctx, summary); err != nil {
|
||||
s.logger.Warn("failed to save gpa summary",
|
||||
zap.String("user_id", userID),
|
||||
@@ -128,11 +133,11 @@ func (s *gradeSyncService) convertGrades(userID string, data *pb.GradesResultDat
|
||||
return grades
|
||||
}
|
||||
|
||||
func (s *gradeSyncService) convertGpaSummary(userID, semester string, gpa *pb.GpaSummary) *model.GpaSummary {
|
||||
func (s *gradeSyncService) convertGpaSummary(userID string, gpa *pb.GpaSummary) *model.GpaSummary {
|
||||
return &model.GpaSummary{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Semester: semester,
|
||||
Semester: gpa.Semesters,
|
||||
AverageGpa: gpa.AverageGpa,
|
||||
Rank: gpa.Rank,
|
||||
ExamTotalGpa: gpa.ExamTotalGpa,
|
||||
@@ -142,8 +147,8 @@ func (s *gradeSyncService) convertGpaSummary(userID, semester string, gpa *pb.Gp
|
||||
}
|
||||
}
|
||||
|
||||
func (s *gradeSyncService) saveGrades(ctx context.Context, userID, semester string, grades []*model.Grade) error {
|
||||
if err := s.gradeRepo.DeleteByUserAndSemester(ctx, userID, semester); err != nil {
|
||||
func (s *gradeSyncService) saveGrades(ctx context.Context, userID string, grades []*model.Grade) error {
|
||||
if err := s.gradeRepo.DeleteByUser(ctx, userID); err != nil {
|
||||
return fmt.Errorf("failed to delete old grades: %w", err)
|
||||
}
|
||||
if len(grades) == 0 {
|
||||
|
||||
@@ -17,9 +17,15 @@ import (
|
||||
"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) (string, error)
|
||||
GenerateToken(roomName, userID string, canPublish, canSubscribe bool, roomCfg ...RoomConfig) (string, error)
|
||||
ProcessWebhook(ctx context.Context, body []byte, authHeader string) (*livekit.WebhookEvent, error)
|
||||
}
|
||||
|
||||
@@ -37,7 +43,7 @@ func NewLiveKitService(cfg *config.Config, logger *zap.Logger) LiveKitService {
|
||||
}
|
||||
|
||||
// GenerateToken 为指定 room 和 user 生成 LiveKit 访问令牌
|
||||
func (s *liveKitService) GenerateToken(roomName, userID string, canPublish, canSubscribe bool) (string, error) {
|
||||
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")
|
||||
}
|
||||
@@ -57,8 +63,9 @@ func (s *liveKitService) GenerateToken(roomName, userID string, canPublish, canS
|
||||
Room: roomName,
|
||||
CanPublish: &canPublish,
|
||||
CanSubscribe: &canSubscribe,
|
||||
CanPublishData: &canSubscribe,
|
||||
CanPublishData: &canPublish,
|
||||
}
|
||||
|
||||
at.SetVideoGrant(grant)
|
||||
|
||||
token, err := at.ToJWT()
|
||||
|
||||
Reference in New Issue
Block a user