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=, 0 = unspecified MaxPlaybackRate int // maxplaybackrate=, 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 { // " 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 }