// 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 // value; defaults to "initiator" LocalCreator string // 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" }