package main import ( "context" "encoding/binary" "errors" "fmt" "io" "log" "math" "net" "os" "os/exec" "sync" "sync/atomic" "time" "github.com/pion/interceptor" "github.com/pion/rtp" "github.com/pion/stun/v4" "github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4/pkg/media" "golang.org/x/sys/unix" "gopkg.in/hraban/opus.v2" ) const ( rate = 48000 channels = 2 frameSamps = 960 // 20 ms per channel opusFmtp = "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;maxaveragebitrate=128000" ) type Call struct { pc *webrtc.PeerConnection track *webrtc.TrackLocalStaticSample ctx context.Context cancel context.CancelFunc once sync.Once started time.Time pcState atomic.Value // string iceState atomic.Value // string route atomic.Value // string: selected candidate pair outPeak atomic.Uint32 // mix-minus to guest, peak*1e4 since last status inPeak atomic.Uint32 // guest to show jb *jitterBuffer jbMu sync.Mutex sendOn atomic.Bool playOn atomic.Bool } var ( pubIPMu sync.Mutex pubIPCache string pubIPAt time.Time ) // publicIP returns the rig's public IPv4: -public-ip if set, else a STUN query (works no // matter how DNS / Cloudflare proxying is set up), else the A record of -public-host. func publicIP() (string, error) { if *publicIPFlag != "" { return *publicIPFlag, nil } pubIPMu.Lock() defer pubIPMu.Unlock() if pubIPCache != "" && time.Since(pubIPAt) < 10*time.Minute { return pubIPCache, nil } for _, srv := range []string{"stun.l.google.com:19302", "stun.cloudflare.com:3478"} { if ip, err := stunIP(srv); err == nil { pubIPCache, pubIPAt = ip, time.Now() return ip, nil } else { log.Printf("stun %s: %v", srv, err) } } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() ips, err := net.DefaultResolver.LookupIP(ctx, "ip4", *publicHost) if err != nil || len(ips) == 0 { return "", fmt.Errorf("no public IP (stun failed; resolve %s: %v)", *publicHost, err) } return ips[0].String(), nil } func stunIP(server string) (string, error) { conn, err := net.DialTimeout("udp4", server, 2*time.Second) if err != nil { return "", err } defer conn.Close() conn.SetDeadline(time.Now().Add(2 * time.Second)) msg := stun.MustBuild(stun.TransactionID, stun.BindingRequest) if _, err := conn.Write(msg.Raw); err != nil { return "", err } buf := make([]byte, 1500) n, err := conn.Read(buf) if err != nil { return "", err } res := &stun.Message{Raw: buf[:n]} if err := res.Decode(); err != nil { return "", err } var xor stun.XORMappedAddress if err := xor.GetFrom(res); err != nil { return "", err } if xor.IP.To4() == nil { return "", errors.New("not ipv4") } return xor.IP.String(), nil } func newAPI() (*webrtc.API, error) { m := &webrtc.MediaEngine{} if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: rate, Channels: 2, SDPFmtpLine: opusFmtp}, PayloadType: 111, }, webrtc.RTPCodecTypeAudio); err != nil { return nil, err } ir := &interceptor.Registry{} if err := webrtc.RegisterDefaultInterceptors(m, ir); err != nil { return nil, err } _, lan, err := net.ParseCIDR(*lanCIDR) if err != nil { return nil, err } se := webrtc.SettingEngine{} se.SetICEUDPMux(hub.mux) se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) se.SetIPFilter(func(ip net.IP) bool { return lan.Contains(ip) }) se.SetICETimeouts(8*time.Second, 25*time.Second, 2*time.Second) if pip, err := publicIP(); err == nil { // Keep the LAN host candidate and ADD one with the public IP on the same port. // Needs UDP forwarded to this machine on the router. if err := se.SetICEAddressRewriteRules(webrtc.ICEAddressRewriteRule{ External: []string{pip}, AsCandidateType: webrtc.ICECandidateTypeHost, Mode: webrtc.ICEAddressRewriteAppend, }); err != nil { return nil, err } } else { log.Printf("warning: %v - offering LAN candidate only", err) } return webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(ir), webrtc.WithSettingEngine(se)), nil } func newCall(offerSDP string) (*Call, string, error) { api, err := newAPI() if err != nil { return nil, "", err } pc, err := api.NewPeerConnection(webrtc.Configuration{}) if err != nil { return nil, "", err } ctx, cancel := context.WithCancel(context.Background()) c := &Call{pc: pc, ctx: ctx, cancel: cancel, started: time.Now()} c.pcState.Store("new") c.iceState.Store("new") c.route.Store("") fail := func(err error) (*Call, string, error) { c.Close(); return nil, "", err } if err := pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: offerSDP}); err != nil { return fail(err) } track, err := webrtc.NewTrackLocalStaticSample( webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: rate, Channels: 2, SDPFmtpLine: opusFmtp}, "audio", "mmo-mixminus") if err != nil { return fail(err) } c.track = track sender, err := pc.AddTrack(track) if err != nil { return fail(err) } go func() { // drain RTCP so interceptors work buf := make([]byte, 1500) for { if _, _, err := sender.Read(buf); err != nil { return } } }() pc.OnICEConnectionStateChange(func(s webrtc.ICEConnectionState) { c.iceState.Store(s.String()) log.Printf("ice: %s", s) if s == webrtc.ICEConnectionStateConnected { c.updateRoute() } }) pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { c.pcState.Store(s.String()) log.Printf("peer connection: %s", s) switch s { case webrtc.PeerConnectionStateConnected: c.updateRoute() c.startSend() case webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed: hub.mu.Lock() mine := hub.call == c hub.mu.Unlock() if mine { go hub.endCall() } } go hub.pushState() }) pc.OnTrack(func(t *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { if t.Kind() != webrtc.RTPCodecTypeAudio { return } log.Printf("guest track: %s %dHz ch=%d", t.Codec().MimeType, t.Codec().ClockRate, t.Codec().Channels) c.startPlay(t) }) answer, err := pc.CreateAnswer(nil) if err != nil { return fail(err) } gather := webrtc.GatheringCompletePromise(pc) if err := pc.SetLocalDescription(answer); err != nil { return fail(err) } select { case <-gather: case <-time.After(3 * time.Second): } return c, pc.LocalDescription().SDP, nil } func (c *Call) updateRoute() { for _, tr := range c.pc.GetTransceivers() { if s := tr.Sender(); s != nil && s.Transport() != nil { ice := s.Transport().ICETransport() if ice == nil { continue } if pair, err := ice.GetSelectedCandidatePair(); err == nil && pair != nil { r := fmt.Sprintf("%s:%d <-> %s:%d (%s)", pair.Local.Address, pair.Local.Port, pair.Remote.Address, pair.Remote.Port, pair.Remote.Typ) if old, _ := c.route.Swap(r).(string); old != r { log.Printf("route: %s", r) } return } } } } func (c *Call) Close() { c.once.Do(func() { c.cancel() c.pc.Close() log.Printf("call closed after %s", time.Since(c.started).Round(time.Second)) c.jbMu.Lock() jb := c.jb c.jbMu.Unlock() if jb != nil { k, _, t := jb.snapshot() log.Printf("call totals: lost %d (fec %d) concealed %d late %d underruns %d drift-drops %d inserted %d padding %d, final target %d ms", k.Lost, k.FEC, k.PLC, k.Late, k.Underruns, k.Dropped, k.Inserted, k.Padding, t) } }) } // logRx writes one line a minute about the guest's audio, so a bad night can be // diagnosed afterwards (v1 only showed these numbers live on the host console). func (c *Call) logRx(jb *jitterBuffer) { t := time.NewTicker(time.Minute) defer t.Stop() var prev jbCounters for { select { case <-c.ctx.Done(): return case <-t.C: } k, d, tg := jb.snapshot() log.Printf("rx 1m: buffer %d/%d ms, lost %d (fec %d), concealed %d, late %d, underruns %d, drift-drops %d, inserted %d, padding %d", d, tg, k.Lost-prev.Lost, k.FEC-prev.FEC, k.PLC-prev.PLC, k.Late-prev.Late, k.Underruns-prev.Underruns, k.Dropped-prev.Dropped, k.Inserted-prev.Inserted, k.Padding-prev.Padding) prev = k } } func (c *Call) status() map[string]any { st := map[string]any{ "pc": c.pcState.Load(), "ice": c.iceState.Load(), "route": c.route.Load(), "secs": int(time.Since(c.started).Seconds()), "sending": c.sendOn.Load(), "playing": c.playOn.Load(), "outPeak": float64(c.outPeak.Swap(0)) / 1e4, "inPeak": float64(c.inPeak.Swap(0)) / 1e4, } c.jbMu.Lock() if c.jb != nil { st["jb"] = c.jb.stats() } c.jbMu.Unlock() return st } func storeMax(a *atomic.Uint32, v uint32) { for { o := a.Load() if v <= o || a.CompareAndSwap(o, v) { return } } } func peakOf(pcm []int16) uint32 { var p int32 for _, s := range pcm { v := int32(s) if v < 0 { v = -v } if v > p { p = v } } return uint32(float64(p) / 32768 * 1e4) } // ---------------------------------------------------------------- mix-minus -> guest func (c *Call) startSend() { if c.sendOn.Swap(true) { return } go func() { defer c.sendOn.Store(false) for c.ctx.Err() == nil { if err := c.sendLoop(); err != nil && c.ctx.Err() == nil { log.Printf("send: %v - retrying in 1s", err) select { case <-c.ctx.Done(): case <-time.After(time.Second): } } } }() } func (c *Call) sendLoop() error { cmd := exec.CommandContext(c.ctx, "parec", "-d", *sourceName, "--raw", "--format=s16le", fmt.Sprintf("--rate=%d", rate), fmt.Sprintf("--channels=%d", channels), "--latency-msec=20", "--property=node.dont-fallback=true", "--property=node.dont-reconnect=true", "--property=node.dont-move=true", "--client-name=guestlink", "--stream-name=mix-minus to guest") out, err := cmd.StdoutPipe() if err != nil { return err } cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return err } defer cmd.Wait() defer cmd.Process.Kill() enc, err := opus.NewEncoder(rate, channels, opus.AppAudio) if err != nil { return err } enc.SetBitrate(128000) enc.SetInBandFEC(true) enc.SetPacketLossPerc(5) enc.SetComplexity(9) raw := make([]byte, frameSamps*channels*2) pcm := make([]int16, frameSamps*channels) pkt := make([]byte, 4000) for { if _, err := io.ReadFull(out, raw); err != nil { if c.ctx.Err() != nil { return nil } return fmt.Errorf("parec read: %w", err) } for i := range pcm { pcm[i] = int16(binary.LittleEndian.Uint16(raw[i*2:])) } storeMax(&c.outPeak, peakOf(pcm)) n, err := enc.Encode(pcm, pkt) if err != nil { return fmt.Errorf("opus encode: %w", err) } data := make([]byte, n) copy(data, pkt[:n]) if err := c.track.WriteSample(media.Sample{Data: data, Duration: 20 * time.Millisecond}); err != nil && !errors.Is(err, io.ErrClosedPipe) { return fmt.Errorf("write sample: %w", err) } } } // ---------------------------------------------------------------- guest -> show func (c *Call) startPlay(t *webrtc.TrackRemote) { dec, err := opus.NewDecoder(rate, channels) if err != nil { log.Printf("decoder: %v", err) return } jb := newJitterBuffer(dec, uint8(t.PayloadType())) c.jbMu.Lock() c.jb = jb c.jbMu.Unlock() go c.logRx(jb) go func() { // network -> jitter buffer for { p, _, err := t.ReadRTP() if err != nil { return } jb.push(p) } }() go func() { // jitter buffer -> pacat, paced by the MOTU clock via a tiny pipe c.playOn.Store(true) defer c.playOn.Store(false) for c.ctx.Err() == nil { if err := c.playLoop(jb); err != nil && c.ctx.Err() == nil { log.Printf("play: %v - retrying in 1s", err) select { case <-c.ctx.Done(): case <-time.After(time.Second): } } } }() } func (c *Call) playLoop(jb *jitterBuffer) error { if *testPlayoutFile != "" { return c.playToFile(jb) } r, w, err := os.Pipe() if err != nil { return err } // Small pipe => writes block on the sink's consumption => the MOTU clock paces us, // and the jitter buffer depth tells us about clock drift. unix.FcntlInt(w.Fd(), unix.F_SETPIPE_SZ, 4096) cmd := exec.CommandContext(c.ctx, "pacat", "--playback", "-d", *sinkName, "--raw", "--format=s16le", fmt.Sprintf("--rate=%d", rate), fmt.Sprintf("--channels=%d", channels), "--latency-msec=30", "--property=node.dont-fallback=true", "--property=node.dont-reconnect=true", "--property=node.dont-move=true", "--client-name=guestlink", "--stream-name=guest voice") cmd.Stdin = r cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { r.Close() w.Close() return err } r.Close() defer cmd.Wait() defer cmd.Process.Kill() defer w.Close() buf := make([]byte, 0, 5760*channels*2) for c.ctx.Err() == nil { pcm := jb.next() storeMax(&c.inPeak, peakOf(pcm)) buf = buf[:0] for _, s := range pcm { buf = binary.LittleEndian.AppendUint16(buf, uint16(s)) } if _, err := w.Write(buf); err != nil { if c.ctx.Err() != nil { return nil } return fmt.Errorf("pacat write: %w", err) } } return nil } // playToFile is a test path: playout to a raw PCM file paced by a 20 ms ticker instead of // the sound card, so the receive chain can be verified without touching PipeWire. func (c *Call) playToFile(jb *jitterBuffer) error { f, err := os.Create(*testPlayoutFile) if err != nil { return err } defer f.Close() t := time.NewTicker(20 * time.Millisecond) defer t.Stop() buf := make([]byte, 0, 5760*channels*2) for { select { case <-c.ctx.Done(): return nil case <-t.C: } pcm := jb.next() storeMax(&c.inPeak, peakOf(pcm)) buf = buf[:0] for _, s := range pcm { buf = binary.LittleEndian.AppendUint16(buf, uint16(s)) } f.Write(buf) } } // ---------------------------------------------------------------- jitter buffer // jitterBuffer reorders Opus packets, conceals losses (FEC, then PLC), adapts its depth to // network jitter, and corrects clock drift between the guest's sound card and the MOTU. // // It is pulled once per 20 ms by the MOTU clock. Rules (v2, 2026-11-04): // - Underrun (nothing buffered): HOLD the sequence position and refill to the target // before playing again. The delayed packets are then played, not discarded as late. // (v1 skipped ahead and kept playing from an empty buffer, so every later wobble // became another glitch - ~12/min with a guest clock 30 ppm slow.) // - Too deep (fast guest clock / after a stall): discard quiet frames. // - Too shallow (slow guest clock): insert a concealment frame during a quiet moment. // - Underruns and late packets raise the target; it relaxes after 2 calm minutes. type jitterBuffer struct { mu sync.Mutex dec *opus.Decoder pkts map[uint16][]byte skip map[uint16]bool // sequence numbers that carry no audio (padding/probes) pt uint8 // the negotiated Opus payload type nextSeq uint16 haveSeq bool // nextSeq is meaningful playing bool // false = filling up to target target int // frames gap int // frames produced while refilling lastBad time.Time lastBump time.Time lateRun int insertNow bool floor float64 // running estimate of the guest's background-noise RMS pcm []int16 c jbCounters } type jbCounters struct { Lost, FEC, PLC, Dropped, Inserted, Underruns, Late int Padding int // packets with no audio (skipped, not concealed) } const ( jbFloor = 4 // 80 ms minimum target jbMax = 15 // 300 ms maximum target quietRMS = 0.003 // always counts as quiet (-50 dBFS) quietCap = 0.02 // never counts as quiet (-34 dBFS) ) // isQuiet: below -50 dBFS, or within ~6 dB of the guest's own noise floor (so drift // correction still works for a guest in a noisy room), but never above -34 dBFS. func (j *jitterBuffer) isQuiet(pcm []int16) bool { r := rms(pcm) if r < j.floor { j.floor = r } else { j.floor *= 1.0005 // creeps up ~2.5%/s so it follows a rising floor } lim := math.Min(quietCap, math.Max(quietRMS, 2*j.floor)) return r < lim } func newJitterBuffer(dec *opus.Decoder, pt uint8) *jitterBuffer { return &jitterBuffer{dec: dec, pt: pt, pkts: map[uint16][]byte{}, skip: map[uint16]bool{}, target: jbFloor, pcm: make([]int16, 5760*channels), lastBad: time.Now(), floor: quietRMS} } func seqLess(a, b uint16) bool { return int16(a-b) < 0 } func (j *jitterBuffer) reset() { j.pkts = map[uint16][]byte{} j.skip = map[uint16]bool{} j.haveSeq, j.playing, j.lateRun, j.insertNow = false, false, 0, false } // bump raises the target after an underrun or a late packet (at most once a second, so a // burst of late packets counts once). func (j *jitterBuffer) bump() { j.lastBad = time.Now() if j.target < jbMax && time.Since(j.lastBump) > time.Second { j.target++ j.lastBump = time.Now() } } func (j *jitterBuffer) push(p *rtp.Packet) { j.mu.Lock() defer j.mu.Unlock() if len(p.Payload) == 0 || (j.pt != 0 && p.PayloadType != j.pt) { // Padding / bandwidth probe: it uses a sequence number but carries no audio and no // time. v1/v2a ignored it, which left a hole that was then "concealed" - an // invented 20 ms frame that the drift correction later had to remove again. j.c.Padding++ if !j.haveSeq || !seqLess(p.SequenceNumber, j.nextSeq) { if len(j.skip) > 200 { j.skip = map[uint16]bool{} } j.skip[p.SequenceNumber] = true } return } if j.haveSeq { if seqLess(p.SequenceNumber, j.nextSeq) { j.c.Late++ j.bump() if j.lateRun++; j.lateRun > 50 { // sender restarted its sequence numbers j.reset() } return } if d := uint16(p.SequenceNumber - j.nextSeq); d > 500 { // stream restarted j.reset() } } j.lateRun = 0 if len(j.pkts) > 100 { // hard cap - should never happen j.reset() } j.pkts[p.SequenceNumber] = append([]byte(nil), p.Payload...) } func (j *jitterBuffer) oldest() uint16 { first := true var m uint16 for s := range j.pkts { if first || seqLess(s, m) { m, first = s, false } } return m } func (j *jitterBuffer) silence() []int16 { return make([]int16, frameSamps*channels) } func (j *jitterBuffer) decode(p []byte) []int16 { n, err := j.dec.Decode(p, j.pcm) if err != nil || n <= 0 { return j.conceal() } return append([]int16(nil), j.pcm[:n*channels]...) } func (j *jitterBuffer) plcFrame() []int16 { out := make([]int16, frameSamps*channels) if err := j.dec.DecodePLC(out); err != nil { return j.silence() } return out } func (j *jitterBuffer) conceal() []int16 { j.c.PLC++; return j.plcFrame() } func rms(pcm []int16) float64 { if len(pcm) == 0 { return 0 } var s float64 for _, v := range pcm { f := float64(v) / 32768 s += f * f } return math.Sqrt(s / float64(len(pcm))) } // fill is played while the buffer refills: a few concealment frames (Opus PLC fades out // naturally), then silence. func (j *jitterBuffer) fill() []int16 { j.gap++ if j.haveSeq && j.gap <= 3 { return j.conceal() } return j.silence() } // next returns the next block of interleaved PCM to play (normally 20 ms). func (j *jitterBuffer) next() []int16 { j.mu.Lock() defer j.mu.Unlock() // Relax the target after two calm minutes. if j.target > jbFloor && time.Since(j.lastBad) > 2*time.Minute { j.target-- j.lastBad = time.Now() } if !j.playing { if len(j.pkts) < j.target { return j.fill() } if !j.haveSeq { j.nextSeq, j.haveSeq = j.oldest(), true } j.playing, j.gap = true, 0 } if j.insertNow { // slow guest clock: stretch a quiet moment by one frame j.insertNow = false j.c.Inserted++ return j.plcFrame() } for { if j.skip[j.nextSeq] { delete(j.skip, j.nextSeq) j.nextSeq++ continue } if p, ok := j.pkts[j.nextSeq]; ok { delete(j.pkts, j.nextSeq) j.nextSeq++ pcm := j.decode(p) depth := len(j.pkts) quiet := j.isQuiet(pcm) if (depth > j.target+2 && quiet) || depth > j.target+10 { j.c.Dropped++ continue } if depth < j.target-1 && quiet { j.insertNow = true } return pcm } if len(j.pkts) == 0 { // Underrun: keep our place and refill to a larger target. j.c.Underruns++ j.lastBump = time.Time{} // an underrun always counts j.bump() j.playing = false return j.fill() } // Gap with later packets present: this packet is lost (or too late to wait for). j.c.Lost++ if fp, ok := j.pkts[j.nextSeq+1]; ok { out := make([]int16, frameSamps*channels) if err := j.dec.DecodeFEC(fp, out); err == nil { j.c.FEC++ j.nextSeq++ return out } } j.nextSeq++ return j.conceal() } } func (j *jitterBuffer) snapshot() (c jbCounters, depthMs, targetMs int) { j.mu.Lock() defer j.mu.Unlock() return j.c, len(j.pkts) * 20, j.target * 20 } func (j *jitterBuffer) stats() map[string]any { c, d, t := j.snapshot() return map[string]any{ "depthMs": d, "targetMs": t, "lost": c.Lost, "fec": c.FEC, "plc": c.PLC, "dropped": c.Dropped, "inserted": c.Inserted, "underruns": c.Underruns, "late": c.Late, "padding": c.Padding, } }