301 lines
7.1 KiB
Go
301 lines
7.1 KiB
Go
/**
|
||
* 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
|
||
}
|
||
}
|
||
}
|