mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 06:15:15 +03:00
849 lines
24 KiB
Go
849 lines
24 KiB
Go
package telemost
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
"github.com/sagernet/sing-box/transport/call/tunnel"
|
|
"github.com/sagernet/sing/common/logger"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
const (
|
|
tmAPIBase = APIBase
|
|
tmOrigin = Origin
|
|
tmPingPeriod = 5 * time.Second
|
|
)
|
|
|
|
var clientInstanceID = uuid.New().String()
|
|
|
|
type ConnInfo struct {
|
|
ConferenceURI string
|
|
RoomID string
|
|
PeerID string
|
|
Credentials string
|
|
MediaServerURL string
|
|
ServiceName string
|
|
ICEServers []webrtc.ICEServer
|
|
StateCheckIntervalS int
|
|
}
|
|
|
|
type Bridge struct {
|
|
mu sync.Mutex
|
|
ws *websocket.Conn
|
|
relay *SFURelay
|
|
connInfo *ConnInfo
|
|
config TMConfig
|
|
cookieStr string
|
|
pubSeq int
|
|
subSeq int
|
|
peers map[string]string
|
|
readBuf int
|
|
activeBridge *tunnel.RelayBridge
|
|
selfName string
|
|
dialer N.Dialer
|
|
logger logger.ContextLogger
|
|
|
|
setSlotsKey int
|
|
initBundleSent bool
|
|
pendingKicks map[string]chan struct{}
|
|
boundPeers map[string]bool
|
|
unboundPeers map[string]bool
|
|
}
|
|
|
|
func tmRequest(dialer N.Dialer, method, path string, body interface{}, cookieStr string, cfg TMConfig) ([]byte, int, error) {
|
|
c := Client{HTTP: common.HttpClient(dialer), Cookie: cookieStr, AppVersion: cfg.AppVersion, InstanceID: clientInstanceID}
|
|
return c.Do(method, path, body)
|
|
}
|
|
|
|
func parseICEServersJSON(raw json.RawMessage) []webrtc.ICEServer {
|
|
var rawIce []struct {
|
|
URLs []string `json:"urls"`
|
|
Username string `json:"username"`
|
|
Credential string `json:"credential"`
|
|
}
|
|
json.Unmarshal(raw, &rawIce)
|
|
var out []webrtc.ICEServer
|
|
for _, s := range rawIce {
|
|
ice := webrtc.ICEServer{URLs: s.URLs}
|
|
if s.Username != "" {
|
|
ice.Username = s.Username
|
|
ice.Credential = s.Credential
|
|
}
|
|
out = append(out, ice)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func getConnection(dialer N.Dialer, cookieStr, confURL string, cfg TMConfig) (*ConnInfo, error) {
|
|
r, status, err := tmRequest(dialer, "GET",
|
|
"/conferences/"+confURL+"/connection?next_gen_media_platform_allowed=true&display_name=Headless&waiting_room_supported=true",
|
|
nil, cookieStr, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get connection: %w", err)
|
|
}
|
|
if status != 200 {
|
|
return nil, fmt.Errorf("get connection: status %d: %s", status, string(r))
|
|
}
|
|
var conn struct {
|
|
PeerID string `json:"peer_id"`
|
|
RoomID string `json:"room_id"`
|
|
Credentials string `json:"credentials"`
|
|
ClientConfig struct {
|
|
MediaServerURL string `json:"media_server_url"`
|
|
ServiceName string `json:"service_name"`
|
|
ICEServers json.RawMessage `json:"ice_servers"`
|
|
StateCheckIntervalSecs int `json:"state_check_interval_seconds"`
|
|
} `json:"client_configuration"`
|
|
}
|
|
json.Unmarshal(r, &conn)
|
|
if conn.ClientConfig.MediaServerURL == "" {
|
|
return nil, fmt.Errorf("empty media_server_url: %s", string(r))
|
|
}
|
|
return &ConnInfo{
|
|
RoomID: conn.RoomID,
|
|
PeerID: conn.PeerID,
|
|
Credentials: conn.Credentials,
|
|
MediaServerURL: conn.ClientConfig.MediaServerURL,
|
|
ServiceName: conn.ClientConfig.ServiceName,
|
|
ICEServers: parseICEServersJSON(conn.ClientConfig.ICEServers),
|
|
StateCheckIntervalS: conn.ClientConfig.StateCheckIntervalSecs,
|
|
}, nil
|
|
}
|
|
|
|
func joinExistingConference(dialer N.Dialer, cookieStr, conferenceURI string, cfg TMConfig, logger logger.ContextLogger) (*ConnInfo, error) {
|
|
conferenceURI = strings.TrimSpace(conferenceURI)
|
|
if conferenceURI == "" {
|
|
return nil, fmt.Errorf("empty -tm-link")
|
|
}
|
|
logger.Info(fmt.Sprintf("[auth] Joining existing conference: %s", conferenceURI))
|
|
info, err := getConnection(dialer, cookieStr, url.QueryEscape(conferenceURI), cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info.ConferenceURI = conferenceURI
|
|
logger.Debug(fmt.Sprintf("[auth] peer_id=%s room_id=%s", info.PeerID, info.RoomID))
|
|
logger.Debug(fmt.Sprintf("[auth] media_server=%s", info.MediaServerURL))
|
|
return info, nil
|
|
}
|
|
|
|
func CreateAndJoinCall(dialer N.Dialer, cookieStr string, cfg TMConfig, logger logger.ContextLogger) (*ConnInfo, error) {
|
|
logger.Info("[auth] Creating conference...")
|
|
r, status, err := tmRequest(dialer, "POST", "/conferences?next_gen_media_platform_allowed=true",
|
|
struct{}{}, cookieStr, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create conference: %w", err)
|
|
}
|
|
if status != 200 && status != 201 {
|
|
return nil, fmt.Errorf("create conference: status %d: %s", status, string(r))
|
|
}
|
|
var conf struct {
|
|
URI string `json:"uri"`
|
|
}
|
|
json.Unmarshal(r, &conf)
|
|
if conf.URI == "" {
|
|
return nil, fmt.Errorf("empty conference URI: %s", string(r))
|
|
}
|
|
logger.Info(fmt.Sprintf("[auth] Conference: %s", conf.URI))
|
|
logger.Debug("[auth] Getting connection...")
|
|
info, err := getConnection(dialer, cookieStr, url.QueryEscape(conf.URI), cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info.ConferenceURI = conf.URI
|
|
logger.Debug(fmt.Sprintf("[auth] peer_id=%s room_id=%s", info.PeerID, info.RoomID))
|
|
logger.Debug(fmt.Sprintf("[auth] media_server=%s", info.MediaServerURL))
|
|
return info, nil
|
|
}
|
|
|
|
func (b *Bridge) wsSend(msg interface{}) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.ws == nil {
|
|
return
|
|
}
|
|
data, _ := json.Marshal(msg)
|
|
b.ws.WriteMessage(websocket.TextMessage, data)
|
|
}
|
|
|
|
func (b *Bridge) ack(uid string) {
|
|
b.wsSend(map[string]interface{}{
|
|
"uid": uid,
|
|
"ack": map[string]interface{}{
|
|
"status": map[string]interface{}{"code": "OK", "description": ""},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (b *Bridge) sendHello() {
|
|
b.mu.Lock()
|
|
b.selfName = "Headless"
|
|
b.mu.Unlock()
|
|
b.wsSend(map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"hello": map[string]interface{}{
|
|
"participantMeta": map[string]interface{}{"name": "Headless", "role": "SPEAKER", "description": "", "sendAudio": false, "sendVideo": true},
|
|
"participantAttributes": map[string]interface{}{"name": "Headless", "role": "SPEAKER", "description": ""},
|
|
"sendAudio": false, "sendVideo": true, "sendSharing": false,
|
|
"participantId": b.connInfo.PeerID, "roomId": b.connInfo.RoomID,
|
|
"serviceName": b.connInfo.ServiceName, "credentials": b.connInfo.Credentials,
|
|
"capabilitiesOffer": CapabilitiesOffer,
|
|
"sdkInfo": map[string]interface{}{"implementation": "browser", "version": b.config.SDKVersion, "userAgent": common.UserAgent, "hwConcurrency": 8},
|
|
"sdkInitializationId": uuid.New().String(),
|
|
"disablePublisher": false, "disableSubscriber": false, "disableSubscriberAudio": false,
|
|
},
|
|
})
|
|
b.logger.Debug("[tm-ws] -> hello")
|
|
}
|
|
|
|
func (b *Bridge) sendPubOffer() {
|
|
offer, err := b.relay.CreatePubOffer()
|
|
if err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] pub offer failed: %v", err))
|
|
return
|
|
}
|
|
audioMid, videoMid := parseMids(offer.SDP)
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] -> publisherSdpOffer pcSeq=%d", b.pubSeq))
|
|
var tracks []map[string]interface{}
|
|
if audioMid != "" {
|
|
tracks = append(tracks, map[string]interface{}{"mid": audioMid, "transceiverMid": audioMid, "kind": "AUDIO", "priority": 0, "label": "", "codecs": map[string]interface{}{}, "groupId": 1, "description": ""})
|
|
}
|
|
if videoMid != "" {
|
|
tracks = append(tracks, map[string]interface{}{"mid": videoMid, "transceiverMid": videoMid, "kind": "VIDEO", "priority": 0, "label": "", "codecs": map[string]interface{}{}, "groupId": 2, "description": ""})
|
|
}
|
|
b.wsSend(map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"publisherSdpOffer": map[string]interface{}{"pcSeq": b.pubSeq, "sdp": offer.SDP, "tracks": tracks},
|
|
})
|
|
}
|
|
|
|
func (b *Bridge) sendICE(cand *webrtc.ICECandidate, target string, pcSeq int) {
|
|
c := cand.ToJSON()
|
|
mid := ""
|
|
if c.SDPMid != nil {
|
|
mid = *c.SDPMid
|
|
}
|
|
var idx uint16
|
|
if c.SDPMLineIndex != nil {
|
|
idx = *c.SDPMLineIndex
|
|
}
|
|
b.wsSend(map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"webrtcIceCandidate": map[string]interface{}{
|
|
"candidate": c.Candidate, "sdpMid": mid,
|
|
"usernameFragment": extractUfrag(c.Candidate),
|
|
"sdpMlineIndex": idx, "target": target, "pcSeq": pcSeq,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (b *Bridge) requestVideoSlots() {
|
|
b.setSlotsKey++
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] -> setSlots key=%d", b.setSlotsKey))
|
|
b.wsSend(SetSlotsMessage(b.setSlotsKey))
|
|
}
|
|
|
|
func (b *Bridge) forceReconnect(reason string) {
|
|
oldPeerID := b.connInfo.PeerID
|
|
b.logger.Info(fmt.Sprintf("[tm-ws] forcing reconnect: %s", reason))
|
|
if oldPeerID != "" {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] kicking self pid=%s to leave call cleanly", oldPeerID))
|
|
if err := b.kickPeer(oldPeerID); err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] self-kick failed: %v", err))
|
|
}
|
|
}
|
|
clientInstanceID = uuid.New().String()
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] new instance-id=%s", clientInstanceID))
|
|
b.mu.Lock()
|
|
ws := b.ws
|
|
b.mu.Unlock()
|
|
if ws != nil {
|
|
ws.Close()
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) sendInitBundle() {
|
|
if b.initBundleSent {
|
|
return
|
|
}
|
|
b.initBundleSent = true
|
|
b.logger.Debug("[tm-ws] -> sdkCodecsInfo + updatePublisherTrackDescription")
|
|
b.wsSend(SdkCodecsInfoMessage())
|
|
b.wsSend(UpdatePublisherTrackDescriptionMessage(b.relay.pubPC, "Microphone", "MacBook Pro Camera (0000:0001)"))
|
|
b.sendStartupSlotsRamp()
|
|
}
|
|
|
|
func (b *Bridge) sendStartupSlotsRamp() {
|
|
for i := 0; i < 4; i++ {
|
|
b.setSlotsKey++
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] -> setSlots key=%d (startup %d/4)", b.setSlotsKey, i+1))
|
|
b.wsSend(StartupSetSlotsMessage(i, b.setSlotsKey))
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) handleMessage(raw []byte) {
|
|
var msg map[string]interface{}
|
|
if err := json.Unmarshal(raw, &msg); err != nil {
|
|
return
|
|
}
|
|
uid, _ := msg["uid"].(string)
|
|
if sh, ok := msg["serverHello"]; ok {
|
|
b.logger.Debug("[tm-ws] <- serverHello")
|
|
if shMap, ok := sh.(map[string]interface{}); ok {
|
|
b.parseICEServers(shMap)
|
|
}
|
|
b.ack(uid)
|
|
b.logger.Debug("[tm-ws] -> setSlotsOffset")
|
|
b.wsSend(SetSlotsOffsetMessage(0))
|
|
b.initRelay()
|
|
return
|
|
}
|
|
if pa, ok := msg["publisherSdpAnswer"]; ok {
|
|
paMap, _ := pa.(map[string]interface{})
|
|
sdp, _ := paMap["sdp"].(string)
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- publisherSdpAnswer %d bytes", len(sdp)))
|
|
if err := b.relay.SetPubAnswer(sdp); err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] error: %v", err))
|
|
return
|
|
}
|
|
b.sendInitBundle()
|
|
return
|
|
}
|
|
if so, ok := msg["subscriberSdpOffer"]; ok {
|
|
soMap, _ := so.(map[string]interface{})
|
|
sdp, _ := soMap["sdp"].(string)
|
|
pcSeq, _ := soMap["pcSeq"].(float64)
|
|
b.subSeq = int(pcSeq)
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- subscriberSdpOffer pcSeq=%d", b.subSeq))
|
|
b.ack(uid)
|
|
answer, err := b.relay.SetSubOffer(sdp)
|
|
if err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] error: %v", err))
|
|
return
|
|
}
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] -> subscriberSdpAnswer pcSeq=%d", b.subSeq))
|
|
b.wsSend(map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"subscriberSdpAnswer": map[string]interface{}{"sdp": answer.SDP, "pcSeq": b.subSeq},
|
|
})
|
|
b.sendPubOffer()
|
|
return
|
|
}
|
|
if ic, ok := msg["webrtcIceCandidate"]; ok {
|
|
icMap, _ := ic.(map[string]interface{})
|
|
candidate, _ := icMap["candidate"].(string)
|
|
sdpMid, _ := icMap["sdpMid"].(string)
|
|
target, _ := icMap["target"].(string)
|
|
sdpIdx, _ := icMap["sdpMlineIndex"].(float64)
|
|
idx := uint16(sdpIdx)
|
|
cand := webrtc.ICECandidateInit{Candidate: candidate, SDPMid: &sdpMid, SDPMLineIndex: &idx}
|
|
if target == "PUBLISHER" {
|
|
b.relay.AddPubICECandidate(cand)
|
|
} else {
|
|
b.relay.AddSubICECandidate(cand)
|
|
}
|
|
b.ack(uid)
|
|
return
|
|
}
|
|
if ackData, ok := msg["ack"]; ok {
|
|
if ackMap, ok := ackData.(map[string]interface{}); ok {
|
|
if status, ok := ackMap["status"].(map[string]interface{}); ok {
|
|
if code, _ := status["code"].(string); code != "OK" {
|
|
desc, _ := status["description"].(string)
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] <- ack error: %s %s", code, desc))
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if ud, ok := msg["updateDescription"]; ok {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- updateDescription %s", BriefJSON(ud)))
|
|
udMap, _ := ud.(map[string]interface{})
|
|
descs, _ := udMap["description"].([]interface{})
|
|
b.applyDescriptionSnapshot(descs)
|
|
b.ack(uid)
|
|
return
|
|
}
|
|
if ud, ok := msg["upsertDescription"]; ok {
|
|
udMap, _ := ud.(map[string]interface{})
|
|
descs, _ := udMap["description"].([]interface{})
|
|
for _, d := range descs {
|
|
dm, _ := d.(map[string]interface{})
|
|
b.applyDescriptionEntry(dm)
|
|
}
|
|
b.kickStaleSelves()
|
|
b.ack(uid)
|
|
return
|
|
}
|
|
if rd, ok := msg["removeDescription"]; ok {
|
|
rdMap, _ := rd.(map[string]interface{})
|
|
ids, _ := rdMap["descriptionId"].([]interface{})
|
|
for _, id := range ids {
|
|
pid, _ := id.(string)
|
|
b.mu.Lock()
|
|
name := b.peers[pid]
|
|
delete(b.peers, pid)
|
|
remaining := len(b.peers)
|
|
ch, hadPendingKick := b.pendingKicks[pid]
|
|
if hadPendingKick {
|
|
delete(b.pendingKicks, pid)
|
|
}
|
|
b.mu.Unlock()
|
|
b.logger.Info(fmt.Sprintf("[tm-ws] Participant left: %s (%s) total=%d", name, pid, remaining))
|
|
if hadPendingKick {
|
|
close(ch)
|
|
}
|
|
if remaining == 0 {
|
|
go b.pollAndAdmit()
|
|
}
|
|
}
|
|
b.ack(uid)
|
|
return
|
|
}
|
|
if n, ok := msg["notification"]; ok {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- notification %s", BriefJSON(n)))
|
|
b.ack(uid)
|
|
go b.pollAndAdmit()
|
|
return
|
|
}
|
|
if pc, ok := msg["participantsChanged"]; ok {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- participantsChanged %s", BriefJSON(pc)))
|
|
b.ack(uid)
|
|
go b.pollAndAdmit()
|
|
return
|
|
}
|
|
if sc, ok := msg["slotsConfig"]; ok {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- slotsConfig %s", BriefJSON(sc)))
|
|
needRebind := false
|
|
presentPids := make(map[string]bool)
|
|
for _, ev := range SlotsConfigBindings(sc) {
|
|
fullPid := ev.ParticipantID
|
|
if fullPid != "" {
|
|
presentPids[fullPid] = true
|
|
}
|
|
pid := fullPid
|
|
if len(pid) > 8 {
|
|
pid = pid[:8]
|
|
}
|
|
if ev.Reason == "NO_LIMITATION" && ev.Mid != "" {
|
|
b.logger.Debug(fmt.Sprintf("[bind] BOUND slot=%d pid=%s mid=%s", ev.Slot, pid, ev.Mid))
|
|
b.mu.Lock()
|
|
if b.boundPeers == nil {
|
|
b.boundPeers = make(map[string]bool)
|
|
}
|
|
b.boundPeers[fullPid] = true
|
|
delete(b.unboundPeers, fullPid)
|
|
b.mu.Unlock()
|
|
} else if fullPid != "" {
|
|
b.mu.Lock()
|
|
wasBound := b.boundPeers[fullPid]
|
|
if wasBound {
|
|
if b.unboundPeers == nil {
|
|
b.unboundPeers = make(map[string]bool)
|
|
}
|
|
b.unboundPeers[fullPid] = true
|
|
delete(b.boundPeers, fullPid)
|
|
}
|
|
b.mu.Unlock()
|
|
if wasBound {
|
|
b.logger.Debug(fmt.Sprintf("[bind] KILL slot=%d pid=%s reason=%s - rebinding", ev.Slot, pid, ev.Reason))
|
|
needRebind = true
|
|
} else {
|
|
b.logger.Debug(fmt.Sprintf("[bind] UNBOUND slot=%d pid=%s reason=%s mid=%q", ev.Slot, pid, ev.Reason, ev.Mid))
|
|
}
|
|
}
|
|
}
|
|
b.mu.Lock()
|
|
for boundPid := range b.boundPeers {
|
|
if !presentPids[boundPid] {
|
|
short := boundPid
|
|
if len(short) > 8 {
|
|
short = short[:8]
|
|
}
|
|
b.logger.Debug(fmt.Sprintf("[bind] VANISHED pid=%s - rebinding", short))
|
|
delete(b.boundPeers, boundPid)
|
|
needRebind = true
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
if needRebind {
|
|
go b.forceReconnect("slot binding killed")
|
|
}
|
|
b.ack(uid)
|
|
return
|
|
}
|
|
for k, v := range msg {
|
|
if k == "uid" || k == "ack" {
|
|
continue
|
|
}
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] <- %s (unhandled) %s", k, BriefJSON(v)))
|
|
break
|
|
}
|
|
if uid != "" {
|
|
b.ack(uid)
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) parseICEServers(sh map[string]interface{}) {
|
|
rtcCfg, ok := sh["rtcConfiguration"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
servers, ok := rtcCfg["iceServers"].([]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
var iceServers []webrtc.ICEServer
|
|
for _, s := range servers {
|
|
sm, _ := s.(map[string]interface{})
|
|
var urls []string
|
|
if u, ok := sm["urls"].([]interface{}); ok {
|
|
for _, v := range u {
|
|
if vs, ok := v.(string); ok {
|
|
urls = append(urls, vs)
|
|
}
|
|
}
|
|
}
|
|
ice := webrtc.ICEServer{URLs: urls}
|
|
if u, ok := sm["username"].(string); ok && u != "" {
|
|
ice.Username = u
|
|
ice.Credential, _ = sm["credential"].(string)
|
|
}
|
|
iceServers = append(iceServers, ice)
|
|
}
|
|
b.connInfo.ICEServers = iceServers
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] %d ICE servers", len(iceServers)))
|
|
}
|
|
|
|
func (b *Bridge) requestStates() error {
|
|
c := Client{HTTP: common.HttpClient(b.dialer), Cookie: b.cookieStr, AppVersion: b.config.AppVersion, InstanceID: clientInstanceID}
|
|
return c.RequestStates(b.connInfo.ConferenceURI, b.connInfo.PeerID)
|
|
}
|
|
|
|
func (b *Bridge) applyDescriptionEntry(dm map[string]interface{}) {
|
|
pid, _ := dm["id"].(string)
|
|
if pid == "" {
|
|
return
|
|
}
|
|
name := ""
|
|
if meta, ok := dm["meta"].(map[string]interface{}); ok {
|
|
name, _ = meta["name"].(string)
|
|
}
|
|
if pid == b.connInfo.PeerID {
|
|
b.mu.Lock()
|
|
if name != "" {
|
|
b.selfName = name
|
|
}
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
_, disconnected := dm["disconnectedAt"]
|
|
b.mu.Lock()
|
|
_, wasKnown := b.peers[pid]
|
|
if disconnected {
|
|
delete(b.peers, pid)
|
|
} else {
|
|
b.peers[pid] = name
|
|
}
|
|
total := len(b.peers)
|
|
b.mu.Unlock()
|
|
switch {
|
|
case disconnected && wasKnown:
|
|
b.logger.Info(fmt.Sprintf("[tm-ws] Participant left: %s (%s) total=%d", name, pid, total))
|
|
case disconnected:
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] Ghost participant: %s (%s) - kicking", name, pid))
|
|
go b.kickPeer(pid)
|
|
case !wasKnown:
|
|
b.logger.Info(fmt.Sprintf("[tm-ws] Participant joined: %s (%s) total=%d", name, pid, total))
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) applyDescriptionSnapshot(descs []interface{}) {
|
|
b.mu.Lock()
|
|
b.peers = make(map[string]string)
|
|
b.mu.Unlock()
|
|
for _, d := range descs {
|
|
dm, _ := d.(map[string]interface{})
|
|
b.applyDescriptionEntry(dm)
|
|
}
|
|
b.kickStaleSelves()
|
|
}
|
|
|
|
func (b *Bridge) kickStaleSelves() {
|
|
b.mu.Lock()
|
|
selfName := b.selfName
|
|
stale := make([]string, 0)
|
|
if selfName != "" {
|
|
for pid, name := range b.peers {
|
|
if name == selfName {
|
|
stale = append(stale, pid)
|
|
}
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
for _, pid := range stale {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] Kicking stale self %s (name=%q)", pid, selfName))
|
|
b.kickPeer(pid)
|
|
b.mu.Lock()
|
|
delete(b.peers, pid)
|
|
b.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) kickPeer(peerID string) error {
|
|
confURL := url.QueryEscape(b.connInfo.ConferenceURI)
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] Kicking %s", peerID))
|
|
body, status, err := tmRequest(b.dialer, "POST", "/conferences/"+confURL+"/commands/kick?peer_id="+url.QueryEscape(peerID)+"&with_ban=false",
|
|
nil, b.cookieStr, b.config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status >= 400 {
|
|
return fmt.Errorf("kick %s status %d: %s", peerID, status, string(body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bridge) pollAndAdmit() {
|
|
confURL := url.QueryEscape(b.connInfo.ConferenceURI)
|
|
r, status, err := tmRequest(b.dialer, "GET", "/conferences/"+confURL+"/waiting-rooms/peers", nil, b.cookieStr, b.config)
|
|
if err != nil || status != 200 {
|
|
return
|
|
}
|
|
var resp struct {
|
|
Peers []struct {
|
|
PeerID string `json:"peer_id"`
|
|
State struct {
|
|
DisplayName string `json:"display_name"`
|
|
} `json:"state"`
|
|
} `json:"peers"`
|
|
}
|
|
json.Unmarshal(r, &resp)
|
|
if len(resp.Peers) == 0 {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
if b.pendingKicks == nil {
|
|
b.pendingKicks = make(map[string]chan struct{})
|
|
}
|
|
toKick := make(map[string]string, len(b.peers))
|
|
waits := make(map[string]<-chan struct{}, len(b.peers))
|
|
for pid, name := range b.peers {
|
|
toKick[pid] = name
|
|
ch := make(chan struct{})
|
|
b.pendingKicks[pid] = ch
|
|
waits[pid] = ch
|
|
}
|
|
b.mu.Unlock()
|
|
for pid, name := range toKick {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] Kicking %s (%s) for one-to-one", name, pid))
|
|
if err := b.kickPeer(pid); err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] kick failed: %v", err))
|
|
b.mu.Lock()
|
|
delete(b.pendingKicks, pid)
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
for pid, ch := range waits {
|
|
<-ch
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] kick confirmed for %s", pid))
|
|
}
|
|
p := resp.Peers[0]
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] Admitting %s (%s)", p.State.DisplayName, p.PeerID))
|
|
tmRequest(b.dialer, "PUT", "/conferences/"+confURL+"/commands/admit?peer_id="+url.QueryEscape(p.PeerID),
|
|
nil, b.cookieStr, b.config)
|
|
}
|
|
|
|
func (b *Bridge) initRelay() {
|
|
if b.relay != nil {
|
|
b.relay.Close()
|
|
}
|
|
b.pubSeq = 1
|
|
b.subSeq = 0
|
|
b.initBundleSent = false
|
|
relay := NewSFURelay(b.logger)
|
|
relay.readBufSize = b.readBuf
|
|
obf, err := tunnel.NewTunnelObfuscator(tunnel.DeriveSecretFromJoinLink(b.connInfo.ConferenceURI))
|
|
if err != nil {
|
|
b.logger.Fatal(fmt.Sprintf("[relay] obfuscator init failed: %v", err))
|
|
}
|
|
relay.SetObfuscator(obf)
|
|
b.logger.Debug(fmt.Sprintf("[relay] obfuscator localEpoch=0x%08x", obf.LocalEpoch()))
|
|
relay.OnPubReady = func() {
|
|
b.logger.Debug("[relay] pub PC connected")
|
|
}
|
|
relay.OnConnected = func(tun *tunnel.VP8DataTunnel) {
|
|
if b.activeBridge != nil {
|
|
b.activeBridge.Reset()
|
|
}
|
|
b.activeBridge = tunnel.NewRelayBridge(tun, "creator", common.VP8BufSize, b.dialer, b.logger)
|
|
b.logger.Debug("[relay] tunnel connected")
|
|
}
|
|
relay.OnPeerRestart = func() {
|
|
if b.activeBridge != nil {
|
|
b.logger.Info("[relay] new peer detected, resetting relay bridge")
|
|
b.activeBridge.Reset()
|
|
}
|
|
}
|
|
relay.OnPubICE = func(cand *webrtc.ICECandidate) {
|
|
if cand == nil {
|
|
return
|
|
}
|
|
b.sendICE(cand, "PUBLISHER", b.pubSeq)
|
|
}
|
|
relay.OnSubICE = func(cand *webrtc.ICECandidate) {
|
|
if cand == nil {
|
|
return
|
|
}
|
|
b.sendICE(cand, "SUBSCRIBER", b.subSeq)
|
|
}
|
|
if err := relay.Init(b.connInfo.ICEServers); err != nil {
|
|
b.logger.Fatal(fmt.Sprintf("[relay] init failed: %v", err))
|
|
}
|
|
b.relay = relay
|
|
}
|
|
|
|
func (b *Bridge) Run() {
|
|
wsHeader := http.Header{}
|
|
wsHeader.Set("User-Agent", common.UserAgent)
|
|
wsHeader.Set("Origin", tmOrigin)
|
|
wsDialer := websocket.Dialer{
|
|
NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return b.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
},
|
|
}
|
|
for {
|
|
b.logger.Debug("[tm-ws] Connecting...")
|
|
ws, _, err := wsDialer.Dial(b.connInfo.MediaServerURL, wsHeader)
|
|
if err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[tm-ws] Connect failed: %s, retrying in 5s...", common.MaskError(err)))
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
b.mu.Lock()
|
|
b.ws = ws
|
|
b.mu.Unlock()
|
|
b.logger.Debug("[tm-ws] Connected")
|
|
b.sendHello()
|
|
go b.pollAndAdmit()
|
|
stopWaitingRoomPoll := make(chan struct{})
|
|
go func() {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stopWaitingRoomPoll:
|
|
return
|
|
case <-ticker.C:
|
|
b.pollAndAdmit()
|
|
}
|
|
}
|
|
}()
|
|
stopPing := make(chan struct{})
|
|
go func() {
|
|
ticker := time.NewTicker(tmPingPeriod)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stopPing:
|
|
return
|
|
case <-ticker.C:
|
|
b.wsSend(map[string]interface{}{"uid": uuid.New().String(), "ping": map[string]interface{}{}})
|
|
}
|
|
}
|
|
}()
|
|
stopStateKeepalive := make(chan struct{})
|
|
go func() {
|
|
interval := b.connInfo.StateCheckIntervalS
|
|
if interval <= 0 {
|
|
interval = 30
|
|
}
|
|
if err := b.requestStates(); err != nil {
|
|
b.logger.Debug(fmt.Sprintf("[tm-state] initial request-states: %v", err))
|
|
}
|
|
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stopStateKeepalive:
|
|
return
|
|
case <-ticker.C:
|
|
if err := b.requestStates(); err != nil {
|
|
b.logger.Debug(fmt.Sprintf("[tm-state] request-states: %v", err))
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
for {
|
|
_, raw, err := ws.ReadMessage()
|
|
if err != nil {
|
|
b.logger.Debug(fmt.Sprintf("[tm-ws] Closed: %s", common.MaskError(err)))
|
|
break
|
|
}
|
|
b.handleMessage(raw)
|
|
}
|
|
close(stopPing)
|
|
close(stopStateKeepalive)
|
|
close(stopWaitingRoomPoll)
|
|
b.mu.Lock()
|
|
b.ws = nil
|
|
b.mu.Unlock()
|
|
b.logger.Debug("[tm-ws] Rejoining in 3s...")
|
|
time.Sleep(3 * time.Second)
|
|
newConn, err := getConnection(b.dialer, b.cookieStr, url.QueryEscape(b.connInfo.ConferenceURI), b.config)
|
|
if err != nil {
|
|
b.logger.Warn(fmt.Sprintf("[rejoin] Failed: %v, retrying in 5s...", err))
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
b.connInfo.PeerID = newConn.PeerID
|
|
b.connInfo.Credentials = newConn.Credentials
|
|
b.connInfo.MediaServerURL = newConn.MediaServerURL
|
|
b.connInfo.ICEServers = newConn.ICEServers
|
|
b.connInfo.StateCheckIntervalS = newConn.StateCheckIntervalS
|
|
}
|
|
}
|
|
|
|
func parseMids(sdp string) (audioMid, videoMid string) {
|
|
var media string
|
|
for _, line := range strings.Split(sdp, "\r\n") {
|
|
if strings.HasPrefix(line, "m=audio") {
|
|
media = "audio"
|
|
} else if strings.HasPrefix(line, "m=video") {
|
|
media = "video"
|
|
}
|
|
if strings.HasPrefix(line, "a=mid:") {
|
|
mid := strings.TrimPrefix(line, "a=mid:")
|
|
if media == "audio" && audioMid == "" {
|
|
audioMid = mid
|
|
} else if media == "video" && videoMid == "" {
|
|
videoMid = mid
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func extractUfrag(candidate string) string {
|
|
parts := strings.Split(candidate, " ")
|
|
for i, p := range parts {
|
|
if p == "ufrag" && i+1 < len(parts) {
|
|
return parts[i+1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|