Files
olcrtc/internal/engine/goolom/media.go
zarazaex69 d65784ff8c refactor: migrate telemost to engine/goolom + auth/telemost
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>
2026-05-11 13:13:21 +03:00

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