mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
249 lines
5.3 KiB
Go
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()
|
|
}
|
|
}
|