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:
566
sdp/sdp.go
566
sdp/sdp.go
@@ -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
180
sdp/sdp_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user