telegabber/xmpp/jingle/convert.go
2026-05-27 08:37:47 -07:00

225 lines
5.5 KiB
Go

// XEP-0166 Jingle stanzas <-> SDP strings
package jingle
import (
"errors"
"fmt"
"strings"
)
const (
hardcodedMediaProtocol = "UDP/TLS/RTP/SAVPF"
hardcodedMediaPort = "9"
hardcodedConnection = "IN IP4 0.0.0.0"
hardcodedOrigin = "- 8770656990916039506 2 IN IP4 127.0.0.1"
)
var (
defaultICEOptions = []string{"trickle"}
wellKnownICEOptions = map[string]bool{"trickle": true, "renomination": true}
)
// direction-flag resolution and metadata for SDPToJingle
type ConvertOpts struct {
// governs how senders=initiator/responder maps to sendonly/recvonly
Initiator bool
// <content creator='...'/> value; defaults to "initiator"
LocalCreator string
// <jingle sid='...'/> value
SessionID string
}
func JingleToSDP(j *JingleIQ, opts ConvertOpts) (string, error) {
if j == nil {
return "", errors.New("nil JingleIQ")
}
b := &sdpBuilder{}
b.line("v=0")
b.line("o=" + hardcodedOrigin)
b.line("s=-")
b.line("t=0 0")
sessAttrs, err := buildSessionAttrs(j)
if err != nil {
return "", err
}
b.appendAttrs(sessAttrs)
bundled := j.Group != nil && j.Group.Semantics != ""
for _, c := range j.Contents {
if c.Description == nil || c.Transport == nil {
continue
}
mediaAttrs, err := buildMediaAttrs(&c, bundled, opts)
if err != nil {
return "", err
}
formats := make([]string, 0, len(c.Description.PayloadTypes))
for _, pt := range c.Description.PayloadTypes {
formats = append(formats, pt.ID)
}
mline := "m=" + c.Description.Media + " " + hardcodedMediaPort + " " + hardcodedMediaProtocol
if len(formats) > 0 {
mline += " " + strings.Join(formats, " ")
}
b.line(mline)
b.line("c=" + hardcodedConnection)
b.appendAttrs(mediaAttrs)
}
return b.String(), nil
}
func SDPToJingle(sdp string, opts ConvertOpts) (*JingleIQ, error) {
doc, err := parseSDP(sdp)
if err != nil {
return nil, err
}
creator := opts.LocalCreator
if creator == "" {
creator = "initiator"
}
j := &JingleIQ{SID: opts.SessionID}
if groupVal := mmFirst(doc.sessionAttrs, "group", ""); groupVal != "" {
parts := strings.Fields(groupVal)
if len(parts) >= 1 {
sem := parts[0]
if strings.ContainsAny(sem, " \t\r\n") {
return nil, fmt.Errorf("group semantics contains whitespace: %q", sem)
}
g := &Group{Semantics: sem}
for _, name := range parts[1:] {
if strings.ContainsAny(name, " \t\r\n") {
return nil, fmt.Errorf("group content name contains whitespace: %q", name)
}
g.Contents = append(g.Contents, GroupContent{Name: name})
}
j.Group = g
}
}
for _, m := range doc.media {
content, err := buildJingleContent(&m, doc.sessionAttrs, creator, opts.Initiator)
if err != nil {
return nil, err
}
j.Contents = append(j.Contents, *content)
}
return j, nil
}
func buildSessionAttrs(j *JingleIQ) ([]kv, error) {
var attrs []kv
if j.Group != nil && j.Group.Semantics != "" {
sem := j.Group.Semantics
if strings.ContainsAny(sem, " \t\r\n") {
return nil, fmt.Errorf("group semantics contains whitespace: %q", sem)
}
tags := make([]string, 0, len(j.Group.Contents))
for _, gc := range j.Group.Contents {
if strings.ContainsAny(gc.Name, " \t\r\n") {
return nil, fmt.Errorf("group content name contains whitespace: %q", gc.Name)
}
tags = append(tags, gc.Name)
}
val := sem
if len(tags) > 0 {
val += " " + strings.Join(tags, " ")
}
attrs = append(attrs, kv{"group", val})
}
attrs = append(attrs, kv{"msid-semantic", " WMS my-media-stream"})
return attrs, nil
}
// attr order matches jinglesdp.tcl: transport, payload/feedback/ssrc,
// mid/direction/rtcp/candidates
func buildMediaAttrs(c *Content, bundled bool, opts ConvertOpts) ([]kv, error) {
var attrs []kv
if err := appendTransportAttrs(c.Transport, &attrs); err != nil {
return nil, err
}
if err := appendDescriptionAttrs(c.Description, &attrs); err != nil {
return nil, err
}
attrs = append(attrs, kv{"mid", c.Name})
attrs = append(attrs, kv{sendersToSDP(c.Senders, opts.Initiator), ""})
if c.Description.RtcpMux != nil || bundled {
attrs = append(attrs, kv{"rtcp-mux", ""})
}
attrs = append(attrs, kv{"rtcp", "9 IN IP4 0.0.0.0"})
for _, cand := range c.Transport.Candidates {
s, err := candidateToSDP(&cand)
if err != nil {
return nil, err
}
attrs = append(attrs, kv{"candidate", s})
}
return attrs, nil
}
func buildJingleContent(m *mediaSection, sessAttrs []kv, creator string, initiator bool) (*Content, error) {
name := mmFirst(m.attrs, "mid", m.media)
c := &Content{
Creator: creator,
Name: name,
}
if s := sendersFromMM(m.attrs, initiator); s != "both" {
c.Senders = s
}
desc, err := buildJingleDescription(m)
if err != nil {
return nil, err
}
c.Description = desc
transport, err := buildJingleTransport(m, sessAttrs)
if err != nil {
return nil, err
}
c.Transport = transport
return c, nil
}
// SDP direction attr name; value is empty because a=sendrecv is bare
func sendersToSDP(senders string, initiator bool) string {
switch senders {
case "", "both":
return "sendrecv"
case "none":
return "inactive"
case "initiator":
if initiator {
return "sendonly"
}
return "recvonly"
case "responder":
if initiator {
return "recvonly"
}
return "sendonly"
}
return "sendrecv"
}
func sendersFromMM(mm []kv, initiator bool) string {
if mmHas(mm, "sendrecv") {
return "both"
}
if mmHas(mm, "inactive") {
return "none"
}
if mmHas(mm, "sendonly") {
if initiator {
return "initiator"
}
return "responder"
}
if mmHas(mm, "recvonly") {
if initiator {
return "responder"
}
return "initiator"
}
return "both"
}