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 }