package sfu import ( "g365sfu/logger" connection "g365sfu/socket/struct" "sync" "github.com/pion/webrtc/v4" ) // Структуры для управления комнатами и пирами в SFU type Peer struct { //Идентификатор пира, который будет использоваться для связи с ним PeerID string //Подсоединенный пир PeerConnection *webrtc.PeerConnection } type RoomTrack struct { TrackID string OwnerPeer string Local *webrtc.TrackLocalStaticRTP } type Room struct { //Уникальный идентификатор комнаты RoomID string //Сервер который создал комнату Server *connection.Connection //Пиры которые подключились к комнате Peers []Peer Tracks []RoomTrack mu sync.RWMutex } // Общие переменные var ( rooms = make(map[string]*Room) roomsMu sync.RWMutex ) // CreateRoom создает комнату func CreateRoom(server *connection.Connection, roomID string) (*Room, error) { roomsMu.Lock() defer roomsMu.Unlock() room := &Room{ RoomID: roomID, Server: server, Peers: []Peer{}, } rooms[roomID] = room return room, nil } // GetRoom получает комнату по идентификатору func GetRoom(roomID string) (*Room, bool) { roomsMu.RLock() defer roomsMu.RUnlock() room, exists := rooms[roomID] return room, exists } // JoinWithOffer позволяет пиру присоединиться к комнате с помощью SDP оффера func JoinWithOffer(roomID string, peerID string, offer webrtc.SessionDescription) (*webrtc.SessionDescription, error) { room, exists := GetRoom(roomID) if !exists { return nil, ErrRoomNotFound } peerConnection, err := api.NewPeerConnection(pcConfig) if err != nil { return nil, err } BindPeerLifecycle(roomID, peerID, peerConnection) SetupForwardingForPeer(roomID, peerID, peerConnection) if err = peerConnection.SetRemoteDescription(offer); err != nil { _ = peerConnection.Close() return nil, err } answer, err := peerConnection.CreateAnswer(nil) if err != nil { _ = peerConnection.Close() return nil, err } gatherDone := webrtc.GatheringCompletePromise(peerConnection) if err = peerConnection.SetLocalDescription(answer); err != nil { _ = peerConnection.Close() return nil, err } <-gatherDone peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { if c == nil { return } if OnLocalICECandidate != nil { OnLocalICECandidate(roomID, peerID, c.ToJSON()) } }) // Добавляем peer в комнату и сразу снимаем snapshot существующих треков // в одном локе — чтобы не было race с OnTrack room.mu.Lock() room.Peers = append(room.Peers, Peer{ PeerID: peerID, PeerConnection: peerConnection, }) existingTracks := make([]RoomTrack, len(room.Tracks)) copy(existingTracks, room.Tracks) room.mu.Unlock() // Подписываем нового peer на уже существующие треки ПОСЛЕ добавления в комнату for _, t := range existingTracks { if t.OwnerPeer == peerID { continue } sender, err := peerConnection.AddTrack(t.Local) if err != nil { continue } senderCopy := sender go func() { buf := make([]byte, 1500) for { if _, _, e := senderCopy.Read(buf); e != nil { return } } }() } // Если были добавлены треки — нужна renegotiation if len(existingTracks) > 0 { go func() { if err := renegotiatePeer(roomID, peerID, peerConnection); err != nil { logger.LogWarnMessage("JoinWithOffer: renegotiatePeer error: " + err.Error()) } }() } return peerConnection.LocalDescription(), nil } func DeleteRoom(roomID string) error { roomsMu.Lock() room, exists := rooms[roomID] server := room.Server if !exists { roomsMu.Unlock() return ErrRoomNotFound } delete(rooms, roomID) roomsMu.Unlock() room.mu.Lock() peers := make([]Peer, len(room.Peers)) copy(peers, room.Peers) room.Peers = nil room.Tracks = nil room.mu.Unlock() for _, p := range peers { _ = p.PeerConnection.Close() cleanupForwardingState(roomID, p.PeerID) } OnRoomDelete(roomID, server) return nil } // LeaveRoom позволяет пиру покинуть комнату func LeaveRoom(roomID string, peerID string) error { room, exists := GetRoom(roomID) if !exists { return ErrRoomNotFound } var ( removedPC *webrtc.PeerConnection removed bool shouldDrop bool ) room.mu.Lock() // удаляем peer nextPeers := make([]Peer, 0, len(room.Peers)) for _, p := range room.Peers { if p.PeerID == peerID { removedPC = p.PeerConnection removed = true continue } nextPeers = append(nextPeers, p) } room.Peers = nextPeers // удаляем треки этого publisher nextTracks := room.Tracks[:0] for _, t := range room.Tracks { if t.OwnerPeer != peerID { nextTracks = append(nextTracks, t) } } room.Tracks = nextTracks shouldDrop = len(room.Peers) == 0 room.mu.Unlock() if !removed { return ErrPeerNotFound } if removedPC != nil { _ = removedPC.Close() } cleanupForwardingState(roomID, peerID) // Комната пустая -> удаляем if shouldDrop { return DeleteRoom(roomID) } // renegotiation оставшимся peer после удаления треков/peer room.mu.RLock() rest := make([]Peer, len(room.Peers)) copy(rest, room.Peers) room.mu.RUnlock() for _, p := range rest { _ = renegotiatePeer(roomID, p.PeerID, p.PeerConnection) } return nil }