telegabber/xmpp/jingle/convert_payload.go
2026-05-25 06:27:20 -07:00

268 lines
6.9 KiB
Go

package jingle
import (
"errors"
"fmt"
"strings"
)
// attr order follows jinglesdp.tcl
func appendDescriptionAttrs(d *Description, attrs *[]kv) error {
if d == nil {
return errors.New("content missing description")
}
if d.Media == "" {
return errors.New("description missing media")
}
for _, pt := range d.PayloadTypes {
rtpmap, err := payloadTypeToSDP(&pt)
if err != nil {
return err
}
*attrs = append(*attrs, kv{"rtpmap", rtpmap})
if len(pt.Parameters) >= 1 {
fmtp, err := paramsToFmtp(pt.ID, pt.Parameters)
if err != nil {
return err
}
*attrs = append(*attrs, kv{"fmtp", fmtp})
}
for _, fb := range pt.RTCPFbs {
s, err := fbToSDP(pt.ID, &fb)
if err != nil {
return err
}
*attrs = append(*attrs, kv{"rtcp-fb", s})
}
for _, fb := range pt.RTCPFbTrrInts {
*attrs = append(*attrs, kv{"rtcp-fb", pt.ID + " trr-int " + fb.Value})
}
}
for _, fb := range d.RTCPFbs {
s, err := fbToSDP("*", &fb)
if err != nil {
return err
}
*attrs = append(*attrs, kv{"rtcp-fb", s})
}
for _, fb := range d.RTCPFbTrrInts {
*attrs = append(*attrs, kv{"rtcp-fb", "* trr-int " + fb.Value})
}
for _, ext := range d.RTPHdrexts {
if ext.ID == "" || ext.URI == "" {
return errors.New("rtp-hdrext missing id or uri")
}
*attrs = append(*attrs, kv{"extmap", ext.ID + " " + ext.URI})
}
if d.ExtmapAllowMixed != nil {
*attrs = append(*attrs, kv{"extmap-allow-mixed", ""})
}
for _, sg := range d.SSRCGroups {
if strings.ContainsAny(sg.Semantics, " \t\r\n") {
return fmt.Errorf("ssrc-group semantics contains whitespace: %q", sg.Semantics)
}
ssrcs := make([]string, 0, len(sg.Sources))
for _, s := range sg.Sources {
ssrcs = append(ssrcs, s.SSRC)
}
*attrs = append(*attrs, kv{"ssrc-group", sg.Semantics + " " + strings.Join(ssrcs, " ")})
}
for _, src := range d.Sources {
for _, p := range src.Parameters {
if p.Name == "" {
return errors.New("source parameter missing name")
}
if strings.ContainsAny(p.Value, "\r\n") {
return errors.New("ssrc parameter value contains CR/LF")
}
if p.Value == "" {
*attrs = append(*attrs, kv{"ssrc", src.SSRC + " " + p.Name})
} else {
*attrs = append(*attrs, kv{"ssrc", src.SSRC + " " + p.Name + ":" + p.Value})
}
}
}
return nil
}
func buildJingleDescription(m *mediaSection) (*Description, error) {
desc := &Description{Media: m.media}
// "*" is the description-level key
fbMap := map[string][]RTCPFb{}
trrMap := map[string][]RTCPFbTrrInt{}
for _, val := range mmGetAll(m.attrs, "rtcp-fb") {
parts := strings.SplitN(val, " ", 3)
if len(parts) < 2 {
continue
}
id := parts[0]
typ := parts[1]
var sub string
if len(parts) >= 3 {
sub = parts[2]
}
if typ == "trr-int" {
trrMap[id] = append(trrMap[id], RTCPFbTrrInt{Value: sub})
} else {
fbMap[id] = append(fbMap[id], RTCPFb{Type: typ, Subtype: sub})
}
}
fmtpMap := map[string][]Parameter{}
for _, val := range mmGetAll(m.attrs, "fmtp") {
sp := strings.SplitN(val, " ", 2)
if len(sp) < 2 {
continue
}
id := sp[0]
rest := sp[1]
var params []Parameter
for _, pair := range strings.Split(rest, ";") {
kvSplit := strings.SplitN(pair, "=", 2)
if len(kvSplit) == 2 {
params = append(params, Parameter{Name: kvSplit[0], Value: kvSplit[1]})
}
}
fmtpMap[id] = params
}
// description-level rtcp-fb (id="*") goes first per Tcl/Java
for _, fb := range fbMap["*"] {
desc.RTCPFbs = append(desc.RTCPFbs, fb)
}
delete(fbMap, "*")
for _, fb := range trrMap["*"] {
desc.RTCPFbTrrInts = append(desc.RTCPFbTrrInts, fb)
}
delete(trrMap, "*")
for _, rtpmap := range mmGetAll(m.attrs, "rtpmap") {
pair := strings.SplitN(rtpmap, " ", 2)
if len(pair) < 2 {
continue
}
id := pair[0]
sp := strings.Split(pair[1], "/")
if len(sp) < 2 {
continue
}
pt := PayloadType{ID: id, Name: sp[0], Clockrate: sp[1]}
if len(sp) >= 3 && sp[2] != "1" {
pt.Channels = sp[2]
}
if params, ok := fmtpMap[id]; ok {
pt.Parameters = params
}
if fbs, ok := fbMap[id]; ok {
pt.RTCPFbs = fbs
}
if trrs, ok := trrMap[id]; ok {
pt.RTCPFbTrrInts = trrs
}
desc.PayloadTypes = append(desc.PayloadTypes, pt)
}
for _, extval := range mmGetAll(m.attrs, "extmap") {
sp := strings.SplitN(extval, " ", 2)
if len(sp) < 2 {
continue
}
desc.RTPHdrexts = append(desc.RTPHdrexts, RTPHdrext{ID: sp[0], URI: sp[1]})
}
if mmHas(m.attrs, "extmap-allow-mixed") {
desc.ExtmapAllowMixed = &ExtmapAllowMixed{}
}
for _, val := range mmGetAll(m.attrs, "ssrc-group") {
sp := strings.Fields(val)
if len(sp) < 2 {
continue
}
if strings.ContainsAny(sp[0], " \t\r\n") {
return nil, fmt.Errorf("ssrc-group semantics contains whitespace: %q", sp[0])
}
sg := SSRCGroup{Semantics: sp[0]}
for _, ssrc := range sp[1:] {
sg.Sources = append(sg.Sources, SSRCGroupSource{SSRC: ssrc})
}
desc.SSRCGroups = append(desc.SSRCGroups, sg)
}
// aggregate ssrc attrs by ssrc id, preserving first-seen order
sourceMap := map[string][]Parameter{}
var sourceOrder []string
for _, val := range mmGetAll(m.attrs, "ssrc") {
sp := strings.SplitN(val, " ", 2)
if len(sp) < 2 {
continue
}
sid := sp[0]
rest := sp[1]
kvParts := strings.SplitN(rest, ":", 2)
pn := kvParts[0]
var pv string
if len(kvParts) >= 2 {
pv = kvParts[1]
}
if pn == "" {
return nil, errors.New("ssrc attribute missing parameter name")
}
if strings.ContainsAny(pv, "\r\n") {
return nil, errors.New("ssrc parameter value contains CR/LF")
}
if _, ok := sourceMap[sid]; !ok {
sourceOrder = append(sourceOrder, sid)
}
sourceMap[sid] = append(sourceMap[sid], Parameter{Name: pn, Value: pv})
}
for _, sid := range sourceOrder {
desc.Sources = append(desc.Sources, Source{SSRC: sid, Parameters: sourceMap[sid]})
}
if mmHas(m.attrs, "rtcp-mux") {
desc.RtcpMux = &RtcpMux{}
}
return desc, nil
}
func payloadTypeToSDP(pt *PayloadType) (string, error) {
if pt.ID == "" {
return "", errors.New("payload-type missing id")
}
if pt.Name == "" {
return "", errors.New("payload-type missing name")
}
if pt.Channels == "" || pt.Channels == "1" {
return pt.ID + " " + pt.Name + "/" + pt.Clockrate, nil
}
return pt.ID + " " + pt.Name + "/" + pt.Clockrate + "/" + pt.Channels, nil
}
func fbToSDP(id string, fb *RTCPFb) (string, error) {
if fb.Type == "" {
return "", errors.New("rtcp-fb missing type")
}
if fb.Subtype == "" {
return id + " " + fb.Type, nil
}
return id + " " + fb.Type + " " + fb.Subtype, nil
}
func paramsToFmtp(id string, params []Parameter) (string, error) {
if len(params) == 1 {
p := params[0]
if p.Name == "" {
return id + " " + p.Value, nil
}
return id + " " + p.Name + "=" + p.Value, nil
}
parts := make([]string, 0, len(params))
for _, p := range params {
if p.Name == "" || p.Value == "" {
return "", errors.New("fmtp parameter missing name or value")
}
parts = append(parts, p.Name+"="+p.Value)
}
return id + " " + strings.Join(parts, ";"), nil
}