/** * package mediaserver * * 通话房间管理 * 功能: * 1. 管理房间内的参与者 * 2. 管理 WebRTC 和 RTMP 流 * 3. 处理用户加入/离开 */ package mediaserver import ( "log" "sync" "time" ) // ParticipantType 参与者类型 type ParticipantType int const ( ParticipantTypeWebRTC ParticipantType = iota // H5/App WebRTC 用户 ParticipantTypeRTMP // 小程序 RTMP 用户 ) // Participant 房间参与者 type Participant struct { UserID string `json:"user_id"` Type ParticipantType `json:"type"` JoinedAt time.Time `json:"joined_at"` HasAudio bool `json:"has_audio"` HasVideo bool `json:"has_video"` // WebRTC 相关 PeerConnection interface{} `json:"-"` // *webrtc.PeerConnection // RTMP 相关 PushURL string `json:"push_url,omitempty"` // 推流地址 PullURL string `json:"pull_url,omitempty"` // 拉流地址 (RTMP) FLVURL string `json:"flv_url,omitempty"` // HTTP-FLV 拉流地址 (给 Web 端用) StreamID string `json:"stream_id,omitempty"` // 流ID } // Room 通话房间 type Room struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` MaxSize int `json:"max_size"` Participants map[string]*Participant `json:"participants"` // 房间状态 CallType string `json:"call_type"` // audio/video IsGroupCall bool `json:"is_group_call"` mu sync.RWMutex closeChan chan struct{} closed bool } // RoomInfo 房间信息(用于API返回) type RoomInfo struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` ParticipantCount int `json:"participant_count"` CallType string `json:"call_type"` IsGroupCall bool `json:"is_group_call"` Participants []ParticipantInfo `json:"participants"` } // ParticipantInfo 参与者信息(用于API返回) type ParticipantInfo struct { UserID string `json:"user_id"` Type ParticipantType `json:"type"` TypeName string `json:"type_name"` HasAudio bool `json:"has_audio"` HasVideo bool `json:"has_video"` JoinedAt time.Time `json:"joined_at"` } // NewRoom 创建新房间 func NewRoom(id string, maxSize int) *Room { return &Room{ ID: id, CreatedAt: time.Now(), MaxSize: maxSize, Participants: make(map[string]*Participant), closeChan: make(chan struct{}), } } // SetCallType 设置通话类型 func (r *Room) SetCallType(callType string, isGroup bool) { r.mu.Lock() defer r.mu.Unlock() r.CallType = callType r.IsGroupCall = isGroup } // AddParticipant 添加参与者 func (r *Room) AddParticipant(userID string, pType ParticipantType) (*Participant, error) { r.mu.Lock() defer r.mu.Unlock() if r.closed { return nil, ErrRoomClosed } // 检查是否已存在 if p, exists := r.Participants[userID]; exists { return p, nil } // 检查房间容量 if len(r.Participants) >= r.MaxSize { return nil, ErrRoomFull } participant := &Participant{ UserID: userID, Type: pType, JoinedAt: time.Now(), } r.Participants[userID] = participant log.Printf("🎥 [Room:%s] 用户 %s 加入 (类型: %d)", r.ID, userID, pType) return participant, nil } // RemoveParticipant 移除参与者 func (r *Room) RemoveParticipant(userID string) { r.mu.Lock() defer r.mu.Unlock() if p, exists := r.Participants[userID]; exists { // 清理 PeerConnection if p.PeerConnection != nil { // TODO: 关闭 PeerConnection } delete(r.Participants, userID) log.Printf("🎥 [Room:%s] 用户 %s 离开", r.ID, userID) } // 如果房间空了,标记为可清理 if len(r.Participants) == 0 { log.Printf("🎥 [Room:%s] 房间已空,等待清理", r.ID) } } // GetParticipant 获取参与者 func (r *Room) GetParticipant(userID string) *Participant { r.mu.RLock() defer r.mu.RUnlock() return r.Participants[userID] } // GetAllParticipants 获取所有参与者 func (r *Room) GetAllParticipants() []*Participant { r.mu.RLock() defer r.mu.RUnlock() participants := make([]*Participant, 0, len(r.Participants)) for _, p := range r.Participants { participants = append(participants, p) } return participants } // GetOtherParticipants 获取除指定用户外的其他参与者 func (r *Room) GetOtherParticipants(excludeUserID string) []*Participant { r.mu.RLock() defer r.mu.RUnlock() participants := make([]*Participant, 0, len(r.Participants)-1) for _, p := range r.Participants { if p.UserID != excludeUserID { participants = append(participants, p) } } return participants } // GetParticipantCount 获取参与者数量 func (r *Room) GetParticipantCount() int { r.mu.RLock() defer r.mu.RUnlock() return len(r.Participants) } // IsEmpty 房间是否为空 func (r *Room) IsEmpty() bool { r.mu.RLock() defer r.mu.RUnlock() return len(r.Participants) == 0 } // IsFull 房间是否已满 func (r *Room) IsFull() bool { r.mu.RLock() defer r.mu.RUnlock() return len(r.Participants) >= r.MaxSize } // GetInfo 获取房间信息 func (r *Room) GetInfo() *RoomInfo { r.mu.RLock() defer r.mu.RUnlock() participants := make([]ParticipantInfo, 0, len(r.Participants)) for _, p := range r.Participants { typeName := "WebRTC" if p.Type == ParticipantTypeRTMP { typeName = "RTMP" } participants = append(participants, ParticipantInfo{ UserID: p.UserID, Type: p.Type, TypeName: typeName, HasAudio: p.HasAudio, HasVideo: p.HasVideo, JoinedAt: p.JoinedAt, }) } return &RoomInfo{ ID: r.ID, CreatedAt: r.CreatedAt, ParticipantCount: len(r.Participants), CallType: r.CallType, IsGroupCall: r.IsGroupCall, Participants: participants, } } // UpdateParticipantMedia 更新参与者媒体状态 func (r *Room) UpdateParticipantMedia(userID string, hasAudio, hasVideo bool) { r.mu.Lock() defer r.mu.Unlock() if p, exists := r.Participants[userID]; exists { p.HasAudio = hasAudio p.HasVideo = hasVideo } } // SetParticipantRTMPURLs 设置参与者的RTMP推拉流地址 func (r *Room) SetParticipantRTMPURLs(userID, pushURL, pullURL, flvURL, streamID string) { r.mu.Lock() defer r.mu.Unlock() if p, exists := r.Participants[userID]; exists { p.PushURL = pushURL p.PullURL = pullURL p.FLVURL = flvURL p.StreamID = streamID } } // Close 关闭房间 func (r *Room) Close() { r.mu.Lock() defer r.mu.Unlock() if r.closed { return } r.closed = true close(r.closeChan) // 清理所有参与者 for userID, p := range r.Participants { if p.PeerConnection != nil { // TODO: 关闭 PeerConnection } delete(r.Participants, userID) } log.Printf("🎥 [Room:%s] 已关闭", r.ID) } // IsClosed 房间是否已关闭 func (r *Room) IsClosed() bool { r.mu.RLock() defer r.mu.RUnlock() return r.closed } // Broadcast 向房间内所有参与者广播消息(用于信令) func (r *Room) Broadcast(excludeUserID string, message interface{}) { r.mu.RLock() defer r.mu.RUnlock() for userID := range r.Participants { if userID != excludeUserID { // TODO: 通过 WebSocket 发送消息 _ = message } } }