You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
769 lines
18 KiB
769 lines
18 KiB
package jazz
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/cacggghp/vk-turn-proxy/internal/namegen"
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/pion/webrtc/v4"
|
|
)
|
|
|
|
const (
|
|
dataChannelLabel = "_reliable"
|
|
maxDataChannelMessageSize = 12288
|
|
)
|
|
|
|
var debugLogging atomic.Bool
|
|
|
|
type Peer struct {
|
|
roomInput string
|
|
name string
|
|
onData func([]byte)
|
|
|
|
roomMu sync.Mutex
|
|
roomInfo *RoomInfo
|
|
|
|
sessionMu sync.RWMutex
|
|
session *peerSession
|
|
|
|
reconnectCbMu sync.RWMutex
|
|
reconnectCb func()
|
|
wsMu sync.Mutex
|
|
sendMu sync.Mutex
|
|
reconnectCh chan struct{}
|
|
closeCh chan struct{}
|
|
closed atomic.Bool
|
|
}
|
|
|
|
type peerSession struct {
|
|
roomInfo *RoomInfo
|
|
ws *websocket.Conn
|
|
pcSub *webrtc.PeerConnection
|
|
pcPub *webrtc.PeerConnection
|
|
dc *webrtc.DataChannel
|
|
groupID string
|
|
connected atomic.Bool
|
|
}
|
|
|
|
func NewPeer(roomInput, name string, onData func([]byte)) *Peer {
|
|
return &Peer{
|
|
roomInput: roomInput,
|
|
name: name,
|
|
onData: onData,
|
|
reconnectCh: make(chan struct{}, 1),
|
|
closeCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func NewConnectedPeer(ctx context.Context, roomInput string, onData func([]byte), onReconnect func()) (*Peer, error) {
|
|
peer := NewPeer(roomInput, namegen.Generate(), onData)
|
|
if onReconnect != nil {
|
|
peer.SetReconnectCallback(onReconnect)
|
|
}
|
|
if err := peer.Connect(ctx); err != nil {
|
|
if closeErr := peer.Close(); closeErr != nil {
|
|
log.Printf("SaluteJazz DataChannel peer cleanup failed after connect error: %v", closeErr)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
go peer.WatchConnection(ctx)
|
|
|
|
return peer, nil
|
|
}
|
|
|
|
func SetDebug(enabled bool) {
|
|
debugLogging.Store(enabled)
|
|
}
|
|
|
|
func DebugEnabled() bool {
|
|
return debugLogging.Load()
|
|
}
|
|
|
|
func debugf(format string, args ...any) {
|
|
if DebugEnabled() {
|
|
log.Printf(format, args...)
|
|
}
|
|
}
|
|
|
|
func (p *Peer) Connect(ctx context.Context) error {
|
|
return p.connectOnce(ctx)
|
|
}
|
|
|
|
func (p *Peer) WatchConnection(ctx context.Context) {
|
|
const (
|
|
maxReconnects = 10
|
|
reconnectWindow = 5 * time.Minute
|
|
)
|
|
|
|
var lastReconnect time.Time
|
|
reconnectCount := 0
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-p.closeCh:
|
|
return
|
|
case <-p.reconnectCh:
|
|
}
|
|
|
|
now := time.Now()
|
|
if now.Sub(lastReconnect) > reconnectWindow {
|
|
reconnectCount = 0
|
|
}
|
|
if reconnectCount >= maxReconnects {
|
|
log.Printf("SaluteJazz DataChannel: max reconnect attempts (%d) reached", maxReconnects)
|
|
return
|
|
}
|
|
reconnectCount++
|
|
lastReconnect = now
|
|
|
|
backoff := time.Duration(reconnectCount) * 2 * time.Second
|
|
if backoff > 30*time.Second {
|
|
backoff = 30 * time.Second
|
|
}
|
|
|
|
for {
|
|
if ctx.Err() != nil || p.closed.Load() {
|
|
return
|
|
}
|
|
|
|
debugf("SaluteJazz DataChannel: reconnecting in %v", backoff)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-p.closeCh:
|
|
return
|
|
case <-time.After(backoff):
|
|
}
|
|
|
|
if err := p.reconnect(ctx); err != nil {
|
|
debugf("SaluteJazz DataChannel: reconnect failed: %v", err)
|
|
continue
|
|
}
|
|
|
|
debugf("SaluteJazz DataChannel: reconnected")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Peer) Send(data []byte) error {
|
|
if len(data) > maxDataChannelMessageSize {
|
|
return fmt.Errorf("datachannel message too large: %d > %d", len(data), maxDataChannelMessageSize)
|
|
}
|
|
|
|
p.sessionMu.RLock()
|
|
session := p.session
|
|
p.sessionMu.RUnlock()
|
|
|
|
if session == nil || session.dc == nil || session.dc.ReadyState() != webrtc.DataChannelStateOpen {
|
|
return fmt.Errorf("datachannel not ready")
|
|
}
|
|
|
|
p.sendMu.Lock()
|
|
defer p.sendMu.Unlock()
|
|
|
|
encoded := EncodeDataPacket(data)
|
|
start := time.Now()
|
|
for session.dc.BufferedAmount() > 256*1024 {
|
|
if time.Since(start) > 2*time.Second {
|
|
return fmt.Errorf("datachannel buffer is full")
|
|
}
|
|
if session.dc.ReadyState() != webrtc.DataChannelStateOpen {
|
|
return fmt.Errorf("datachannel not ready")
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
return session.dc.Send(encoded)
|
|
}
|
|
|
|
func (p *Peer) Close() error {
|
|
if p.closed.Swap(true) {
|
|
return nil
|
|
}
|
|
|
|
select {
|
|
case <-p.closeCh:
|
|
default:
|
|
close(p.closeCh)
|
|
}
|
|
|
|
p.cleanupCurrentSession()
|
|
return nil
|
|
}
|
|
|
|
func (p *Peer) SetReconnectCallback(cb func()) {
|
|
p.reconnectCbMu.Lock()
|
|
p.reconnectCb = cb
|
|
p.reconnectCbMu.Unlock()
|
|
}
|
|
|
|
func (p *Peer) reconnect(ctx context.Context) error {
|
|
p.cleanupCurrentSession()
|
|
if err := p.connectOnce(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.reconnectCbMu.RLock()
|
|
cb := p.reconnectCb
|
|
p.reconnectCbMu.RUnlock()
|
|
if cb != nil {
|
|
cb()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Peer) connectOnce(ctx context.Context) error {
|
|
roomInfo, err := p.connectionInfo(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config := webrtc.Configuration{
|
|
ICEServers: []webrtc.ICEServer{},
|
|
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
|
|
BundlePolicy: webrtc.BundlePolicyMaxBundle,
|
|
}
|
|
|
|
api := webrtc.NewAPI()
|
|
|
|
pcSub, err := api.NewPeerConnection(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pcPub, err := api.NewPeerConnection(config)
|
|
if err != nil {
|
|
_ = pcSub.Close()
|
|
return err
|
|
}
|
|
|
|
session := &peerSession{
|
|
roomInfo: roomInfo,
|
|
pcSub: pcSub,
|
|
pcPub: pcPub,
|
|
}
|
|
|
|
pcSub.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
|
debugf("SaluteJazz subscriber state: %s", state.String())
|
|
if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateDisconnected {
|
|
p.signalReconnectIfCurrent(session, "subscriber state "+state.String())
|
|
}
|
|
})
|
|
pcPub.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
|
debugf("SaluteJazz publisher state: %s", state.String())
|
|
if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateDisconnected {
|
|
p.signalReconnectIfCurrent(session, "publisher state "+state.String())
|
|
}
|
|
})
|
|
|
|
dc, err := pcPub.CreateDataChannel(dataChannelLabel, &webrtc.DataChannelInit{
|
|
Ordered: ptr(true),
|
|
})
|
|
if err != nil {
|
|
p.cleanupSession(session)
|
|
return err
|
|
}
|
|
session.dc = dc
|
|
|
|
dcReady := make(chan struct{}, 1)
|
|
dc.OnOpen(func() {
|
|
if session.connected.CompareAndSwap(false, true) {
|
|
log.Printf("SaluteJazz DataChannel connected")
|
|
}
|
|
select {
|
|
case dcReady <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
dc.OnClose(func() {
|
|
p.signalReconnectIfCurrent(session, "publisher datachannel closed")
|
|
})
|
|
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
|
p.handleIncomingMessage(msg.Data)
|
|
})
|
|
|
|
pcSub.OnDataChannel(func(remoteDC *webrtc.DataChannel) {
|
|
if remoteDC.Label() != dataChannelLabel {
|
|
debugf("SaluteJazz remote datachannel ignored: %s", remoteDC.Label())
|
|
return
|
|
}
|
|
debugf("SaluteJazz remote datachannel opened: %s", remoteDC.Label())
|
|
remoteDC.OnClose(func() {
|
|
p.signalReconnectIfCurrent(session, "remote datachannel "+remoteDC.Label()+" closed")
|
|
})
|
|
remoteDC.OnMessage(func(msg webrtc.DataChannelMessage) {
|
|
p.handleIncomingMessage(msg.Data)
|
|
})
|
|
})
|
|
|
|
ws, resp, err := websocket.DefaultDialer.Dial(roomInfo.ConnectorURL, nil)
|
|
if err != nil {
|
|
if resp != nil && resp.Body != nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
p.cleanupSession(session)
|
|
return err
|
|
}
|
|
if resp != nil && resp.Body != nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
session.ws = ws
|
|
|
|
ws.SetPongHandler(func(string) error {
|
|
return ws.SetReadDeadline(time.Now().Add(60 * time.Second))
|
|
})
|
|
if err := ws.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil {
|
|
p.cleanupSession(session)
|
|
return err
|
|
}
|
|
|
|
p.sessionMu.Lock()
|
|
p.session = session
|
|
p.sessionMu.Unlock()
|
|
|
|
go p.keepAlive(session)
|
|
go p.handleSignaling(session)
|
|
|
|
if err := p.sendJoin(session); err != nil {
|
|
p.cleanupCurrentSession()
|
|
return err
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
p.cleanupCurrentSession()
|
|
return ctx.Err()
|
|
case <-p.closeCh:
|
|
p.cleanupCurrentSession()
|
|
return context.Canceled
|
|
case <-time.After(30 * time.Second):
|
|
p.cleanupCurrentSession()
|
|
return fmt.Errorf("datachannel timeout")
|
|
case <-dcReady:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (p *Peer) connectionInfo(ctx context.Context) (*RoomInfo, error) {
|
|
p.roomMu.Lock()
|
|
defer p.roomMu.Unlock()
|
|
|
|
if p.roomInfo != nil {
|
|
roomInfo, err := JoinRoom(ctx, p.roomInfo.RoomID+":"+p.roomInfo.Password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("refresh room connection: %w", err)
|
|
}
|
|
p.roomInfo.ConnectorURL = roomInfo.ConnectorURL
|
|
return p.roomInfo, nil
|
|
}
|
|
|
|
roomID, _ := parseRoomInput(p.roomInput)
|
|
if roomID == "" || roomID == "any" || roomID == "dummy" {
|
|
roomInfo, err := CreateRoom(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create room: %w", err)
|
|
}
|
|
p.roomInfo = roomInfo
|
|
log.Printf("SaluteJazz room created: %s:%s", roomInfo.RoomID, roomInfo.Password)
|
|
return roomInfo, nil
|
|
}
|
|
|
|
roomInfo, err := JoinRoom(ctx, p.roomInput)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("join room: %w", err)
|
|
}
|
|
p.roomInfo = roomInfo
|
|
log.Printf("SaluteJazz joining room: %s", roomInfo.RoomID)
|
|
return roomInfo, nil
|
|
}
|
|
|
|
func (p *Peer) keepAlive(session *peerSession) {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-p.closeCh:
|
|
return
|
|
case <-ticker.C:
|
|
if err := p.writeControl(session, websocket.PingMessage, []byte{}); err != nil {
|
|
p.signalReconnectIfCurrent(session, "websocket ping failed")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handleIncomingMessage(data []byte) {
|
|
payload, ok := DecodeDataPacket(data)
|
|
if !ok {
|
|
debugf("SaluteJazz DataChannel: failed to decode packet, using raw payload")
|
|
payload = data
|
|
}
|
|
|
|
if p.onData != nil && len(payload) > 0 {
|
|
p.onData(payload)
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handleSignaling(session *peerSession) {
|
|
for {
|
|
var msg map[string]any
|
|
if err := session.ws.ReadJSON(&msg); err != nil {
|
|
if p.isCurrentSession(session) && !p.closed.Load() {
|
|
debugf("SaluteJazz signaling read error: %v", err)
|
|
p.signalReconnectIfCurrent(session, "signaling read failed")
|
|
}
|
|
return
|
|
}
|
|
if err := session.ws.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil {
|
|
if p.isCurrentSession(session) && !p.closed.Load() {
|
|
debugf("SaluteJazz signaling deadline update failed: %v", err)
|
|
p.signalReconnectIfCurrent(session, "signaling deadline update failed")
|
|
}
|
|
return
|
|
}
|
|
|
|
event := stringValue(msg, "event")
|
|
payload := mapValue(msg, "payload")
|
|
|
|
switch event {
|
|
case "join-response":
|
|
p.handleJoinResponse(session, payload)
|
|
case "media-out":
|
|
p.handleMediaOut(session, payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handleJoinResponse(session *peerSession, payload map[string]any) {
|
|
group := mapValue(payload, "participantGroup")
|
|
session.groupID = stringValue(group, "groupId")
|
|
debugf("SaluteJazz peer joined: groupId=%s", session.groupID)
|
|
}
|
|
|
|
func (p *Peer) handleMediaOut(session *peerSession, payload map[string]any) {
|
|
method := stringValue(payload, "method")
|
|
|
|
switch method {
|
|
case "rtc:config":
|
|
p.handleRTCConfig(session, payload)
|
|
case "rtc:offer":
|
|
p.handleSubscriberOffer(session, payload)
|
|
case "rtc:answer":
|
|
p.handlePublisherAnswer(session, payload)
|
|
case "rtc:ice":
|
|
p.handleICE(session, payload)
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handleRTCConfig(session *peerSession, payload map[string]any) {
|
|
config := mapValue(payload, "configuration")
|
|
servers := sliceValue(config, "iceServers")
|
|
|
|
iceServers := make([]webrtc.ICEServer, 0, len(servers))
|
|
for _, rawServer := range servers {
|
|
server, ok := rawServer.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
rawURLs := sliceValue(server, "urls")
|
|
username := stringValue(server, "username")
|
|
credential := stringValue(server, "credential")
|
|
|
|
urls := make([]string, 0, len(rawURLs))
|
|
for _, rawURL := range rawURLs {
|
|
if url, ok := rawURL.(string); ok && url != "" {
|
|
urls = append(urls, url)
|
|
}
|
|
}
|
|
if len(urls) > 0 {
|
|
iceServers = append(iceServers, webrtc.ICEServer{
|
|
URLs: urls,
|
|
Username: username,
|
|
Credential: credential,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(iceServers) == 0 {
|
|
return
|
|
}
|
|
|
|
newConfig := webrtc.Configuration{
|
|
ICEServers: iceServers,
|
|
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
|
|
BundlePolicy: webrtc.BundlePolicyMaxBundle,
|
|
}
|
|
if err := session.pcSub.SetConfiguration(newConfig); err != nil {
|
|
debugf("SaluteJazz subscriber SetConfiguration failed: %v", err)
|
|
}
|
|
if err := session.pcPub.SetConfiguration(newConfig); err != nil {
|
|
debugf("SaluteJazz publisher SetConfiguration failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handleSubscriberOffer(session *peerSession, payload map[string]any) {
|
|
desc := mapValue(payload, "description")
|
|
sdp := stringValue(desc, "sdp")
|
|
if sdp == "" {
|
|
debugf("SaluteJazz subscriber offer missing SDP")
|
|
return
|
|
}
|
|
|
|
if err := session.pcSub.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: sdp}); err != nil {
|
|
debugf("SaluteJazz subscriber SetRemoteDescription failed: %v", err)
|
|
return
|
|
}
|
|
|
|
answer, err := session.pcSub.CreateAnswer(nil)
|
|
if err != nil {
|
|
debugf("SaluteJazz subscriber CreateAnswer failed: %v", err)
|
|
return
|
|
}
|
|
if err := session.pcSub.SetLocalDescription(answer); err != nil {
|
|
debugf("SaluteJazz subscriber SetLocalDescription failed: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := p.writeJSON(session, map[string]any{
|
|
"roomId": session.roomInfo.RoomID,
|
|
"event": "media-in",
|
|
"groupId": session.groupID,
|
|
"requestId": uuid.New().String(),
|
|
"payload": map[string]any{
|
|
"method": "rtc:answer",
|
|
"description": map[string]any{
|
|
"type": "answer",
|
|
"sdp": answer.SDP,
|
|
},
|
|
},
|
|
}); err != nil {
|
|
debugf("SaluteJazz subscriber answer send failed: %v", err)
|
|
return
|
|
}
|
|
|
|
time.Sleep(300 * time.Millisecond)
|
|
p.sendPublisherOffer(session)
|
|
}
|
|
|
|
func (p *Peer) sendPublisherOffer(session *peerSession) {
|
|
offer, err := session.pcPub.CreateOffer(nil)
|
|
if err != nil {
|
|
debugf("SaluteJazz publisher CreateOffer failed: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := session.pcPub.SetLocalDescription(offer); err != nil {
|
|
debugf("SaluteJazz publisher SetLocalDescription failed: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := p.writeJSON(session, map[string]any{
|
|
"roomId": session.roomInfo.RoomID,
|
|
"event": "media-in",
|
|
"groupId": session.groupID,
|
|
"requestId": uuid.New().String(),
|
|
"payload": map[string]any{
|
|
"method": "rtc:offer",
|
|
"description": map[string]any{
|
|
"type": "offer",
|
|
"sdp": offer.SDP,
|
|
},
|
|
},
|
|
}); err != nil {
|
|
debugf("SaluteJazz publisher offer send failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handlePublisherAnswer(session *peerSession, payload map[string]any) {
|
|
desc := mapValue(payload, "description")
|
|
sdp := stringValue(desc, "sdp")
|
|
if sdp == "" {
|
|
debugf("SaluteJazz publisher answer missing SDP")
|
|
return
|
|
}
|
|
if err := session.pcPub.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: sdp}); err != nil {
|
|
debugf("SaluteJazz publisher SetRemoteDescription failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func (p *Peer) handleICE(session *peerSession, payload map[string]any) {
|
|
candidates := sliceValue(payload, "rtcIceCandidates")
|
|
|
|
for _, rawCandidate := range candidates {
|
|
cand, ok := rawCandidate.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
candidate := stringValue(cand, "candidate")
|
|
target := stringValue(cand, "target")
|
|
sdpMid := stringValue(cand, "sdpMid")
|
|
sdpMLineIndex, ok := floatValue(cand, "sdpMLineIndex")
|
|
if candidate == "" || target == "" || !ok {
|
|
continue
|
|
}
|
|
|
|
index := uint16(sdpMLineIndex)
|
|
init := webrtc.ICECandidateInit{
|
|
Candidate: candidate,
|
|
SDPMid: &sdpMid,
|
|
SDPMLineIndex: &index,
|
|
}
|
|
|
|
switch target {
|
|
case "SUBSCRIBER":
|
|
if err := session.pcSub.AddICECandidate(init); err != nil {
|
|
debugf("SaluteJazz subscriber ICE apply failed: %v", err)
|
|
}
|
|
case "PUBLISHER":
|
|
if err := session.pcPub.AddICECandidate(init); err != nil {
|
|
debugf("SaluteJazz publisher ICE apply failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Peer) sendJoin(session *peerSession) error {
|
|
return p.writeJSON(session, map[string]any{
|
|
"roomId": session.roomInfo.RoomID,
|
|
"event": "join",
|
|
"requestId": uuid.New().String(),
|
|
"payload": map[string]any{
|
|
"password": session.roomInfo.Password,
|
|
"participantName": p.name,
|
|
"supportedFeatures": map[string]any{
|
|
"attachedRooms": true,
|
|
"sessionGroups": true,
|
|
"transcription": true,
|
|
},
|
|
"isSilent": false,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Peer) writeJSON(session *peerSession, payload any) error {
|
|
p.wsMu.Lock()
|
|
defer p.wsMu.Unlock()
|
|
|
|
if session.ws == nil {
|
|
return fmt.Errorf("websocket is closed")
|
|
}
|
|
|
|
return session.ws.WriteJSON(payload)
|
|
}
|
|
|
|
func (p *Peer) writeControl(session *peerSession, messageType int, data []byte) error {
|
|
p.wsMu.Lock()
|
|
defer p.wsMu.Unlock()
|
|
|
|
if session.ws == nil {
|
|
return fmt.Errorf("websocket is closed")
|
|
}
|
|
|
|
return session.ws.WriteControl(messageType, data, time.Now().Add(10*time.Second))
|
|
}
|
|
|
|
func (p *Peer) cleanupCurrentSession() {
|
|
p.sessionMu.Lock()
|
|
session := p.session
|
|
p.session = nil
|
|
p.sessionMu.Unlock()
|
|
|
|
p.cleanupSession(session)
|
|
}
|
|
|
|
func (p *Peer) cleanupSession(session *peerSession) {
|
|
if session == nil {
|
|
return
|
|
}
|
|
|
|
if session.connected.Swap(false) {
|
|
log.Printf("SaluteJazz DataChannel closed")
|
|
}
|
|
|
|
if session.dc != nil {
|
|
_ = session.dc.Close()
|
|
}
|
|
if session.pcPub != nil {
|
|
_ = session.pcPub.Close()
|
|
}
|
|
if session.pcSub != nil {
|
|
_ = session.pcSub.Close()
|
|
}
|
|
if session.ws != nil {
|
|
p.wsMu.Lock()
|
|
if err := session.ws.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second)); err != nil {
|
|
debugf("SaluteJazz websocket close control failed: %v", err)
|
|
}
|
|
_ = session.ws.Close()
|
|
p.wsMu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (p *Peer) isCurrentSession(session *peerSession) bool {
|
|
p.sessionMu.RLock()
|
|
defer p.sessionMu.RUnlock()
|
|
return p.session == session
|
|
}
|
|
|
|
func (p *Peer) signalReconnectIfCurrent(session *peerSession, reason string) {
|
|
if p.closed.Load() || !p.isCurrentSession(session) {
|
|
return
|
|
}
|
|
|
|
if session.connected.Swap(false) {
|
|
log.Printf("SaluteJazz DataChannel closed")
|
|
}
|
|
if reason != "" {
|
|
debugf("SaluteJazz DataChannel disconnect reason: %s", reason)
|
|
}
|
|
|
|
select {
|
|
case p.reconnectCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func ptr[T any](value T) *T {
|
|
return &value
|
|
}
|
|
|
|
func stringValue(values map[string]any, key string) string {
|
|
value, ok := values[key].(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
func mapValue(values map[string]any, key string) map[string]any {
|
|
value, ok := values[key].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func sliceValue(values map[string]any, key string) []any {
|
|
value, ok := values[key].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func floatValue(values map[string]any, key string) (float64, bool) {
|
|
value, ok := values[key].(float64)
|
|
return value, ok
|
|
}
|
|
|