Исправление Race при renegotiation

This commit is contained in:
set
2026-03-17 19:17:02 +02:00
parent 96df1e52f9
commit 6dadec6b64
6 changed files with 109 additions and 78 deletions

View File

@@ -59,7 +59,7 @@ func Bootstrap() {
// TURN сервер выключен в конфиге, что может влиять на соединение некоторых пользователей // TURN сервер выключен в конфиге, что может влиять на соединение некоторых пользователей
logger.LogInfoMessage("starting without TURN server, peer connections may fail if clients are behind symmetric NATs") logger.LogInfoMessage("starting without TURN server, peer connections may fail if clients are behind symmetric NATs")
} }
logger.LogInfoMessage("server started at x.x.x.x:" + port) logger.LogInfoMessage("server SFU started at x.x.x.x:" + port)
http.ListenAndServe(":"+port, nil) http.ListenAndServe(":"+port, nil)
} }
@@ -116,13 +116,14 @@ func OnRoomDelete(roomID string, server *connection.Connection) {
server.WriteBinary(buffer.Bytes()) server.WriteBinary(buffer.Bytes())
} }
func OnPeerDisconnected(roomID string, peerID string, server *connection.Connection) { func OnPeerDisconnected(roomID string, peerID string, server *connection.Connection, reason sfu.DisconnectReason) {
buffer := bytebuffer.Allocate(1 + 4 + len([]byte(roomID)) + 4 + len([]byte(peerID))) buffer := bytebuffer.Allocate(1 + 4 + len([]byte(roomID)) + 4 + len([]byte(peerID)) + 4)
buffer.Put(byte(network.ON_PEER_DISCONNECTED)) buffer.Put(byte(network.ON_PEER_DISCONNECTED))
buffer.PutUint32(uint32(len([]byte(roomID)))) buffer.PutUint32(uint32(len([]byte(roomID))))
buffer.PutBytes([]byte(roomID)) buffer.PutBytes([]byte(roomID))
buffer.PutUint32(uint32(len([]byte(peerID)))) buffer.PutUint32(uint32(len([]byte(peerID))))
buffer.PutBytes([]byte(peerID)) buffer.PutBytes([]byte(peerID))
buffer.PutUint32(uint32(reason))
buffer.Flip() buffer.Flip()
server.WriteBinary(buffer.Bytes()) server.WriteBinary(buffer.Bytes())
} }

View File

@@ -20,7 +20,7 @@ func LogInfoMessage(message string) {
fmt.Printf("%s[g365sfu] %s[%s]%s %s[INFO]%s %s\n", fmt.Printf("%s[g365sfu] %s[%s]%s %s[INFO]%s %s\n",
colorBlue, colorBlue,
colorGray, timestamp, colorReset, colorGray, timestamp, colorReset,
colorGreen, colorReset, colorCyan, colorReset,
message, message,
) )
} }
@@ -60,7 +60,7 @@ func LogSuccessMessage(message string) {
fmt.Printf("%s[g365sfu] %s[%s]%s %s[SUCCESS]%s %s\n", fmt.Printf("%s[g365sfu] %s[%s]%s %s[SUCCESS]%s %s\n",
colorBlue, colorBlue,
colorGray, timestamp, colorReset, colorGray, timestamp, colorReset,
colorCyan, colorReset, colorGreen, colorReset,
message, message,
) )
} }

View File

@@ -90,6 +90,7 @@ func BindPeerLifecycle(roomID, peerID string, pc *webrtc.PeerConnection) {
mu sync.Mutex mu sync.Mutex
disconnecting bool disconnecting bool
timer *time.Timer timer *time.Timer
leaveOnce sync.Once
) )
room, exists := GetRoom(roomID) room, exists := GetRoom(roomID)
@@ -99,6 +100,26 @@ func BindPeerLifecycle(roomID, peerID string, pc *webrtc.PeerConnection) {
} }
server := room.Server server := room.Server
cancelTimer := func() {
mu.Lock()
defer mu.Unlock()
if timer != nil {
timer.Stop()
timer = nil
}
disconnecting = false
}
leaveAndNotify := func(reason DisconnectReason) {
leaveOnce.Do(func() {
cancelTimer()
err := LeaveRoom(roomID, peerID)
if OnPeerDisconnected != nil && err == nil {
OnPeerDisconnected(roomID, peerID, server, reason)
}
})
}
startTimer := func() { startTimer := func() {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@@ -114,35 +135,20 @@ func BindPeerLifecycle(roomID, peerID string, pc *webrtc.PeerConnection) {
mu.Unlock() mu.Unlock()
return return
} }
_ = LeaveRoom(roomID, peerID) leaveAndNotify(DisconnectReasonFailed)
if OnPeerDisconnected != nil {
OnPeerDisconnected(roomID, peerID, server)
}
}) })
} }
cancelTimer := func() {
mu.Lock()
defer mu.Unlock()
if timer != nil {
timer.Stop()
timer = nil
}
disconnecting = false
}
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
switch state { switch state {
case webrtc.ICEConnectionStateConnected, webrtc.ICEConnectionStateCompleted: case webrtc.ICEConnectionStateConnected, webrtc.ICEConnectionStateCompleted:
cancelTimer() cancelTimer()
case webrtc.ICEConnectionStateDisconnected: case webrtc.ICEConnectionStateDisconnected:
startTimer() startTimer()
case webrtc.ICEConnectionStateFailed, webrtc.ICEConnectionStateClosed: case webrtc.ICEConnectionStateClosed:
cancelTimer() leaveAndNotify(DisconnectReasonClosed)
_ = LeaveRoom(roomID, peerID) case webrtc.ICEConnectionStateFailed:
if OnPeerDisconnected != nil { leaveAndNotify(DisconnectReasonFailed)
OnPeerDisconnected(roomID, peerID, server)
}
} }
}) })
@@ -152,16 +158,15 @@ func BindPeerLifecycle(roomID, peerID string, pc *webrtc.PeerConnection) {
cancelTimer() cancelTimer()
case webrtc.PeerConnectionStateDisconnected: case webrtc.PeerConnectionStateDisconnected:
startTimer() startTimer()
case webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed: case webrtc.PeerConnectionStateClosed:
cancelTimer() leaveAndNotify(DisconnectReasonClosed)
_ = LeaveRoom(roomID, peerID) case webrtc.PeerConnectionStateFailed:
if OnPeerDisconnected != nil { leaveAndNotify(DisconnectReasonFailed)
OnPeerDisconnected(roomID, peerID, server)
}
} }
}) })
} }
// Вызывается при JoinWithOffer для ретрансляции RTP пакетов от издателя к другим участникам комнаты
// Вызывается при JoinWithOffer для ретрансляции RTP пакетов от издателя к другим участникам комнаты // Вызывается при JoinWithOffer для ретрансляции RTP пакетов от издателя к другим участникам комнаты
func SetupForwardingForPeer(roomID string, publisherPeerID string, publisherPC *webrtc.PeerConnection) { func SetupForwardingForPeer(roomID string, publisherPeerID string, publisherPC *webrtc.PeerConnection) {
publisherPC.OnTrack(func(remote *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { publisherPC.OnTrack(func(remote *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
@@ -178,7 +183,7 @@ func SetupForwardingForPeer(roomID string, publisherPeerID string, publisherPC *
remote.StreamID(), remote.StreamID(),
) )
if err != nil { if err != nil {
logger.LogErrorMessage("SetupForwardingForPeer: NewTrackLocalStaticRTP error") logger.LogErrorMessage("SetupForwardingForPeer: NewTrackLocalStaticRTP error: " + err.Error())
return return
} }
defer removeRoomTrack(roomID, localTrack.ID()) defer removeRoomTrack(roomID, localTrack.ID())
@@ -203,32 +208,35 @@ func SetupForwardingForPeer(roomID string, publisherPeerID string, publisherPC *
continue continue
} }
// Не трогаем закрытые/failed соединения
if !isPeerConnectionAlive(sub.PeerConnection) { if !isPeerConnectionAlive(sub.PeerConnection) {
fmt.Println("SetupForwardingForPeer: skipping dead peer:", sub.PeerID,
sub.PeerConnection.ConnectionState().String())
continue continue
} }
sender, err := sub.PeerConnection.AddTrack(localTrack) sender, err := sub.PeerConnection.AddTrack(localTrack)
if err != nil { if err != nil {
fmt.Println("SetupForwardingForPeer: AddTrack error:", roomID, sub.PeerID, err) logger.LogWarnMessage("SetupForwardingForPeer: AddTrack error: " + sub.PeerID + " " + err.Error())
continue continue
} }
// RTCP drain senderCopy := sender
go func() { go func() {
buf := make([]byte, 1500) buf := make([]byte, 1500)
for { for {
if _, _, e := sender.Read(buf); e != nil { if _, _, e := senderCopy.Read(buf); e != nil {
return return
} }
} }
}() }()
if err = renegotiatePeer(roomID, sub.PeerID, sub.PeerConnection); err != nil { subID := sub.PeerID
fmt.Println("SetupForwardingForPeer: renegotiatePeer error:", roomID, sub.PeerID, err) subPC := sub.PeerConnection
go func() {
logger.LogInfoMessage("SetupForwardingForPeer: starting renegotiation for peer=" + subID)
if err := renegotiatePeer(roomID, subID, subPC); err != nil {
logger.LogWarnMessage("SetupForwardingForPeer: renegotiatePeer error: " + subID + " " + err.Error())
} }
}()
} }
// Для video просим keyframe // Для video просим keyframe
@@ -245,11 +253,11 @@ func SetupForwardingForPeer(roomID string, publisherPeerID string, publisherPC *
if err == io.EOF { if err == io.EOF {
return return
} }
fmt.Println("SetupForwardingForPeer: ReadRTP error:", err) logger.LogWarnMessage("SetupForwardingForPeer: ReadRTP error: " + err.Error())
return return
} }
if err = localTrack.WriteRTP(pkt); err != nil { if err = localTrack.WriteRTP(pkt); err != nil {
fmt.Println("SetupForwardingForPeer: WriteRTP error:", err) logger.LogWarnMessage("SetupForwardingForPeer: WriteRTP error: " + err.Error())
return return
} }
} }

11
sfu/reason.go Normal file
View File

@@ -0,0 +1,11 @@
package sfu
// Причины отключения пира от комнаты, которые могут быть использованы для логирования или уведомлений
type DisconnectReason int
const (
// Пир отключился из-за ошибки соединения или другой проблемы
DisconnectReasonFailed DisconnectReason = 0
// Пир отключился по своей инициативе (например, закрыл приложение)
DisconnectReasonClosed = 1
)

View File

@@ -1,6 +1,7 @@
package sfu package sfu
import ( import (
"g365sfu/logger"
connection "g365sfu/socket/struct" connection "g365sfu/socket/struct"
"sync" "sync"
@@ -76,43 +77,9 @@ func JoinWithOffer(roomID string, peerID string, offer webrtc.SessionDescription
return nil, err return nil, err
} }
peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) {
if c == nil {
return
}
if OnLocalICECandidate != nil {
OnLocalICECandidate(roomID, peerID, c.ToJSON())
}
})
BindPeerLifecycle(roomID, peerID, peerConnection) BindPeerLifecycle(roomID, peerID, peerConnection)
SetupForwardingForPeer(roomID, peerID, peerConnection) SetupForwardingForPeer(roomID, peerID, peerConnection)
room.mu.RLock()
existingTracks := make([]RoomTrack, len(room.Tracks))
copy(existingTracks, room.Tracks)
room.mu.RUnlock()
for _, t := range existingTracks {
if t.OwnerPeer == peerID {
continue
}
sender, err := peerConnection.AddTrack(t.Local)
if err != nil {
continue
}
go func() {
buf := make([]byte, 1500)
for {
if _, _, e := sender.Read(buf); e != nil {
return
}
}
}()
}
if err = peerConnection.SetRemoteDescription(offer); err != nil { if err = peerConnection.SetRemoteDescription(offer); err != nil {
_ = peerConnection.Close() _ = peerConnection.Close()
return nil, err return nil, err
@@ -131,13 +98,57 @@ func JoinWithOffer(roomID string, peerID string, offer webrtc.SessionDescription
} }
<-gatherDone <-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.mu.Lock()
room.Peers = append(room.Peers, Peer{ room.Peers = append(room.Peers, Peer{
PeerID: peerID, PeerID: peerID,
PeerConnection: peerConnection, PeerConnection: peerConnection,
}) })
existingTracks := make([]RoomTrack, len(room.Tracks))
copy(existingTracks, room.Tracks)
room.mu.Unlock() 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 return peerConnection.LocalDescription(), nil
} }

View File

@@ -23,7 +23,7 @@ var OnServerOffer func(roomID string, peerID string, offer webrtc.SessionDescrip
var OnLocalICECandidate func(roomID, peerID string, candidate webrtc.ICECandidateInit) var OnLocalICECandidate func(roomID, peerID string, candidate webrtc.ICECandidateInit)
// Коллбек для обработки отключения пира (обрыв связи) // Коллбек для обработки отключения пира (обрыв связи)
var OnPeerDisconnected func(roomID, peerID string, server *connection.Connection) var OnPeerDisconnected func(roomID, peerID string, server *connection.Connection, reason DisconnectReason)
// Коллбек для обработки удаления комнаты // Коллбек для обработки удаления комнаты
var OnRoomDelete func(roomID string, server *connection.Connection) var OnRoomDelete func(roomID string, server *connection.Connection)