package sdp
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
)
func GetMidValue(media *sdp.MediaDescription) string {
for _, attr := range media.Attributes {
if attr.Key == sdp.AttrKeyMID {
return attr.Value
}
}
return ""
}
func ExtractFingerprint(desc *sdp.SessionDescription) (string, string, error) {
fingerprints := make([]string, 0)
if fingerprint, haveFingerprint := desc.Attribute("fingerprint"); haveFingerprint {
fingerprints = append(fingerprints, fingerprint)
}
for _, m := range desc.MediaDescriptions {
if fingerprint, haveFingerprint := m.Attribute("fingerprint"); haveFingerprint {
fingerprints = append(fingerprints, fingerprint)
}
}
if len(fingerprints) < 1 {
return "", "", webrtc.ErrSessionDescriptionNoFingerprint
}
for _, m := range fingerprints {
if m != fingerprints[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingFingerprints
}
}
parts := strings.Split(fingerprints[0], " ")
if len(parts) != 2 {
return "", "", webrtc.ErrSessionDescriptionInvalidFingerprint
}
return parts[1], parts[0], nil
}
func ExtractDTLSRole(desc *sdp.SessionDescription) webrtc.DTLSRole {
for _, md := range desc.MediaDescriptions {
setup, ok := md.Attribute(sdp.AttrKeyConnectionSetup)
if !ok {
continue
}
if setup == sdp.ConnectionRoleActive.String() {
return webrtc.DTLSRoleClient
}
if setup == sdp.ConnectionRolePassive.String() {
return webrtc.DTLSRoleServer
}
}
return webrtc.DTLSRoleClient
}
func ExtractICECredential(desc *sdp.SessionDescription) (string, string, error) {
pwds := []string{}
ufrags := []string{}
if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag {
ufrags = append(ufrags, ufrag)
}
if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd {
pwds = append(pwds, pwd)
}
for _, m := range desc.MediaDescriptions {
if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag {
ufrags = append(ufrags, ufrag)
}
if pwd, havePwd := m.Attribute("ice-pwd"); havePwd {
pwds = append(pwds, pwd)
}
}
if len(ufrags) == 0 {
return "", "", webrtc.ErrSessionDescriptionMissingIceUfrag
} else if len(pwds) == 0 {
return "", "", webrtc.ErrSessionDescriptionMissingIcePwd
}
for _, m := range ufrags {
if m != ufrags[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingIceUfrag
}
}
for _, m := range pwds {
if m != pwds[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingIcePwd
}
}
return ufrags[0], pwds[0], nil
}
func ExtractStreamID(media *sdp.MediaDescription) (string, bool) {
var streamID string
msid, ok := media.Attribute(sdp.AttrKeyMsid)
if !ok {
return "", false
}
ids := strings.Split(msid, " ")
if len(ids) < 2 {
streamID = msid
} else {
streamID = ids[1]
}
return streamID, true
}
func GetIP(sdp *sdp.SessionDescription) string {
if sdp.ConnectionInformation != nil && sdp.ConnectionInformation.NetworkType == "IN" {
return sdp.ConnectionInformation.Address.Address
}
for _, media := range sdp.MediaDescriptions {
if media.ConnectionInformation != nil && media.ConnectionInformation.NetworkType == "IN" {
return media.ConnectionInformation.Address.Address
}
}
return ""
}
func GetMediaStreamTrack(m *sdp.MediaDescription) string {
mst := ""
msid, ok := m.Attribute(sdp.AttrKeyMsid)
if ok {
if parts := strings.Split(msid, " "); len(parts) == 2 {
mst = parts[1]
}
}
if mst == "" {
attr, ok := m.Attribute(sdp.AttrKeySSRC)
if ok {
parts := strings.Split(attr, " ")
if len(parts) == 3 && strings.HasPrefix(strings.ToLower(parts[1]), "msid:") {
mst = parts[2]
}
}
}
return mst
}
func GetSimulcastRids(m *sdp.MediaDescription) ([]string, bool) {
val, ok := m.Attribute("simulcast")
if !ok {
return nil, false
}
parts := strings.Split(val, " ")
if len(parts) != 2 || parts[0] != "send" {
return nil, false
}
return strings.Split(parts[1], ";"), true
}
func CodecsFromMediaDescription(m *sdp.MediaDescription) (out []sdp.Codec, err error) {
s := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{m},
}
for _, payloadStr := range m.MediaName.Formats {
payloadType, err := strconv.ParseUint(payloadStr, 10, 8)
if err != nil {
return nil, err
}
codec, err := s.GetCodecForPayloadType(uint8(payloadType))
if err != nil {
if payloadType == 0 {
continue
}
return nil, err
}
out = append(out, codec)
}
return out, nil
}
func GetBundleMid(parsed *sdp.SessionDescription) (string, bool) {
if groupAttribute, found := parsed.Attribute(sdp.AttrKeyGroup); found {
bundleIDs := strings.Split(groupAttribute, " ")
if len(bundleIDs) > 1 && strings.EqualFold(bundleIDs[0], "BUNDLE") {
return bundleIDs[1], true
}
}
return "", false
}
type sdpFragmentICE struct {
ufrag string
pwd string
lite *bool
options string
}
func (i *sdpFragmentICE) Unmarshal(attributes []sdp.Attribute) error {
getAttr := func(key string) (string, bool) {
for _, a := range attributes {
if a.Key == key {
return a.Value, true
}
}
return "", false
}
iceUfrag, found := getAttr("ice-ufrag")
if found {
i.ufrag = iceUfrag
}
icePwd, found := getAttr("ice-pwd")
if found {
i.pwd = icePwd
}
_, found = getAttr(sdp.AttrKeyICELite)
if found {
lite := true
i.lite = &lite
}
iceOptions, found := getAttr("ice-options")
if found {
i.options = iceOptions
}
return nil
}
func (i *sdpFragmentICE) Marshal() (string, error) {
iceFragment := []byte{}
addKeyValue := func(key string, value string) {
iceFragment = append(iceFragment, key...)
if value != "" {
iceFragment = append(iceFragment, value...)
}
iceFragment = append(iceFragment, "\r\n"...)
}
if i.ufrag != "" {
addKeyValue("a=ice-ufrag:", i.ufrag)
}
if i.pwd != "" {
addKeyValue("a=ice-pwd:", i.pwd)
}
if i.lite != nil && *i.lite {
addKeyValue("a=ice-lite", "")
}
if i.options != "" {
addKeyValue("a=ice-options:", i.options)
}
return string(iceFragment), nil
}
type sdpFragmentMedia struct {
info string
mid string
ice *sdpFragmentICE
candidates []string
endOfCandidates *bool
}
func (m *sdpFragmentMedia) Unmarshal(md *sdp.MediaDescription) error {
var info []byte
appendList := func(list []string, sep byte) {
for i, p := range list {
if i != 0 && i != len(list) {
info = append(info, sep)
}
info = append(info, p...)
}
}
info = append(append(info, md.MediaName.Media...), ' ')
info = strconv.AppendInt(info, int64(md.MediaName.Port.Value), 10)
if md.MediaName.Port.Range != nil {
info = append(info, '/')
info = strconv.AppendInt(info, int64(*md.MediaName.Port.Range), 10)
}
info = append(info, ' ')
appendList(md.MediaName.Protos, '/')
info = append(info, ' ')
appendList(md.MediaName.Formats, ' ')
m.info = string(info)
mid, found := md.Attribute(sdp.AttrKeyMID)
if found {
m.mid = mid
}
m.ice = &sdpFragmentICE{}
if err := m.ice.Unmarshal(md.Attributes); err != nil {
return err
}
for _, a := range md.Attributes {
if a.IsICECandidate() {
m.candidates = append(m.candidates, a.Value)
}
}
_, found = md.Attribute(sdp.AttrKeyEndOfCandidates)
if found {
endOfCandidates := true
m.endOfCandidates = &endOfCandidates
}
return nil
}
func (m *sdpFragmentMedia) Marshal() (string, error) {
mediaFragment := []byte{}
addKeyValue := func(key string, value string) {
mediaFragment = append(mediaFragment, key...)
if value != "" {
mediaFragment = append(mediaFragment, value...)
}
mediaFragment = append(mediaFragment, "\r\n"...)
}
if m.info != "" {
addKeyValue("m=", m.info)
}
if m.mid != "" {
addKeyValue("a=mid:", m.mid)
}
if m.ice != nil {
iceFragment, err := m.ice.Marshal()
if err != nil {
return "", err
}
mediaFragment = append(mediaFragment, iceFragment...)
}
for _, c := range m.candidates {
addKeyValue("a=candidate:", c)
}
if m.endOfCandidates != nil && *m.endOfCandidates {
addKeyValue("a=end-of-candidates", "")
}
return string(mediaFragment), nil
}
type SDPFragment struct {
group string
ice *sdpFragmentICE
media *sdpFragmentMedia
}
func (s *SDPFragment) Unmarshal(frag string) error {
s.ice = &sdpFragmentICE{}
lines := strings.Split(frag, "\n")
for _, line := range lines {
line = strings.TrimRight(line, " \r")
if len(line) == 0 {
continue
}
if line[0] == 'm' {
if s.media != nil {
return errors.New("too many media sections")
}
s.media = &sdpFragmentMedia{}
s.media.ice = &sdpFragmentICE{}
s.media.info = line[2:]
}
if line[0] != 'a' {
continue
}
if line[1] != '=' {
return errors.New("invalid attribute")
}
line = line[2:]
delimIndex := strings.Index(line, ":")
if delimIndex < 0 {
if line == sdp.AttrKeyICELite {
lite := true
if s.media != nil {
s.media.ice.lite = &lite
} else {
s.ice.lite = &lite
}
}
continue
}
value := line[delimIndex+1:]
switch line[:delimIndex] {
case sdp.AttrKeyGroup:
s.group = value
case "ice-ufrag":
if s.media != nil {
s.media.ice.ufrag = value
} else {
s.ice.ufrag = value
}
case "ice-pwd":
if s.media != nil {
s.media.ice.pwd = value
} else {
s.ice.pwd = value
}
case "ice-options":
if s.media != nil {
s.media.ice.options = value
} else {
s.ice.options = value
}
case sdp.AttrKeyMID:
if s.media != nil {
s.media.mid = value
}
case sdp.AttrKeyCandidate:
if s.media != nil {
s.media.candidates = append(s.media.candidates, value)
}
case sdp.AttrKeyEndOfCandidates:
endOfCandidates := true
if s.media != nil {
s.media.endOfCandidates = &endOfCandidates
}
}
}
if s.media == nil {
return errors.New("missing media section")
}
if s.group != "" {
bundleIDs := strings.Split(s.group, " ")
if len(bundleIDs) > 1 && strings.EqualFold(bundleIDs[0], "BUNDLE") {
if s.media.mid != bundleIDs[1] {
return fmt.Errorf("bundle media mismatch, expected: %s, got: %s", bundleIDs[1], s.media.mid)
}
}
}
return nil
}
func (s *SDPFragment) Marshal() (string, error) {
sdpFragment := []byte{}
addKeyValue := func(key string, value string) {
sdpFragment = append(sdpFragment, key...)
if value != "" {
sdpFragment = append(sdpFragment, value...)
}
sdpFragment = append(sdpFragment, "\r\n"...)
}
if s.group != "" {
addKeyValue("a=group:", s.group)
}
if s.ice != nil {
iceFragment, err := s.ice.Marshal()
if err != nil {
return "", err
}
sdpFragment = append(sdpFragment, iceFragment...)
}
if s.media != nil {
mediaFragment, err := s.media.Marshal()
if err != nil {
return "", err
}
sdpFragment = append(sdpFragment, mediaFragment...)
}
return string(sdpFragment), nil
}
func (s *SDPFragment) Mid() string {
if s.media != nil {
return s.media.mid
}
return ""
}
func (s *SDPFragment) Candidates() []string {
if s.media != nil {
return s.media.candidates
}
return nil
}
func (s *SDPFragment) ExtractICECredential() (string, string, error) {
pwds := []string{}
ufrags := []string{}
if s.ice != nil {
if s.ice.ufrag != "" {
ufrags = append(ufrags, s.ice.ufrag)
}
if s.ice.pwd != "" {
pwds = append(pwds, s.ice.pwd)
}
}
if s.media != nil {
if s.media.ice.ufrag != "" {
ufrags = append(ufrags, s.media.ice.ufrag)
}
if s.media.ice.pwd != "" {
pwds = append(pwds, s.media.ice.pwd)
}
}
if len(ufrags) == 0 {
return "", "", webrtc.ErrSessionDescriptionMissingIceUfrag
} else if len(pwds) == 0 {
return "", "", webrtc.ErrSessionDescriptionMissingIcePwd
}
for _, m := range ufrags {
if m != ufrags[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingIceUfrag
}
}
for _, m := range pwds {
if m != pwds[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingIcePwd
}
}
return ufrags[0], pwds[0], nil
}
func (s *SDPFragment) PatchICECredentialAndCandidatesIntoSDP(parsed *sdp.SessionDescription) error {
if s.ice != nil && (s.ice.lite != nil || s.ice.options != "") {
for _, a := range parsed.Attributes {
switch a.Key {
case "ice-lite":
if s.ice.lite == nil || !*s.ice.lite {
return errors.New("ice lite mismatch")
}
case "ice-options":
if s.ice.options != "" && s.ice.options != a.Value {
return errors.New("ice options mismatch")
}
}
}
}
foundMediaMid := false
if s.media != nil && s.media.mid != "" {
for _, md := range parsed.MediaDescriptions {
mid, found := md.Attribute(sdp.AttrKeyMID)
if found && mid == s.media.mid {
foundMediaMid = true
break
}
}
}
if !foundMediaMid {
return errors.New("could not find media mid")
}
if s.media != nil && s.media.ice != nil && (s.media.ice.lite != nil || s.media.ice.options != "") {
for _, md := range parsed.MediaDescriptions {
for _, a := range md.Attributes {
switch a.Key {
case "ice-lite":
if s.media.ice.lite == nil || !*s.media.ice.lite {
return errors.New("ice lite mismatch")
}
case "ice-options":
if s.media.ice.options != "" && s.media.ice.options != a.Value {
return errors.New("ice options mismatch")
}
}
}
}
}
if s.ice != nil && s.ice.ufrag != "" && s.ice.pwd != "" {
for idx, a := range parsed.Attributes {
switch a.Key {
case "ice-ufrag":
parsed.Attributes[idx] = sdp.Attribute{
Key: "ice-ufrag",
Value: s.ice.ufrag,
}
case "ice-pwd":
parsed.Attributes[idx] = sdp.Attribute{
Key: "ice-pwd",
Value: s.ice.pwd,
}
}
}
}
if s.media != nil {
for _, md := range parsed.MediaDescriptions {
for idx, a := range md.Attributes {
switch a.Key {
case "ice-ufrag":
if s.media.ice.ufrag != "" {
md.Attributes[idx] = sdp.Attribute{
Key: "ice-ufrag",
Value: s.media.ice.ufrag,
}
}
case "ice-pwd":
if s.media.ice.pwd != "" {
md.Attributes[idx] = sdp.Attribute{
Key: "ice-pwd",
Value: s.media.ice.pwd,
}
}
}
}
for idx, a := range md.Attributes {
if a.IsICECandidate() || a.Key == sdp.AttrKeyEndOfCandidates {
md.Attributes = append(md.Attributes[:idx], md.Attributes[idx+1:]...)
}
}
for _, ic := range s.media.candidates {
md.Attributes = append(
md.Attributes,
sdp.Attribute{
Key: sdp.AttrKeyCandidate,
Value: ic,
},
)
}
if s.media.endOfCandidates != nil && *s.media.endOfCandidates {
md.Attributes = append(
md.Attributes,
sdp.Attribute{Key: sdp.AttrKeyEndOfCandidates},
)
}
}
}
return nil
}
func ExtractSDPFragment(parsed *sdp.SessionDescription) (*SDPFragment, error) {
bundleMid, found := GetBundleMid(parsed)
if !found {
return nil, errors.New("could not get bundle mid")
}
s := &SDPFragment{}
if group, found := parsed.Attribute(sdp.AttrKeyGroup); found {
s.group = group
}
s.ice = &sdpFragmentICE{}
if err := s.ice.Unmarshal(parsed.Attributes); err != nil {
return nil, err
}
foundBundleMedia := false
for _, md := range parsed.MediaDescriptions {
mid, found := md.Attribute(sdp.AttrKeyMID)
if !found || mid != bundleMid {
continue
}
foundBundleMedia = true
s.media = &sdpFragmentMedia{}
if err := s.media.Unmarshal(md); err != nil {
return nil, err
}
break
}
if !foundBundleMedia {
return nil, fmt.Errorf("could not find bundle media: %s", bundleMid)
}
return s, nil
}