telegabber/calls/audio/ntgadapter/adapter.go
2026-05-27 08:37:47 -07:00

97 lines
2.6 KiB
Go

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