mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-06-04 11:29:45 +00:00
Decompose the monolithic internal/provider/telemost package into two orthogonal layers: engine/goolom (Yandex proprietary SFU wire protocol — WebSocket signaling, dual pub/sub PeerConnections, DataChannel, telemetry) and auth/telemost (HTTP connection-info fetch → engine.Credentials). Add engine.Config.Refresh callback so Goolom can obtain fresh peerID and credentials on every reconnect without a direct dependency on the auth package. engine_adapter wires the Refresh closure from authProvider.Issue. Delete internal/provider/ entirely (telemost was the last tenant) and remove the now-obsolete provider_adapter + its test from builtin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
319 lines
7.3 KiB
Go
319 lines
7.3 KiB
Go
package goolom
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/openlibrecommunity/olcrtc/internal/logger"
|
|
"github.com/pion/webrtc/v4"
|
|
)
|
|
|
|
func (s *Session) setupDataChannelHandlers(dcReady chan struct{}, sessionCloseCh chan struct{}) {
|
|
s.dc.OnOpen(func() {
|
|
numWorkers := 4
|
|
for i := range numWorkers {
|
|
s.wg.Add(1)
|
|
go func(workerID int) {
|
|
defer s.wg.Done()
|
|
s.processSendQueue(workerID, sessionCloseCh)
|
|
}(i)
|
|
}
|
|
close(dcReady)
|
|
})
|
|
|
|
s.dc.OnClose(s.onDataChannelClose)
|
|
s.dc.OnMessage(s.onDataChannelMessage)
|
|
|
|
s.pcSub.OnDataChannel(func(dc *webrtc.DataChannel) {
|
|
if s.onData != nil {
|
|
dc.OnMessage(s.onDataChannelMessage)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *Session) onDataChannelClose() {
|
|
if !s.closed.Load() {
|
|
s.queueReconnect()
|
|
}
|
|
}
|
|
|
|
func (s *Session) onDataChannelMessage(msg webrtc.DataChannelMessage) {
|
|
if s.onData != nil && len(msg.Data) > 0 {
|
|
s.onData(msg.Data)
|
|
}
|
|
}
|
|
|
|
func (s *Session) handleSdpOffer(offer map[string]any, uid string, sendPub bool) error {
|
|
sdp, _ := offer["sdp"].(string)
|
|
pcSeq, _ := offer["pcSeq"].(float64)
|
|
|
|
if err := s.pcSub.SetRemoteDescription(webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeOffer,
|
|
SDP: sdp,
|
|
}); err != nil {
|
|
return fmt.Errorf("set remote desc: %w", err)
|
|
}
|
|
|
|
answer, err := s.pcSub.CreateAnswer(nil)
|
|
if err != nil {
|
|
return fmt.Errorf("create answer: %w", err)
|
|
}
|
|
|
|
if err := s.pcSub.SetLocalDescription(answer); err != nil {
|
|
return fmt.Errorf("set local desc: %w", err)
|
|
}
|
|
|
|
s.wsMu.Lock()
|
|
_ = s.ws.WriteJSON(map[string]any{
|
|
keyUID: uuid.New().String(),
|
|
"subscriberSdpAnswer": map[string]any{
|
|
keyPcSeq: int(pcSeq),
|
|
"sdp": answer.SDP,
|
|
},
|
|
})
|
|
s.wsMu.Unlock()
|
|
|
|
s.sendAck(uid)
|
|
|
|
if s.onData == nil {
|
|
if err := s.sendSetSlots(); err != nil {
|
|
logger.Debugf("setSlots error: %v", err)
|
|
}
|
|
}
|
|
|
|
if !sendPub {
|
|
return nil
|
|
}
|
|
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
pubOffer, err := s.pcPub.CreateOffer(nil)
|
|
if err != nil {
|
|
return fmt.Errorf("create pub offer: %w", err)
|
|
}
|
|
if err := s.pcPub.SetLocalDescription(pubOffer); err != nil {
|
|
return fmt.Errorf("set local pub desc: %w", err)
|
|
}
|
|
|
|
s.wsMu.Lock()
|
|
_ = s.ws.WriteJSON(map[string]any{
|
|
keyUID: uuid.New().String(),
|
|
"publisherSdpOffer": map[string]any{
|
|
keyPcSeq: 1,
|
|
"sdp": pubOffer.SDP,
|
|
"tracks": s.publisherTrackDescriptions(),
|
|
},
|
|
})
|
|
s.wsMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) handleSdpAnswer(answer map[string]any, uid string) {
|
|
sdp, _ := answer["sdp"].(string)
|
|
if err := s.pcPub.SetRemoteDescription(webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer,
|
|
SDP: sdp,
|
|
}); err != nil {
|
|
logger.Debugf("SetRemoteDescription error: %v", err)
|
|
}
|
|
s.sendAck(uid)
|
|
}
|
|
|
|
func (s *Session) handleICE(cand map[string]any) {
|
|
candStr, _ := cand["candidate"].(string)
|
|
target, _ := cand["target"].(string)
|
|
sdpMid, _ := cand["sdpMid"].(string)
|
|
sdpMLineIndex, _ := cand["sdpMlineIndex"].(float64)
|
|
|
|
parts := strings.Fields(candStr)
|
|
if len(parts) < 8 {
|
|
return
|
|
}
|
|
|
|
init := webrtc.ICECandidateInit{
|
|
Candidate: candStr,
|
|
SDPMid: &sdpMid,
|
|
SDPMLineIndex: func() *uint16 { v := uint16(sdpMLineIndex); return &v }(),
|
|
}
|
|
switch target {
|
|
case "SUBSCRIBER":
|
|
_ = s.pcSub.AddICECandidate(init)
|
|
case "PUBLISHER":
|
|
_ = s.pcPub.AddICECandidate(init)
|
|
}
|
|
}
|
|
|
|
func (s *Session) setupICEHandlers() {
|
|
s.pcSub.OnICECandidate(func(c *webrtc.ICECandidate) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
init := c.ToJSON()
|
|
s.wsMu.Lock()
|
|
_ = s.ws.WriteJSON(map[string]any{
|
|
keyUID: uuid.New().String(),
|
|
"webrtcIceCandidate": map[string]any{
|
|
"candidate": init.Candidate,
|
|
"sdpMid": init.SDPMid,
|
|
"sdpMlineIndex": init.SDPMLineIndex,
|
|
"target": "SUBSCRIBER",
|
|
keyPcSeq: 1,
|
|
},
|
|
})
|
|
s.wsMu.Unlock()
|
|
})
|
|
|
|
s.pcPub.OnICECandidate(func(c *webrtc.ICECandidate) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
init := c.ToJSON()
|
|
s.wsMu.Lock()
|
|
_ = s.ws.WriteJSON(map[string]any{
|
|
keyUID: uuid.New().String(),
|
|
"webrtcIceCandidate": map[string]any{
|
|
"candidate": init.Candidate,
|
|
"sdpMid": init.SDPMid,
|
|
"sdpMlineIndex": init.SDPMLineIndex,
|
|
"target": "PUBLISHER",
|
|
keyPcSeq: 1,
|
|
},
|
|
})
|
|
s.wsMu.Unlock()
|
|
})
|
|
}
|
|
|
|
func (s *Session) sendSetSlots() error {
|
|
s.wsMu.Lock()
|
|
defer s.wsMu.Unlock()
|
|
|
|
// Goolom only forwards as many remote videos as the subscriber asks for via
|
|
// setSlots. Request a generous count so each subscriber sees every active
|
|
// publisher in the room.
|
|
slots := make([]map[string]int, 0, 8)
|
|
for range 8 {
|
|
slots = append(slots, map[string]int{"width": 1280, "height": 720})
|
|
}
|
|
if err := s.ws.WriteJSON(map[string]any{
|
|
keyUID: uuid.New().String(),
|
|
"setSlots": map[string]any{
|
|
"slots": slots,
|
|
"audioSlotsCount": 0,
|
|
"key": 1,
|
|
"shutdownAllVideo": nil,
|
|
"withSelfView": false,
|
|
"selfViewVisibility": "ON_LOADING_THEN_SHOW",
|
|
"gridConfig": map[string]any{},
|
|
},
|
|
}); err != nil {
|
|
return fmt.Errorf("write set slots: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) publisherTrackDescriptions() []map[string]any {
|
|
if s.pcPub == nil {
|
|
return nil
|
|
}
|
|
tracks := make([]map[string]any, 0)
|
|
for _, transceiver := range s.pcPub.GetTransceivers() {
|
|
sender := transceiver.Sender()
|
|
if sender == nil {
|
|
continue
|
|
}
|
|
track := sender.Track()
|
|
if track == nil {
|
|
continue
|
|
}
|
|
kind := "VIDEO"
|
|
if track.Kind() == webrtc.RTPCodecTypeAudio {
|
|
kind = "AUDIO"
|
|
}
|
|
tracks = append(tracks, map[string]any{
|
|
"mid": transceiver.Mid(),
|
|
"transceiverMid": transceiver.Mid(),
|
|
"kind": kind,
|
|
"priority": 0,
|
|
"label": track.ID(),
|
|
"codecs": map[string]any{},
|
|
"groupId": 1,
|
|
keyDescription: "",
|
|
})
|
|
}
|
|
return tracks
|
|
}
|
|
|
|
func isNonTURNURL(url string) bool {
|
|
return url != "" && !strings.HasPrefix(url, "turn:") && !strings.HasPrefix(url, "turns:")
|
|
}
|
|
|
|
func parseICEURLs(server map[string]any) []string {
|
|
var urls []string
|
|
switch rawURLs := server["urls"].(type) {
|
|
case []any:
|
|
for _, rawURL := range rawURLs {
|
|
if url, ok := rawURL.(string); ok && isNonTURNURL(url) {
|
|
urls = append(urls, url)
|
|
}
|
|
}
|
|
case []string:
|
|
for _, url := range rawURLs {
|
|
if isNonTURNURL(url) {
|
|
urls = append(urls, url)
|
|
}
|
|
}
|
|
}
|
|
return urls
|
|
}
|
|
|
|
func parseICEServer(rawServer any) (webrtc.ICEServer, bool) {
|
|
server, ok := rawServer.(map[string]any)
|
|
if !ok {
|
|
return webrtc.ICEServer{}, false
|
|
}
|
|
urls := parseICEURLs(server)
|
|
if len(urls) == 0 {
|
|
return webrtc.ICEServer{}, false
|
|
}
|
|
ice := webrtc.ICEServer{URLs: urls}
|
|
if username, ok := server["username"].(string); ok {
|
|
ice.Username = username
|
|
}
|
|
if credential, ok := server["credential"].(string); ok {
|
|
ice.Credential = credential
|
|
}
|
|
return ice, true
|
|
}
|
|
|
|
func (s *Session) applyServerHelloConfig(serverHello map[string]any) {
|
|
rawCfg, ok := serverHello["rtcConfiguration"].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
rawServers, ok := rawCfg["iceServers"].([]any)
|
|
if !ok || len(rawServers) == 0 {
|
|
return
|
|
}
|
|
iceServers := make([]webrtc.ICEServer, 0, len(rawServers))
|
|
for _, rawServer := range rawServers {
|
|
if ice, ok := parseICEServer(rawServer); ok {
|
|
iceServers = append(iceServers, ice)
|
|
}
|
|
}
|
|
if len(iceServers) == 0 {
|
|
return
|
|
}
|
|
cfg := webrtc.Configuration{
|
|
ICEServers: iceServers,
|
|
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
|
|
}
|
|
if s.pcSub != nil {
|
|
_ = s.pcSub.SetConfiguration(cfg)
|
|
}
|
|
if s.pcPub != nil {
|
|
_ = s.pcPub.SetConfiguration(cfg)
|
|
}
|
|
}
|