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 "" }