Files
olcrtc/internal/engine/goolom/signaling.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

304 lines
6.3 KiB
Go

package goolom
import (
"context"
"fmt"
"runtime"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/openlibrecommunity/olcrtc/internal/logger"
)
func (s *Session) sendHello() error {
hello := map[string]any{
keyUID: uuid.New().String(),
"hello": map[string]any{
"participantMeta": map[string]any{
keyName: s.name,
"role": "SPEAKER",
keyDescription: "",
"sendAudio": false,
"sendVideo": s.hasLocalVideoTracks(),
},
"participantAttributes": map[string]any{
keyName: s.name,
"role": "SPEAKER",
keyDescription: "",
},
"sendAudio": false,
"sendVideo": s.hasLocalVideoTracks(),
"sendSharing": false,
"participantId": s.peerID,
"roomId": s.roomID,
"serviceName": "telemost",
"credentials": s.credentials,
"capabilitiesOffer": goolomCapabilitiesOffer(),
"sdkInfo": map[string]any{
"implementation": "browser",
"version": "5.27.0",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
"hwConcurrency": runtime.NumCPU(),
},
"sdkInitializationId": uuid.New().String(),
"disablePublisher": !s.hasLocalVideoTracks(),
"disableSubscriber": false,
"disableSubscriberAudio": true,
},
}
s.wsMu.Lock()
defer s.wsMu.Unlock()
if err := s.ws.WriteJSON(hello); err != nil {
return fmt.Errorf("write hello: %w", err)
}
return nil
}
func (s *Session) handleSignaling(ctx context.Context) {
pubSent := false
for {
var msg map[string]any
if err := s.ws.ReadJSON(&msg); err != nil {
if !s.closed.Load() {
logger.Debugf("ws read error: %v", err)
s.queueReconnect()
}
return
}
s.updateWSDeadline()
uid, _ := msg[keyUID].(string)
s.handleMessageEvents(ctx, msg, uid)
if isConferenceEndMessage(msg) {
s.signalEnded("conference ended")
return
}
if offer, ok := msg["subscriberSdpOffer"].(map[string]any); ok {
if err := s.handleSdpOffer(offer, uid, !pubSent); err != nil {
logger.Debugf("sdp offer error: %v", err)
continue
}
pubSent = true
}
s.handleSignalingResponses(msg, uid)
}
}
func (s *Session) handleMessageEvents(ctx context.Context, msg map[string]any, uid string) {
if _, ok := msg["ack"]; ok {
s.resolveAck(uid)
}
if serverHello, ok := msg["serverHello"].(map[string]any); ok {
s.applyServerHelloConfig(serverHello)
s.startTelemetry(ctx, serverHello)
s.sendAck(uid)
}
s.handleCommonMessages(msg, uid)
}
func (s *Session) handleSignalingResponses(msg map[string]any, uid string) {
if answer, ok := msg["publisherSdpAnswer"].(map[string]any); ok {
s.handleSdpAnswer(answer, uid)
}
if cand, ok := msg["webrtcIceCandidate"].(map[string]any); ok {
s.handleICE(cand)
}
}
func (s *Session) updateWSDeadline() {
s.wsMu.Lock()
if s.ws != nil {
_ = s.ws.SetReadDeadline(time.Now().Add(wsReadTimeout))
}
s.wsMu.Unlock()
}
func (s *Session) handleCommonMessages(msg map[string]any, uid string) {
if _, ok := msg["updateDescription"]; ok {
s.sendAck(uid)
}
if _, ok := msg["vadActivity"]; ok {
s.sendAck(uid)
}
if _, ok := msg["ping"]; ok {
s.sendPong(uid)
}
if _, ok := msg["pong"]; ok {
s.sendAck(uid)
}
}
func (s *Session) sendAck(uid string) {
if uid == "" {
return
}
s.wsMu.Lock()
defer s.wsMu.Unlock()
_ = s.ws.WriteJSON(map[string]any{
keyUID: uid,
"ack": map[string]any{
"status": map[string]any{"code": "OK"},
},
})
}
func (s *Session) sendPong(uid string) {
s.wsMu.Lock()
defer s.wsMu.Unlock()
_ = s.ws.WriteJSON(map[string]any{
keyUID: uid,
"pong": map[string]any{},
})
}
func (s *Session) registerAckWaiter(uid string) chan struct{} {
ch := make(chan struct{})
s.ackMu.Lock()
s.ackWaiters[uid] = ch
s.ackMu.Unlock()
return ch
}
func (s *Session) removeAckWaiter(uid string) {
s.ackMu.Lock()
delete(s.ackWaiters, uid)
s.ackMu.Unlock()
}
func (s *Session) waitForAck(uid string, ch <-chan struct{}, timeout time.Duration) bool {
if uid == "" {
return false
}
defer s.removeAckWaiter(uid)
select {
case <-ch:
return true
case <-time.After(timeout):
return false
case <-s.closeCh:
return false
}
}
func (s *Session) resolveAck(uid string) {
if uid == "" {
return
}
s.ackMu.Lock()
ch := s.ackWaiters[uid]
if ch != nil {
delete(s.ackWaiters, uid)
close(ch)
}
s.ackMu.Unlock()
}
func (s *Session) sendLeave(uid string) bool {
s.wsMu.Lock()
defer s.wsMu.Unlock()
if s.ws == nil {
return false
}
leave := map[string]any{
keyUID: uid,
"leave": map[string]any{},
}
if err := s.ws.WriteJSON(leave); err != nil {
return false
}
return true
}
func (s *Session) keepAlive(keepAliveCh <-chan struct{}) {
wsTicker := time.NewTicker(30 * time.Second)
defer wsTicker.Stop()
appTicker := time.NewTicker(5 * time.Second)
defer appTicker.Stop()
for {
select {
case <-wsTicker.C:
if !s.sendWSPing() {
return
}
case <-appTicker.C:
if !s.sendAppPing() {
return
}
case <-keepAliveCh:
return
case <-s.closeCh:
return
}
}
}
func (s *Session) sendWSPing() bool {
s.wsMu.Lock()
defer s.wsMu.Unlock()
if s.ws != nil {
if err := s.ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
logger.Debugf("ws ping error: %v", err)
s.queueReconnect()
return false
}
}
return true
}
func (s *Session) sendAppPing() bool {
s.wsMu.Lock()
defer s.wsMu.Unlock()
if s.ws != nil {
if err := s.ws.WriteJSON(map[string]any{
keyUID: uuid.New().String(),
"ping": map[string]any{},
}); err != nil {
logger.Debugf("app ping error: %v", err)
s.queueReconnect()
return false
}
}
return true
}
func isConferenceEndMessage(msg map[string]any) bool {
for _, key := range []string{"conferenceClosed", "conferenceEnded", "roomClosed", "roomEnded", "callEnded"} {
if _, ok := msg[key]; ok {
return true
}
}
if raw, ok := msg["conference"].(map[string]any); ok {
if state, _ := raw["state"].(string); isEndedState(state) {
return true
}
}
if raw, ok := msg["conferenceState"].(map[string]any); ok {
if state, _ := raw["state"].(string); isEndedState(state) {
return true
}
}
return false
}
func isEndedState(state string) bool {
switch strings.ToLower(state) {
case "closed", "ended", "finished", stateTerminated:
return true
default:
return false
}
}