mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 20:17:08 +00:00
268 lines
6.9 KiB
Go
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
|
|
}
|