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