Helpers to deal with SDP fragments. (#1039)

* Helpers to deal with SDP fragments.

WHIP uses SDP fragments which fail parsing using full-fledged SDP
libraries like pion/sdp, go-sdp.

Add utlities to deal with SDP fragments.

* generated protobuf

* SDP tests, mostly happy path, will add more later

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Raja Subramanian
2025-04-09 00:07:14 +05:30
committed by GitHub
parent c193b8d080
commit 0975d34864
2 changed files with 722 additions and 24 deletions

View File

@@ -15,6 +15,8 @@
package sdp
import (
"errors"
"fmt"
"strconv"
"strings"
@@ -96,44 +98,44 @@ func ExtractDTLSRole(desc *sdp.SessionDescription) webrtc.DTLSRole {
}
func ExtractICECredential(desc *sdp.SessionDescription) (string, string, error) {
remotePwds := []string{}
remoteUfrags := []string{}
pwds := []string{}
ufrags := []string{}
if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag {
remoteUfrags = append(remoteUfrags, ufrag)
ufrags = append(ufrags, ufrag)
}
if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd {
remotePwds = append(remotePwds, pwd)
pwds = append(pwds, pwd)
}
for _, m := range desc.MediaDescriptions {
if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag {
remoteUfrags = append(remoteUfrags, ufrag)
ufrags = append(ufrags, ufrag)
}
if pwd, havePwd := m.Attribute("ice-pwd"); havePwd {
remotePwds = append(remotePwds, pwd)
pwds = append(pwds, pwd)
}
}
if len(remoteUfrags) == 0 {
if len(ufrags) == 0 {
return "", "", webrtc.ErrSessionDescriptionMissingIceUfrag
} else if len(remotePwds) == 0 {
} else if len(pwds) == 0 {
return "", "", webrtc.ErrSessionDescriptionMissingIcePwd
}
for _, m := range remoteUfrags {
if m != remoteUfrags[0] {
for _, m := range ufrags {
if m != ufrags[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingIceUfrag
}
}
for _, m := range remotePwds {
if m != remotePwds[0] {
for _, m := range pwds {
if m != pwds[0] {
return "", "", webrtc.ErrSessionDescriptionConflictingIcePwd
}
}
return remoteUfrags[0], remotePwds[0], nil
return ufrags[0], pwds[0], nil
}
func ExtractStreamID(media *sdp.MediaDescription) (string, bool) {
@@ -151,17 +153,6 @@ func ExtractStreamID(media *sdp.MediaDescription) (string, bool) {
return streamID, true
}
func GetTrackIDFromMediaDescription(m *sdp.MediaDescription) string {
trackId := ""
msid, ok := m.Attribute(sdp.AttrKeyMsid)
if ok {
if split := strings.Split(msid, " "); len(split) >= 2 {
trackId = split[1]
}
}
return trackId
}
func IsMediaDescriptionSimulcast(m *sdp.MediaDescription) bool {
_, ok := m.Attribute("simulcast")
return ok
@@ -191,3 +182,530 @@ func CodecsFromMediaDescription(m *sdp.MediaDescription) (out []sdp.Codec, err e
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 {
// MediaName conversion to string taken from github.com/pion/sdp
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
}
// primarily for use with WHIP Trickle ICE - https://www.rfc-editor.org/rfc/rfc9725.html#name-trickle-ice
func (s *SDPFragment) Unmarshal(frag string) error {
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' {
// not an attribute, skip
continue
}
if line[1] != '=' {
return errors.New("invalid attribute")
}
attrParts := strings.Split(line[2:], ":")
if len(attrParts) != 2 {
return errors.New("invalid attribute")
}
if s.ice == nil {
s.ice = &sdpFragmentICE{}
}
switch attrParts[0] {
case sdp.AttrKeyGroup:
s.group = attrParts[1]
case "ice-ufrag":
if s.media != nil {
s.media.ice.ufrag = attrParts[1]
} else {
s.ice.ufrag = attrParts[1]
}
case "ice-pwd":
if s.media != nil {
s.media.ice.pwd = attrParts[1]
} else {
s.ice.pwd = attrParts[1]
}
case sdp.AttrKeyICELite:
lite := true
if s.media != nil {
s.media.ice.lite = &lite
} else {
s.ice.lite = &lite
}
case "ice-options":
if s.media != nil {
s.media.ice.options = attrParts[1]
} else {
s.ice.options = attrParts[1]
}
case sdp.AttrKeyMID:
if s.media != nil {
s.media.mid = attrParts[1]
}
case sdp.AttrKeyCandidate:
if s.media != nil {
s.media.candidates = append(s.media.candidates, attrParts[1])
}
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
}
// primarily for use with WHIP ICE Restart - https://www.rfc-editor.org/rfc/rfc9725.html#name-ice-restarts
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
}
// primarily for use with WHIP ICE Restart - https://www.rfc-editor.org/rfc/rfc9725.html#name-ice-restarts
func (s *SDPFragment) PatchICECredentialIntoSDP(parsed *sdp.SessionDescription) error {
// ice-options and ice-lite should match
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")
}
}
}
}
if s.media != nil && s.media.mid != "" && s.media.ice != nil && (s.media.ice.lite != nil || s.media.ice.options != "") {
for _, md := range parsed.MediaDescriptions {
mid, found := md.Attribute(sdp.AttrKeyMID)
if !found || mid != s.media.mid {
continue
}
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 != "" {
fmt.Printf("patching ufrag/pwd\n") // REMOVE
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 && s.media.mid != "" {
for _, md := range parsed.MediaDescriptions {
mid, found := md.Attribute(sdp.AttrKeyMID)
if !found || mid != s.media.mid {
continue
}
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,
}
}
}
}
// clean out existing candidates and patch in new ones
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
}
// primarily for use with WHIP ICE Restart - https://www.rfc-editor.org/rfc/rfc9725.html#name-ice-restarts
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
}

180
sdp/sdp_test.go Normal file
View File

@@ -0,0 +1,180 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sdp
import (
"testing"
pionsdp "github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
)
func TestSDPUtilFunctions(t *testing.T) {
sdp := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:0\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:1\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n"
sd := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: sdp,
}
parsed, err := sd.Unmarshal()
require.NoError(t, err)
require.Equal(t, "0", GetMidValue(parsed.MediaDescriptions[0]))
require.Equal(t, "1", GetMidValue(parsed.MediaDescriptions[1]))
fp, alg, err := ExtractFingerprint(parsed)
require.NoError(t, err)
require.Equal(t, "40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67", fp)
require.Equal(t, "sha-256", alg)
dtlsRole := ExtractDTLSRole(parsed)
require.Equal(t, webrtc.DTLSRoleServer, dtlsRole)
ufrag, pwd, err := ExtractICECredential(parsed)
require.NoError(t, err)
require.Equal(t, "1hhfzwf0ijpzm", ufrag)
require.Equal(t, "jm5puo2ab1op3vs59ca53bdk7s", pwd)
streamID, found := ExtractStreamID(parsed.MediaDescriptions[0])
require.False(t, found)
require.Empty(t, streamID)
require.False(t, IsMediaDescriptionSimulcast(parsed.MediaDescriptions[1]))
codecs, err := CodecsFromMediaDescription(parsed.MediaDescriptions[0])
require.NoError(t, err)
expectedCodec := pionsdp.Codec{
PayloadType: 96,
Name: "H264",
ClockRate: 90000,
Fmtp: "packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: []string{"nack", "goog-remb"},
}
require.Equal(t, expectedCodec, codecs[0])
bundleMid, found := GetBundleMid(parsed)
require.True(t, found)
require.Equal(t, "0", bundleMid)
}
func TestSDPFragment(t *testing.T) {
fragment := "a=ice-options:trickle ice2\r\na=group:BUNDLE 0 1\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\na=mid:0\r\na=ice-ufrag:ysXw\r\na=ice-pwd:vw5LmwG4y/e6dPP/zAP9Gp5k\r\na=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1\r\na=candidate:3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2\r\na=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1\r\na=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2\r\n"
expectedSDPFragment := SDPFragment{
group: "BUNDLE 0 1",
ice: &sdpFragmentICE{
options: "trickle ice2",
},
media: &sdpFragmentMedia{
info: "audio 9 UDP/TLS/RTP/SAVPF 111",
mid: "0",
ice: &sdpFragmentICE{
ufrag: "ysXw",
pwd: "vw5LmwG4y/e6dPP/zAP9Gp5k",
},
candidates: []string{
"1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1",
"3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2",
"473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1",
"2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2",
},
},
}
sdpFragment := &SDPFragment{}
err := sdpFragment.Unmarshal(fragment)
require.NoError(t, err)
require.Equal(t, expectedSDPFragment, *sdpFragment)
expectedMarshalled := "a=group:BUNDLE 0 1\r\na=ice-options:trickle ice2\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\na=mid:0\r\na=ice-ufrag:ysXw\r\na=ice-pwd:vw5LmwG4y/e6dPP/zAP9Gp5k\r\na=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1\r\na=candidate:3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2\r\na=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1\r\na=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2\r\n"
marshalled, err := sdpFragment.Marshal()
require.NoError(t, err)
require.Equal(t, expectedMarshalled, marshalled)
expectedCandidates := []string{
"1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1",
"3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2",
"473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1",
"2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2",
}
sdpFragment1 := &SDPFragment{}
err = sdpFragment1.Unmarshal(marshalled)
require.NoError(t, err)
require.Equal(t, expectedSDPFragment, *sdpFragment1)
require.Equal(t, "0", sdpFragment1.Mid())
require.Equal(t, expectedCandidates, sdpFragment1.Candidates())
ufrag, pwd, err := sdpFragment1.ExtractICECredential()
require.NoError(t, err)
require.Equal(t, "ysXw", ufrag)
require.Equal(t, "vw5LmwG4y/e6dPP/zAP9Gp5k", pwd)
mismatchedMidFragment := "a=ice-options:trickle ice2\r\na=group:BUNDLE 0 1\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\na=mid:1\r\na=ice-ufrag:ysXw\r\na=ice-pwd:vw5LmwG4y/e6dPP/zAP9Gp5k\r\na=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1\r\na=candidate:3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2\r\na=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1\r\na=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2\r\n"
sdpFragment2 := &SDPFragment{}
err = sdpFragment2.Unmarshal(mismatchedMidFragment)
require.Error(t, err)
sdp := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=ice-options:trickle ice2\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:0\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:1\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n"
sd := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: sdp,
}
parsed, _ := sd.Unmarshal()
err = sdpFragment1.PatchICECredentialIntoSDP(parsed)
require.NoError(t, err)
ufrag, pwd, err = ExtractICECredential(parsed)
require.NoError(t, err)
require.Equal(t, "ysXw", ufrag)
require.Equal(t, "vw5LmwG4y/e6dPP/zAP9Gp5k", pwd)
candidates := []string{}
for _, a := range parsed.MediaDescriptions[0].Attributes {
if a.IsICECandidate() {
candidates = append(candidates, a.Value)
}
}
require.Equal(t, expectedCandidates, candidates)
sdpFragment3, err := ExtractSDPFragment(parsed)
require.NoError(t, err)
expectedSDPFragment3 := SDPFragment{
group: "BUNDLE 0 1",
ice: &sdpFragmentICE{
options: "trickle ice2",
},
media: &sdpFragmentMedia{
info: "video 9 UDP/TLS/RTP/SAVPF 96",
mid: "0",
ice: &sdpFragmentICE{
ufrag: "ysXw",
pwd: "vw5LmwG4y/e6dPP/zAP9Gp5k",
},
candidates: []string{
"1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1",
"3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2",
"473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1",
"2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2",
},
},
}
require.Equal(t, expectedSDPFragment3, *sdpFragment3)
marshalledSDPFragment3, err := sdpFragment3.Marshal()
require.NoError(t, err)
expectedMarshalledSDPFragment3 := "a=group:BUNDLE 0 1\r\na=ice-options:trickle ice2\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\na=mid:0\r\na=ice-ufrag:ysXw\r\na=ice-pwd:vw5LmwG4y/e6dPP/zAP9Gp5k\r\na=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1\r\na=candidate:3471623853 1 udp 2122194687 198.51.100.2 61765 typ host generation 0 ufrag EsAw network-id 2\r\na=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1\r\na=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2\r\n"
require.Equal(t, expectedMarshalledSDPFragment3, marshalledSDPFragment3)
}