mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 20:17:08 +00:00
160 lines
3.8 KiB
Go
160 lines
3.8 KiB
Go
// optional offline-replay dump of the two media streams; gated by
|
|
// CALLS_CAPTURE_DIR (no-op when unset). big-endian throughout, ts is ms
|
|
// since writer construction.
|
|
// header: [magic 8B][ver u8=1][reserved 7B]
|
|
// PCM rec (tg->xmpp): [ts_ms i64][ssrc u32][len u32][pcm...]
|
|
// RTP rec (xmpp->tg): [ts_ms i64][ssrc u32][seq u16][rtp_ts u32][pt u8][len u32][opus...]
|
|
package audio
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
captureEnvVar = "CALLS_CAPTURE_DIR"
|
|
captureVer = byte(1)
|
|
)
|
|
|
|
var (
|
|
magicPCM = [8]byte{'T', 'G', 'C', 'A', 'P', 'P', 'C', 'M'}
|
|
magicRTP = [8]byte{'T', 'G', 'C', 'A', 'P', 'R', 'T', 'P'}
|
|
)
|
|
|
|
// per-call file sink; nil-safe so call sites can stay branchless
|
|
type captureWriter struct {
|
|
mu sync.Mutex
|
|
f *os.File
|
|
start time.Time
|
|
closed bool
|
|
}
|
|
|
|
// returns (nil, nil) when CALLS_CAPTURE_DIR is unset
|
|
func newCaptureWriter(name string, magic [8]byte) (*captureWriter, error) {
|
|
dir := os.Getenv(captureEnvVar)
|
|
if dir == "" {
|
|
return nil, nil
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("capture: mkdir %s: %w", dir, err)
|
|
}
|
|
path := filepath.Join(dir, name)
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capture: create %s: %w", path, err)
|
|
}
|
|
var hdr [16]byte
|
|
copy(hdr[0:8], magic[:])
|
|
hdr[8] = captureVer
|
|
if _, err := f.Write(hdr[:]); err != nil {
|
|
_ = f.Close()
|
|
return nil, fmt.Errorf("capture: write header: %w", err)
|
|
}
|
|
return &captureWriter{f: f, start: time.Now()}, nil
|
|
}
|
|
|
|
func (w *captureWriter) writePCM(ssrc uint32, data []byte) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.closed {
|
|
return
|
|
}
|
|
var hdr [16]byte
|
|
binary.BigEndian.PutUint64(hdr[0:8], uint64(time.Since(w.start).Milliseconds()))
|
|
binary.BigEndian.PutUint32(hdr[8:12], ssrc)
|
|
binary.BigEndian.PutUint32(hdr[12:16], uint32(len(data)))
|
|
_, _ = w.f.Write(hdr[:])
|
|
_, _ = w.f.Write(data)
|
|
}
|
|
|
|
func (w *captureWriter) writeRTP(ssrc uint32, seq uint16, rtpTS uint32, pt uint8, data []byte) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.closed {
|
|
return
|
|
}
|
|
var hdr [23]byte
|
|
binary.BigEndian.PutUint64(hdr[0:8], uint64(time.Since(w.start).Milliseconds()))
|
|
binary.BigEndian.PutUint32(hdr[8:12], ssrc)
|
|
binary.BigEndian.PutUint16(hdr[12:14], seq)
|
|
binary.BigEndian.PutUint32(hdr[14:18], rtpTS)
|
|
hdr[18] = pt
|
|
binary.BigEndian.PutUint32(hdr[19:23], uint32(len(data)))
|
|
_, _ = w.f.Write(hdr[:])
|
|
_, _ = w.f.Write(data)
|
|
}
|
|
|
|
func (w *captureWriter) close() {
|
|
if w == nil {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.closed {
|
|
return
|
|
}
|
|
w.closed = true
|
|
_ = w.f.Close()
|
|
}
|
|
|
|
// one tg->xmpp record from a PCM capture file
|
|
type CapturedPCM struct {
|
|
TimestampMs int64
|
|
SSRC uint32
|
|
Data []byte
|
|
}
|
|
|
|
func ReadPCMCapture(r io.Reader) ([]CapturedPCM, error) {
|
|
if err := verifyMagic(r, magicPCM); err != nil {
|
|
return nil, err
|
|
}
|
|
var out []CapturedPCM
|
|
for {
|
|
var hdr [16]byte
|
|
_, err := io.ReadFull(r, hdr[:])
|
|
if errors.Is(err, io.EOF) {
|
|
return out, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rec := CapturedPCM{
|
|
TimestampMs: int64(binary.BigEndian.Uint64(hdr[0:8])),
|
|
SSRC: binary.BigEndian.Uint32(hdr[8:12]),
|
|
}
|
|
length := binary.BigEndian.Uint32(hdr[12:16])
|
|
rec.Data = make([]byte, length)
|
|
if _, err := io.ReadFull(r, rec.Data); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, rec)
|
|
}
|
|
}
|
|
|
|
func verifyMagic(r io.Reader, want [8]byte) error {
|
|
var hdr [16]byte
|
|
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
|
return fmt.Errorf("capture: read header: %w", err)
|
|
}
|
|
var got [8]byte
|
|
copy(got[:], hdr[0:8])
|
|
if got != want {
|
|
return fmt.Errorf("capture: bad magic %q, want %q", string(got[:]), string(want[:]))
|
|
}
|
|
if hdr[8] != captureVer {
|
|
return fmt.Errorf("capture: unsupported version %d", hdr[8])
|
|
}
|
|
return nil
|
|
}
|