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