telegabber/calls/signaling/bridge.go
2026-05-27 08:37:47 -07:00

249 lines
5.3 KiB
Go

// coordinates one bridged call between a caller and a callee
package signaling
import (
"fmt"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
type State uint8
const (
StateIdle State = iota
StateOriginating
StateRinging
StateAccepted
StateEstablished
StateTerminated
)
func (s State) String() string {
names := [...]string{"idle", "originating", "ringing", "accepted", "established", "terminated"}
if int(s) < len(names) {
return names[s]
}
return fmt.Sprintf("state(%d)", uint8(s))
}
type TerminationReason uint8
const (
ReasonUnknown TerminationReason = iota
ReasonHangup
ReasonDecline
ReasonPeerGone
ReasonRingTimeout
ReasonExchangeTimeout
ReasonConnectTimeout
ReasonMediaFailed
ReasonBusy
)
type TimerKind uint8
const (
TimerRing TimerKind = iota
TimerExchange
TimerConnect
)
// which transport reported a media event; bridge waits for both
// before going to Established (otherwise the slow side lags the call)
type Side uint8
const (
CallerSide Side = iota
CalleeSide
)
type Caller interface {
Start()
Terminate(reason TerminationReason)
}
// Ringing tells the peer the callee is alerting; tg side is a no-op
type Callee interface {
Ringing()
Accept()
Terminate(reason TerminationReason)
}
// must call Bridge.Timeout on fire; Cancel is a no-op if not set
type Timers interface {
Set(kind TimerKind, dur time.Duration)
Cancel(kind TimerKind)
}
type Config struct {
Caller Caller
Callee Callee
Timers Timers
RingTimeout time.Duration // default 60s
ExchangeTimeout time.Duration // default 20s
ConnectTimeout time.Duration // default 15s
// fires once after both sides reported MediaConnected; invoked unlocked
OnEstablished func()
// fires once on transition into Terminated
OnTerminated func()
}
// caller/callee impls must not call back into Bridge synchronously
// from a method Bridge invoked - that deadlocks
type Bridge struct {
cfg Config
mu sync.Mutex
state State
callerMediaUp bool
calleeMediaUp bool
establishedAt time.Time
}
func New(cfg Config) *Bridge {
if cfg.RingTimeout == 0 {
cfg.RingTimeout = 60 * time.Second
}
if cfg.ExchangeTimeout == 0 {
cfg.ExchangeTimeout = 20 * time.Second
}
if cfg.ConnectTimeout == 0 {
cfg.ConnectTimeout = 15 * time.Second
}
return &Bridge{cfg: cfg, state: StateIdle}
}
func (b *Bridge) State() State {
b.mu.Lock()
defer b.mu.Unlock()
return b.state
}
// time both endpoints have been MediaConnected, or 0 if never established;
// tgsig passes this to tdlib DiscardCall so the call isn't classified as missed
func (b *Bridge) Duration() time.Duration {
b.mu.Lock()
defer b.mu.Unlock()
if b.establishedAt.IsZero() {
return 0
}
return time.Since(b.establishedAt)
}
func (b *Bridge) Start() {
b.mu.Lock()
if b.state != StateIdle {
st := b.state
b.mu.Unlock()
log.WithField("state", st).Warn("Bridge.Start: not idle, skipping")
return
}
b.state = StateOriginating
b.cfg.Timers.Set(TimerRing, b.cfg.RingTimeout)
b.mu.Unlock()
b.cfg.Caller.Start()
}
func (b *Bridge) Ringing() {
b.mu.Lock()
defer b.mu.Unlock()
if b.state != StateOriginating {
return
}
b.state = StateRinging
b.cfg.Callee.Ringing()
}
func (b *Bridge) CalleeAccepted() {
b.mu.Lock()
defer b.mu.Unlock()
if b.state != StateOriginating && b.state != StateRinging {
return
}
b.state = StateAccepted
b.cfg.Timers.Cancel(TimerRing)
b.cfg.Timers.Set(TimerExchange, b.cfg.ExchangeTimeout)
b.cfg.Callee.Accept()
}
// one transport endpoint is up; transition to Established only after both
func (b *Bridge) MediaConnected(side Side) {
var fireEstablished func()
b.mu.Lock()
if b.state == StateAccepted {
switch side {
case CallerSide:
b.callerMediaUp = true
case CalleeSide:
b.calleeMediaUp = true
}
if b.callerMediaUp && b.calleeMediaUp {
b.state = StateEstablished
b.establishedAt = time.Now()
b.cfg.Timers.Cancel(TimerExchange)
b.cfg.Timers.Cancel(TimerConnect)
fireEstablished = b.cfg.OnEstablished
} else {
// first side up; arm connect timer for the laggard
b.cfg.Timers.Cancel(TimerExchange)
b.cfg.Timers.Set(TimerConnect, b.cfg.ConnectTimeout)
}
}
// already Established: further reports no-op here, ConnectionState Failed
// still goes through MediaFailed
b.mu.Unlock()
if fireEstablished != nil {
fireEstablished()
}
}
func (b *Bridge) MediaFailed(reason TerminationReason) {
b.terminate(reason)
}
func (b *Bridge) Terminate(reason TerminationReason) {
b.terminate(reason)
}
func (b *Bridge) Timeout(kind TimerKind) {
var reason TerminationReason
switch kind {
case TimerRing:
reason = ReasonRingTimeout
case TimerExchange:
reason = ReasonExchangeTimeout
case TimerConnect:
reason = ReasonConnectTimeout
default:
return
}
b.terminate(reason)
}
func (b *Bridge) terminate(reason TerminationReason) {
b.mu.Lock()
if b.state == StateIdle || b.state == StateTerminated {
b.mu.Unlock()
return
}
log.WithFields(log.Fields{
"prev_state": b.state,
"reason": reason,
}).Info("Bridge terminating")
b.state = StateTerminated
b.cfg.Timers.Cancel(TimerRing)
b.cfg.Timers.Cancel(TimerExchange)
b.cfg.Timers.Cancel(TimerConnect)
b.mu.Unlock()
// callbacks run unlocked; re-entrant bridge methods bail on StateTerminated
b.cfg.Caller.Terminate(reason)
b.cfg.Callee.Terminate(reason)
if b.cfg.OnTerminated != nil {
b.cfg.OnTerminated()
}
}