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

142 lines
3.4 KiB
Go

package audio
import (
"strconv"
"strings"
)
// opus fmtp params from peer SDP.
// ApplyTo honors UseInBandFEC, UseDTX, MaxAverageBitrate, MaxPlaybackRate.
// CBR/Stereo/Ptime are parsed but not applied (pipeline is fixed shape and
// hraban/opus.v2 doesn't wrap OPUS_SET_VBR).
type Fmtp struct {
UseInBandFEC bool // useinbandfec=1
UseDTX bool // usedtx=1
CBR bool // cbr=1 (not applied; see type doc)
MaxAverageBitrate int // maxaveragebitrate=<bps>, 0 = unspecified
MaxPlaybackRate int // maxplaybackrate=<Hz>, 0 = unspecified
SpropStereo bool // sprop-stereo=1 (not applied)
Stereo bool // stereo=1 (not applied)
MinPtime int // ms (not applied)
MaxPtime int // ms (not applied)
}
// params half of the opus a=fmtp line; empty if no opus fmtp.
// Two-pass because pion may emit rtpmap and fmtp in any order.
func ExtractOpusFmtp(sdp string) string {
opusPTs := make(map[string]bool)
type fmtpEntry struct {
pt string
params string
}
var fmtps []fmtpEntry
for _, raw := range strings.Split(sdp, "\n") {
line := strings.TrimRight(raw, "\r")
if !strings.HasPrefix(line, "a=") {
continue
}
body := line[2:]
if rest, ok := strings.CutPrefix(body, "rtpmap:"); ok {
// "<pt> opus/48000/2"
sp := strings.IndexByte(rest, ' ')
if sp < 0 {
continue
}
pt, codec := rest[:sp], rest[sp+1:]
if strings.HasPrefix(strings.ToLower(codec), "opus/") {
opusPTs[pt] = true
}
continue
}
if rest, ok := strings.CutPrefix(body, "fmtp:"); ok {
sp := strings.IndexByte(rest, ' ')
if sp < 0 {
continue
}
fmtps = append(fmtps, fmtpEntry{pt: rest[:sp], params: rest[sp+1:]})
}
}
for _, f := range fmtps {
if opusPTs[f.pt] {
return f.params
}
}
return ""
}
// unknown keys and malformed numerics are silently ignored; weird SDP
// shouldn't kill the call
func ParseFmtp(s string) Fmtp {
var f Fmtp
for _, kv := range strings.Split(s, ";") {
kv = strings.TrimSpace(kv)
if kv == "" {
continue
}
eq := strings.IndexByte(kv, '=')
if eq < 0 {
continue
}
key := strings.ToLower(strings.TrimSpace(kv[:eq]))
val := strings.TrimSpace(kv[eq+1:])
switch key {
case "useinbandfec":
f.UseInBandFEC = val == "1"
case "usedtx":
f.UseDTX = val == "1"
case "cbr":
f.CBR = val == "1"
case "maxaveragebitrate":
if n, err := strconv.Atoi(val); err == nil {
f.MaxAverageBitrate = n
}
case "maxplaybackrate":
if n, err := strconv.Atoi(val); err == nil {
f.MaxPlaybackRate = n
}
case "sprop-stereo":
f.SpropStereo = val == "1"
case "stereo":
f.Stereo = val == "1"
case "minptime":
if n, err := strconv.Atoi(val); err == nil {
f.MinPtime = n
}
case "maxptime":
if n, err := strconv.Atoi(val); err == nil {
f.MaxPtime = n
}
}
}
return f
}
func (f Fmtp) ApplyTo(e *Encoder) error {
bitrate := f.MaxAverageBitrate
if bitrate <= 0 {
bitrate = DefaultBitrate
}
if err := e.SetBitrate(bitrate); err != nil {
return err
}
if f.MaxPlaybackRate > 0 {
if err := e.SetMaxBandwidth(BandwidthForPlaybackRate(f.MaxPlaybackRate)); err != nil {
return err
}
}
if f.UseInBandFEC {
if err := e.SetInBandFEC(true); err != nil {
return err
}
// hint to opus, not a guarantee
if err := e.SetPacketLossPerc(5); err != nil {
return err
}
}
if f.UseDTX {
if err := e.SetDTX(true); err != nil {
return err
}
}
return nil
}