15 changed files with 1547 additions and 237 deletions
@ -0,0 +1,88 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"log" |
||||
|
"net" |
||||
|
"sync/atomic" |
||||
|
|
||||
|
"github.com/cacggghp/vk-turn-proxy/internal/jazz" |
||||
|
"github.com/cacggghp/vk-turn-proxy/internal/telemost" |
||||
|
) |
||||
|
|
||||
|
type dataChannelPeer interface { |
||||
|
Send([]byte) error |
||||
|
Close() error |
||||
|
} |
||||
|
|
||||
|
type dataChannelConnectFunc func(context.Context, string, func([]byte), func()) (dataChannelPeer, error) |
||||
|
|
||||
|
func connectTelemostDataChannelPeer(ctx context.Context, room string, onData func([]byte), onReconnect func()) (dataChannelPeer, error) { |
||||
|
return telemost.NewConnectedPeer(ctx, room, onData, onReconnect) |
||||
|
} |
||||
|
|
||||
|
func connectJazzDataChannelPeer(ctx context.Context, room string, onData func([]byte), onReconnect func()) (dataChannelPeer, error) { |
||||
|
return jazz.NewConnectedPeer(ctx, room, onData, onReconnect) |
||||
|
} |
||||
|
|
||||
|
func runTelemostDataChannelMode(ctx context.Context, inviteLink, listenAddr string) error { |
||||
|
return runDataChannelMode(ctx, "Telemost", connectTelemostDataChannelPeer, inviteLink, listenAddr) |
||||
|
} |
||||
|
|
||||
|
func runJazzDataChannelMode(ctx context.Context, room, listenAddr string) error { |
||||
|
return runDataChannelMode(ctx, "SaluteJazz", connectJazzDataChannelPeer, room, listenAddr) |
||||
|
} |
||||
|
|
||||
|
func runDataChannelMode(ctx context.Context, providerName string, connectPeer dataChannelConnectFunc, room, listenAddr string) error { |
||||
|
listenConn, err := net.ListenPacket("udp", listenAddr) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer func(listenConn net.PacketConn) { |
||||
|
err := listenConn.Close() |
||||
|
if err != nil { |
||||
|
log.Println(err) |
||||
|
} |
||||
|
}(listenConn) |
||||
|
|
||||
|
var activeLocalPeer atomic.Value |
||||
|
peer, err := connectPeer(ctx, room, func(data []byte) { |
||||
|
addr, ok := activeLocalPeer.Load().(net.Addr) |
||||
|
if !ok || addr == nil { |
||||
|
return |
||||
|
} |
||||
|
if _, writeErr := listenConn.WriteTo(data, addr); writeErr != nil { |
||||
|
log.Printf("%s DataChannel: failed to write local packet: %v", providerName, writeErr) |
||||
|
} |
||||
|
}, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer func(peer dataChannelPeer) { |
||||
|
err := peer.Close() |
||||
|
if err != nil { |
||||
|
log.Println(err) |
||||
|
} |
||||
|
}(peer) |
||||
|
|
||||
|
closeOnContextDone(ctx, listenConn) |
||||
|
|
||||
|
log.Printf("%s DataChannel mode: listening on %s", providerName, listenAddr) |
||||
|
|
||||
|
buf := make([]byte, 2048) |
||||
|
for { |
||||
|
n, addr, err := listenConn.ReadFrom(buf) |
||||
|
if err != nil { |
||||
|
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) { |
||||
|
return nil |
||||
|
} |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
activeLocalPeer.Store(addr) |
||||
|
if err := peer.Send(buf[:n]); err != nil { |
||||
|
log.Printf("%s DataChannel: dropped outbound packet (%d bytes): %v", providerName, n, err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,64 +0,0 @@ |
|||||
package main |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"errors" |
|
||||
"log" |
|
||||
"net" |
|
||||
"sync/atomic" |
|
||||
|
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/telemost" |
|
||||
) |
|
||||
|
|
||||
func runTelemostDataChannelMode(ctx context.Context, inviteLink, listenAddr string) error { |
|
||||
listenConn, err := net.ListenPacket("udp", listenAddr) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
defer func(listenConn net.PacketConn) { |
|
||||
err := listenConn.Close() |
|
||||
if err != nil { |
|
||||
log.Println(err) |
|
||||
} |
|
||||
}(listenConn) |
|
||||
|
|
||||
var activeLocalPeer atomic.Value |
|
||||
peer, err := telemost.NewConnectedPeer(ctx, inviteLink, func(data []byte) { |
|
||||
addr, ok := activeLocalPeer.Load().(net.Addr) |
|
||||
if !ok || addr == nil { |
|
||||
return |
|
||||
} |
|
||||
if _, writeErr := listenConn.WriteTo(data, addr); writeErr != nil { |
|
||||
log.Printf("Telemost DataChannel: failed to write local packet: %v", writeErr) |
|
||||
} |
|
||||
}, nil) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
defer func(peer *telemost.Peer) { |
|
||||
err := peer.Close() |
|
||||
if err != nil { |
|
||||
log.Println(err) |
|
||||
} |
|
||||
}(peer) |
|
||||
|
|
||||
closeOnContextDone(ctx, listenConn) |
|
||||
|
|
||||
log.Printf("Telemost DataChannel mode: listening on %s", listenAddr) |
|
||||
|
|
||||
buf := make([]byte, 2048) |
|
||||
for { |
|
||||
n, addr, err := listenConn.ReadFrom(buf) |
|
||||
if err != nil { |
|
||||
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) { |
|
||||
return nil |
|
||||
} |
|
||||
return err |
|
||||
} |
|
||||
|
|
||||
activeLocalPeer.Store(addr) |
|
||||
if err := peer.Send(buf[:n]); err != nil { |
|
||||
log.Printf("Telemost DataChannel: dropped outbound packet (%d bytes): %v", n, err) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,205 @@ |
|||||
|
package jazz |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"encoding/json" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/cacggghp/vk-turn-proxy/internal/namegen" |
||||
|
"github.com/google/uuid" |
||||
|
) |
||||
|
|
||||
|
const defaultAPIBaseURL = "https://bk.salutejazz.ru" |
||||
|
|
||||
|
var ( |
||||
|
apiBaseURL = defaultAPIBaseURL |
||||
|
apiHTTPClient = &http.Client{Timeout: 15 * time.Second} |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
errCreateRoomFailed = errors.New("create room failed") |
||||
|
errPreconnectFailed = errors.New("preconnect failed") |
||||
|
) |
||||
|
|
||||
|
type RoomInfo struct { |
||||
|
RoomID string `json:"roomId"` |
||||
|
Password string `json:"password"` |
||||
|
ConnectorURL string `json:"connectorUrl"` |
||||
|
} |
||||
|
|
||||
|
func CreateRoom(ctx context.Context) (*RoomInfo, error) { |
||||
|
headers := jazzHeaders() |
||||
|
|
||||
|
createResp, err := createMeeting(ctx, headers) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("create meeting: %w", err) |
||||
|
} |
||||
|
|
||||
|
connectorURL, err := preconnect(ctx, createResp.RoomID, createResp.Password, headers) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("preconnect: %w", err) |
||||
|
} |
||||
|
|
||||
|
return &RoomInfo{ |
||||
|
RoomID: createResp.RoomID, |
||||
|
Password: createResp.Password, |
||||
|
ConnectorURL: connectorURL, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
func JoinRoom(ctx context.Context, roomInput string) (*RoomInfo, error) { |
||||
|
roomID, password := parseRoomInput(roomInput) |
||||
|
if roomID == "" { |
||||
|
return nil, fmt.Errorf("jazz room is required") |
||||
|
} |
||||
|
|
||||
|
connectorURL, err := preconnect(ctx, roomID, password, jazzHeaders()) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &RoomInfo{ |
||||
|
RoomID: roomID, |
||||
|
Password: password, |
||||
|
ConnectorURL: connectorURL, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
func parseRoomInput(roomInput string) (string, string) { |
||||
|
roomInput = strings.TrimSpace(roomInput) |
||||
|
roomInput = strings.TrimPrefix(roomInput, "https://salutejazz.ru/") |
||||
|
roomInput = strings.TrimPrefix(roomInput, "http://salutejazz.ru/") |
||||
|
roomInput = strings.TrimPrefix(roomInput, "https://jazz.sber.ru/") |
||||
|
roomInput = strings.TrimPrefix(roomInput, "http://jazz.sber.ru/") |
||||
|
if idx := strings.IndexAny(roomInput, "/?#"); idx != -1 { |
||||
|
roomInput = roomInput[:idx] |
||||
|
} |
||||
|
|
||||
|
roomID, password, _ := strings.Cut(roomInput, ":") |
||||
|
return strings.TrimSpace(roomID), strings.TrimSpace(password) |
||||
|
} |
||||
|
|
||||
|
func jazzHeaders() map[string]string { |
||||
|
return map[string]string{ |
||||
|
"X-Jazz-ClientId": uuid.New().String(), |
||||
|
"X-Jazz-AuthType": "ANONYMOUS", |
||||
|
"X-Client-AuthType": "ANONYMOUS", |
||||
|
"Content-Type": "application/json", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type createResponse struct { |
||||
|
RoomID string `json:"roomId"` |
||||
|
Password string `json:"password"` |
||||
|
} |
||||
|
|
||||
|
func createMeeting(ctx context.Context, headers map[string]string) (*createResponse, error) { |
||||
|
createPayload := map[string]any{ |
||||
|
"title": namegen.Generate() + " ДР", |
||||
|
"guestEnabled": true, |
||||
|
"lobbyEnabled": false, |
||||
|
"serverVideoRecordAutoStartEnabled": false, |
||||
|
"sipEnabled": false, |
||||
|
"moderatorEmails": []string{}, |
||||
|
"summarizationEnabled": false, |
||||
|
"room3dEnabled": false, |
||||
|
"room3dScene": "XRLobby", |
||||
|
} |
||||
|
|
||||
|
body, err := json.Marshal(createPayload) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("marshal create payload: %w", err) |
||||
|
} |
||||
|
|
||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiBaseURL+"/room/create-meeting", bytes.NewReader(body)) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("create request: %w", err) |
||||
|
} |
||||
|
setHeaders(req, headers) |
||||
|
|
||||
|
resp, err := apiHTTPClient.Do(req) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("do create request: %w", err) |
||||
|
} |
||||
|
defer func() { _ = resp.Body.Close() }() |
||||
|
|
||||
|
if resp.StatusCode != http.StatusOK { |
||||
|
return nil, statusError(errCreateRoomFailed, resp) |
||||
|
} |
||||
|
|
||||
|
var res createResponse |
||||
|
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { |
||||
|
return nil, fmt.Errorf("decode create response: %w", err) |
||||
|
} |
||||
|
|
||||
|
return &res, nil |
||||
|
} |
||||
|
|
||||
|
func preconnect(ctx context.Context, roomID, password string, headers map[string]string) (string, error) { |
||||
|
preconnectPayload := map[string]any{ |
||||
|
"password": password, |
||||
|
"jazzNextMigration": map[string]any{ |
||||
|
"b2bBaseRoomSupport": true, |
||||
|
"demoRoomBaseSupport": true, |
||||
|
"demoRoomVersionSupport": 2, |
||||
|
"mediaWithoutAutoSubscribeSupport": true, |
||||
|
"webinarSpeakerSupport": true, |
||||
|
"webinarViewerSupport": true, |
||||
|
"sdkRoomSupport": true, |
||||
|
"sberclassRoomSupport": true, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
body, err := json.Marshal(preconnectPayload) |
||||
|
if err != nil { |
||||
|
return "", fmt.Errorf("marshal preconnect payload: %w", err) |
||||
|
} |
||||
|
|
||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/room/%s/preconnect", apiBaseURL, roomID), bytes.NewReader(body)) |
||||
|
if err != nil { |
||||
|
return "", fmt.Errorf("create preconnect request: %w", err) |
||||
|
} |
||||
|
setHeaders(req, headers) |
||||
|
|
||||
|
resp, err := apiHTTPClient.Do(req) |
||||
|
if err != nil { |
||||
|
return "", fmt.Errorf("do preconnect request: %w", err) |
||||
|
} |
||||
|
defer func() { _ = resp.Body.Close() }() |
||||
|
|
||||
|
if resp.StatusCode != http.StatusOK { |
||||
|
return "", statusError(errPreconnectFailed, resp) |
||||
|
} |
||||
|
|
||||
|
var preconnectResp struct { |
||||
|
ConnectorURL string `json:"connectorUrl"` |
||||
|
} |
||||
|
if err := json.NewDecoder(resp.Body).Decode(&preconnectResp); err != nil { |
||||
|
return "", fmt.Errorf("decode preconnect response: %w", err) |
||||
|
} |
||||
|
if preconnectResp.ConnectorURL == "" { |
||||
|
return "", fmt.Errorf("preconnect response missing connector URL") |
||||
|
} |
||||
|
|
||||
|
return preconnectResp.ConnectorURL, nil |
||||
|
} |
||||
|
|
||||
|
func setHeaders(req *http.Request, headers map[string]string) { |
||||
|
for key, value := range headers { |
||||
|
req.Header.Set(key, value) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func statusError(base error, resp *http.Response) error { |
||||
|
body, err := io.ReadAll(resp.Body) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("%w: status %s and response body read failed: %v", base, resp.Status, err) |
||||
|
} |
||||
|
return fmt.Errorf("%w: status %s: %s", base, resp.Status, strings.TrimSpace(string(body))) |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
package jazz |
||||
|
|
||||
|
import ( |
||||
|
"encoding/binary" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
|
||||
|
"github.com/google/uuid" |
||||
|
) |
||||
|
|
||||
|
func encodeVarint(value uint64) []byte { |
||||
|
buf := make([]byte, binary.MaxVarintLen64) |
||||
|
n := binary.PutUvarint(buf, value) |
||||
|
return buf[:n] |
||||
|
} |
||||
|
|
||||
|
func encodeField(fieldNumber int, wireType int, data []byte) []byte { |
||||
|
tag := encodeVarint(uint64(fieldNumber)<<3 | uint64(wireType)) //nolint:gosec
|
||||
|
switch wireType { |
||||
|
case 2: |
||||
|
length := encodeVarint(uint64(len(data))) |
||||
|
result := make([]byte, 0, len(tag)+len(length)+len(data)) |
||||
|
result = append(result, tag...) |
||||
|
result = append(result, length...) |
||||
|
result = append(result, data...) |
||||
|
return result |
||||
|
default: |
||||
|
result := make([]byte, 0, len(tag)+len(data)) |
||||
|
result = append(result, tag...) |
||||
|
result = append(result, data...) |
||||
|
return result |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func EncodeDataPacket(payload []byte) []byte { |
||||
|
userFields := encodeField(2, 2, payload) |
||||
|
userFields = append(userFields, encodeField(8, 2, []byte(uuid.New().String()))...) |
||||
|
|
||||
|
packet := encodeField(1, 0, encodeVarint(0)) |
||||
|
packet = append(packet, encodeField(2, 2, userFields)...) |
||||
|
|
||||
|
return packet |
||||
|
} |
||||
|
|
||||
|
func DecodeDataPacket(raw []byte) ([]byte, bool) { |
||||
|
userData, ok := parseFields(raw, 2) |
||||
|
if !ok { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
payload, ok := parseFields(userData, 2) |
||||
|
return payload, ok |
||||
|
} |
||||
|
|
||||
|
func parseFields(data []byte, targetField int) ([]byte, bool) { |
||||
|
reader := &byteReader{data: data} |
||||
|
var result []byte |
||||
|
|
||||
|
for reader.pos < len(reader.data) { |
||||
|
tagVal, err := readVarint(reader) |
||||
|
if err != nil { |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
fieldNumber := int(tagVal >> 3) |
||||
|
wireType := int(tagVal & 0x07) |
||||
|
|
||||
|
fieldData, ok := handleWireType(reader, wireType, len(data)) |
||||
|
if !ok { |
||||
|
return result, len(result) > 0 |
||||
|
} |
||||
|
|
||||
|
if fieldNumber == targetField && wireType == 2 { |
||||
|
result = fieldData |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result, len(result) > 0 |
||||
|
} |
||||
|
|
||||
|
func readVarint(r io.ByteReader) (uint64, error) { |
||||
|
val, err := binary.ReadUvarint(r) |
||||
|
if err != nil { |
||||
|
return 0, fmt.Errorf("read uvarint: %w", err) |
||||
|
} |
||||
|
return val, nil |
||||
|
} |
||||
|
|
||||
|
func handleWireType(reader *byteReader, wireType int, dataLen int) ([]byte, bool) { |
||||
|
switch wireType { |
||||
|
case 0: |
||||
|
_, err := readVarint(reader) |
||||
|
return nil, err == nil |
||||
|
case 1: |
||||
|
reader.pos += 8 |
||||
|
return nil, reader.pos <= dataLen |
||||
|
case 2: |
||||
|
length, err := readVarint(reader) |
||||
|
if err != nil { |
||||
|
return nil, false |
||||
|
} |
||||
|
if length > uint64(dataLen)-uint64(reader.pos) { //nolint:gosec
|
||||
|
return nil, false |
||||
|
} |
||||
|
fieldData := make([]byte, length) |
||||
|
n, err := reader.Read(fieldData) |
||||
|
return fieldData, err == nil && uint64(n) == length //nolint:gosec
|
||||
|
case 5: |
||||
|
reader.pos += 4 |
||||
|
return nil, reader.pos <= dataLen |
||||
|
default: |
||||
|
return nil, false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type byteReader struct { |
||||
|
data []byte |
||||
|
pos int |
||||
|
} |
||||
|
|
||||
|
func (b *byteReader) ReadByte() (byte, error) { |
||||
|
if b.pos >= len(b.data) { |
||||
|
return 0, io.EOF |
||||
|
} |
||||
|
c := b.data[b.pos] |
||||
|
b.pos++ |
||||
|
return c, nil |
||||
|
} |
||||
|
|
||||
|
func (b *byteReader) Read(p []byte) (int, error) { |
||||
|
if b.pos >= len(b.data) { |
||||
|
return 0, io.EOF |
||||
|
} |
||||
|
n := copy(p, b.data[b.pos:]) |
||||
|
b.pos += n |
||||
|
return n, nil |
||||
|
} |
||||
@ -0,0 +1,769 @@ |
|||||
|
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 |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"log" |
||||
|
"net" |
||||
|
|
||||
|
"github.com/cacggghp/vk-turn-proxy/internal/jazz" |
||||
|
"github.com/cacggghp/vk-turn-proxy/internal/telemost" |
||||
|
) |
||||
|
|
||||
|
type dataChannelPeer interface { |
||||
|
Send([]byte) error |
||||
|
Close() error |
||||
|
} |
||||
|
|
||||
|
type dataChannelConnectFunc func(context.Context, string, func([]byte), func()) (dataChannelPeer, error) |
||||
|
|
||||
|
func connectTelemostDataChannelPeer(ctx context.Context, room string, onData func([]byte), onReconnect func()) (dataChannelPeer, error) { |
||||
|
return telemost.NewConnectedPeer(ctx, room, onData, onReconnect) |
||||
|
} |
||||
|
|
||||
|
func connectJazzDataChannelPeer(ctx context.Context, room string, onData func([]byte), onReconnect func()) (dataChannelPeer, error) { |
||||
|
return jazz.NewConnectedPeer(ctx, room, onData, onReconnect) |
||||
|
} |
||||
|
|
||||
|
func runTelemostDataChannelMode(ctx context.Context, inviteLink, connectAddr string) error { |
||||
|
return runDataChannelMode(ctx, "Telemost", connectTelemostDataChannelPeer, inviteLink, connectAddr) |
||||
|
} |
||||
|
|
||||
|
func runJazzDataChannelMode(ctx context.Context, room, connectAddr string) error { |
||||
|
return runDataChannelMode(ctx, "SaluteJazz", connectJazzDataChannelPeer, room, connectAddr) |
||||
|
} |
||||
|
|
||||
|
func runDataChannelMode(ctx context.Context, providerName string, connectPeer dataChannelConnectFunc, room, connectAddr string) error { |
||||
|
backendConn, err := net.Dial("udp", connectAddr) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer func(backendConn net.Conn) { |
||||
|
err := backendConn.Close() |
||||
|
if err != nil { |
||||
|
log.Println(err) |
||||
|
} |
||||
|
}(backendConn) |
||||
|
|
||||
|
peer, err := connectPeer(ctx, room, func(data []byte) { |
||||
|
if _, writeErr := backendConn.Write(data); writeErr != nil { |
||||
|
log.Printf("%s DataChannel: failed to write backend packet: %v", providerName, writeErr) |
||||
|
} |
||||
|
}, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer func(peer dataChannelPeer) { |
||||
|
err := peer.Close() |
||||
|
if err != nil { |
||||
|
log.Println(err) |
||||
|
} |
||||
|
}(peer) |
||||
|
|
||||
|
closeOnContextDone(ctx, backendConn) |
||||
|
|
||||
|
log.Printf("%s DataChannel mode: forwarding to %s", providerName, connectAddr) |
||||
|
|
||||
|
buf := make([]byte, 2048) |
||||
|
for { |
||||
|
n, err := backendConn.Read(buf) |
||||
|
if err != nil { |
||||
|
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) { |
||||
|
return nil |
||||
|
} |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if err := peer.Send(buf[:n]); err != nil { |
||||
|
log.Printf("%s DataChannel: dropped backend packet (%d bytes): %v", providerName, n, err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,57 +0,0 @@ |
|||||
package main |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"errors" |
|
||||
"log" |
|
||||
"net" |
|
||||
|
|
||||
"github.com/cacggghp/vk-turn-proxy/internal/telemost" |
|
||||
) |
|
||||
|
|
||||
func runTelemostDataChannelMode(ctx context.Context, inviteLink, connectAddr string) error { |
|
||||
backendConn, err := net.Dial("udp", connectAddr) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
defer func(backendConn net.Conn) { |
|
||||
err := backendConn.Close() |
|
||||
if err != nil { |
|
||||
log.Println(err) |
|
||||
} |
|
||||
}(backendConn) |
|
||||
|
|
||||
peer, err := telemost.NewConnectedPeer(ctx, inviteLink, func(data []byte) { |
|
||||
if _, writeErr := backendConn.Write(data); writeErr != nil { |
|
||||
log.Printf("Telemost DataChannel: failed to write backend packet: %v", writeErr) |
|
||||
} |
|
||||
}, nil) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
defer func(peer *telemost.Peer) { |
|
||||
err := peer.Close() |
|
||||
if err != nil { |
|
||||
log.Println(err) |
|
||||
} |
|
||||
}(peer) |
|
||||
|
|
||||
closeOnContextDone(ctx, backendConn) |
|
||||
|
|
||||
log.Printf("Telemost DataChannel mode: forwarding to %s", connectAddr) |
|
||||
|
|
||||
buf := make([]byte, 2048) |
|
||||
for { |
|
||||
n, err := backendConn.Read(buf) |
|
||||
if err != nil { |
|
||||
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) { |
|
||||
return nil |
|
||||
} |
|
||||
return err |
|
||||
} |
|
||||
|
|
||||
if err := peer.Send(buf[:n]); err != nil { |
|
||||
log.Printf("Telemost DataChannel: dropped backend packet (%d bytes): %v", n, err) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue