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

130 lines
3.4 KiB
Go

package tgsig
import (
"sync"
"dev.narayana.im/narayana/telegabber/calls/audio"
"dev.narayana.im/narayana/telegabber/calls/signaling"
log "github.com/sirupsen/logrus"
"github.com/zelenin/go-tdlib/client"
)
// per-call tg-side state shared by Caller and Callee
type callBase struct {
adapter *Adapter
userID int64
isVideo bool
// CallerSide / CalleeSide - lets ntg callbacks fire MediaConnected per-side
side signaling.Side
mu sync.Mutex
bridge *signaling.Bridge
callID int32
}
// must run before any callback that re-enters the bridge
func (b *callBase) Bind(br *signaling.Bridge) {
b.mu.Lock()
b.bridge = br
b.mu.Unlock()
}
func (b *callBase) getBridge() *signaling.Bridge {
b.mu.Lock()
defer b.mu.Unlock()
return b.bridge
}
func (b *callBase) getCallID() int32 {
b.mu.Lock()
defer b.mu.Unlock()
return b.callID
}
func (b *callBase) setCallID(id int32) {
b.mu.Lock()
b.callID = id
b.mu.Unlock()
}
// audio-layer view of the ntgcalls client; nil for signaling-only tests
func (b *callBase) NtgClient() audio.NtgClient { return b.adapter.audioNtg }
// for P2P calls this is ntgcalls' "chatId" - the remote user_id
func (b *callBase) ChatID() int64 { return b.userID }
// ask tdlib to discard; map removal happens in cleanup from
// Manager.endCall once tdlib echoes Discarded.
//
// Duration matters: with 0 the call shows up as missed on other tdlib
// clients of the same account.
func (b *callBase) Terminate(reason signaling.TerminationReason) {
callID := b.getCallID()
if callID == 0 {
// CreateCall never succeeded
return
}
var duration int32
if br := b.getBridge(); br != nil {
duration = int32(br.Duration().Seconds())
}
b.adapter.withCallContext(func() {
if _, err := b.adapter.tdlib.DiscardCall(&client.DiscardCallRequest{
CallId: callID,
IsDisconnected: reason == signaling.ReasonMediaFailed,
Duration: duration,
IsVideo: b.isVideo,
ConnectionId: 0,
}); err != nil {
log.WithFields(log.Fields{
"call_id": callID,
"user_id": b.userID,
"reason": reason,
"duration": duration,
}).WithError(err).Error("DiscardCall failed")
}
})
}
// drives the ntgcalls P2P setup on tdlib's CallStateReady
func (b *callBase) onReady(state *client.CallStateReady, isOutgoing bool, entry *log.Entry) {
b.adapter.withCallContext(func() {
a := b.adapter
if err := a.ntg.CreateP2PCall(b.userID); err != nil {
entry.WithError(err).Error("Failed to create P2P call")
return
}
if err := a.ntg.SkipExchange(b.userID, state.EncryptionKey, isOutgoing); err != nil {
entry.WithError(err).Error("Failed to skip exchange")
_ = a.ntg.Stop(b.userID)
return
}
rtcServers := convertCallServers(state.Servers)
versions := state.Protocol.LibraryVersions
if err := a.ntg.ConnectP2P(b.userID, rtcServers, versions, state.AllowP2p); err != nil {
entry.WithError(err).Error("Failed to connect P2P")
_ = a.ntg.Stop(b.userID)
return
}
entry.Info("P2P connection established")
})
}
// drops the side from both maps and stops ntgcalls
func (b *callBase) cleanup() {
b.adapter.withCallContext(func() {
a := b.adapter
callID := b.getCallID()
if callID != 0 {
a.mgr.Drop(a.jid, callID)
}
a.dropSide(b.userID)
if err := a.ntg.Stop(b.userID); err != nil {
log.WithFields(log.Fields{
"call_id": callID,
"user_id": b.userID,
}).WithError(err).Debug("ntgcalls Stop returned error (call may not have started)")
}
})
}