Files
nl-im-service/internal/mediaserver/room.go
2025-12-15 13:21:08 +08:00

301 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
}
}
}