// adapts *ntgcalls.Client to audio.NtgClient; sub-package so audio/ stays // buildable without libntgcalls/CGO locally package ntgadapter import ( "sync" "dev.narayana.im/narayana/telegabber/calls/audio" "gotgcalls/ntgcalls" ) // wraps *ntgcalls.Client and filters OnFrame to (Playback, Microphone) - // the bridge only wants the peer's audio coming back via the external mic type Adapter struct { c *ntgcalls.Client mu sync.RWMutex handlers map[uint64]audio.NtgFrameHandler nextID uint64 wired bool } func New(c *ntgcalls.Client) *Adapter { return &Adapter{c: c, handlers: make(map[uint64]audio.NtgFrameHandler)} } // lazily wires the ntgcalls callback on first call; cancel drops from // the adapter-owned dispatch table (C++ binding's OnFrame is append-only) func (a *Adapter) OnFrame(h audio.NtgFrameHandler) func() { a.mu.Lock() id := a.nextID a.nextID++ a.handlers[id] = h wire := !a.wired a.wired = true a.mu.Unlock() if wire { a.c.OnFrame(func(chatID int64, mode ntgcalls.StreamMode, device ntgcalls.StreamDevice, frames []ntgcalls.Frame) { if mode != ntgcalls.PlaybackStream || device != ntgcalls.MicrophoneStream { return } pcm := make([]audio.PCMFrame, 0, len(frames)) for _, f := range frames { pcm = append(pcm, audio.PCMFrame{SSRC: f.Ssrc, Data: f.Data}) } a.mu.RLock() hs := make([]audio.NtgFrameHandler, 0, len(a.handlers)) for _, h := range a.handlers { hs = append(hs, h) } a.mu.RUnlock() for _, h := range hs { h(chatID, pcm) } }) } return func() { a.mu.Lock() delete(a.handlers, id) a.mu.Unlock() } } // uses the Microphone device slot in both directions (ntgcalls P2P pattern) func (a *Adapter) SetExternalMicrophone(chatID int64, capture bool, sampleRate uint32, channels uint8) error { desc := ntgcalls.MediaDescription{ Microphone: &ntgcalls.AudioDescription{ MediaSource: ntgcalls.MediaSourceExternal, SampleRate: sampleRate, ChannelCount: channels, }, } mode := ntgcalls.PlaybackStream if capture { mode = ntgcalls.CaptureStream } return a.c.SetStreamSources(chatID, mode, desc) } // errors only if both clears fail; partial cleanup is best-effort func (a *Adapter) ClearStreams(chatID int64) error { empty := ntgcalls.MediaDescription{} capErr := a.c.SetStreamSources(chatID, ntgcalls.CaptureStream, empty) pbErr := a.c.SetStreamSources(chatID, ntgcalls.PlaybackStream, empty) if capErr != nil && pbErr != nil { return capErr } return nil } func (a *Adapter) SendMicrophonePCM(chatID int64, pcm []byte) error { return a.c.SendExternalFrame(chatID, ntgcalls.MicrophoneStream, pcm, ntgcalls.FrameData{}) } var _ audio.NtgClient = (*Adapter)(nil)