Add extmap support

Extmaps are configured via the SettingEngine. This allows a user
to set arbitrary values, and when answering ids and entries are
properly excluded.

Co-authored-by: Gabor Pongracz <gabor.pongracz@proemergotech.com>
This commit is contained in:
Sean DuBois
2020-07-03 00:40:10 -07:00
committed by Sean DuBois
parent a08d7a869c
commit ef1d5a4a8b
5 changed files with 284 additions and 9 deletions

View File

@@ -178,6 +178,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu
* [Vitaliy F](https://github.com/funvit) * [Vitaliy F](https://github.com/funvit)
* [Ivan Egorov](https://github.com/vany-egorov) * [Ivan Egorov](https://github.com/vany-egorov)
* [Nick Mykins](https://github.com/nmyk) * [Nick Mykins](https://github.com/nmyk)
* [Jason Brady](https://github.com/jbrady42)
### License ### License
MIT License - see [LICENSE](LICENSE) for full text MIT License - see [LICENSE](LICENSE) for full text

View File

@@ -1787,7 +1787,7 @@ func (pc *PeerConnection) generateUnmatchedSDP(useIdentity bool) (*sdp.SessionDe
return nil, err return nil, err
} }
return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState()) return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState(), pc.api.settingEngine.getSDPExtensions())
} }
// generateMatchedSDP generates a SDP and takes the remote state into account // generateMatchedSDP generates a SDP and takes the remote state into account
@@ -1889,7 +1889,12 @@ func (pc *PeerConnection) generateMatchedSDP(useIdentity bool, includeUnmatched
return nil, err return nil, err
} }
return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState()) matchedSDPMap, err := matchedAnswerExt(pc.RemoteDescription().parsed, pc.api.settingEngine.getSDPExtensions())
if err != nil {
return nil, err
}
return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState(), matchedSDPMap)
} }
func (pc *PeerConnection) setGatherCompleteHdlr(hdlr func()) { func (pc *PeerConnection) setGatherCompleteHdlr(hdlr func()) {

104
sdp.go
View File

@@ -20,6 +20,16 @@ type trackDetails struct {
ssrc uint32 ssrc uint32
} }
// SDPSectionType specifies media type sections
type SDPSectionType string
// Common SDP sections
const (
SDPSectionGlobal = SDPSectionType("global")
SDPSectionVideo = SDPSectionType("video")
SDPSectionAudio = SDPSectionType("audio")
)
// extract all trackDetails from an SDP. // extract all trackDetails from an SDP.
func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) map[uint32]trackDetails { func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) map[uint32]trackDetails {
incomingTracks := map[uint32]trackDetails{} incomingTracks := map[uint32]trackDetails{}
@@ -200,7 +210,7 @@ func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGathe
} }
} }
func addTransceiverSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, transceivers ...*RTPTransceiver) (bool, error) { func addTransceiverSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, extMaps map[SDPSectionType][]sdp.ExtMap, transceivers ...*RTPTransceiver) (bool, error) {
if len(transceivers) < 1 { if len(transceivers) < 1 {
return false, fmt.Errorf("addTransceiverSDP() called with 0 transceivers") return false, fmt.Errorf("addTransceiverSDP() called with 0 transceivers")
} }
@@ -219,9 +229,6 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints
for _, feedback := range codec.RTPCodecCapability.RTCPFeedback { for _, feedback := range codec.RTPCodecCapability.RTCPFeedback {
media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter)) media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter))
if feedback.Type == TypeRTCPFBTransportCC {
media.WithTransportCCExtMap()
}
} }
} }
if len(codecs) == 0 { if len(codecs) == 0 {
@@ -237,6 +244,13 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints
return false, nil return false, nil
} }
// Add extmaps
if maps, ok := extMaps[SDPSectionType(t.kind.String())]; ok {
for _, m := range maps {
media.WithExtMap(m)
}
}
for _, mt := range transceivers { for _, mt := range transceivers {
if mt.Sender() != nil && mt.Sender().track != nil { if mt.Sender() != nil && mt.Sender().track != nil {
track := mt.Sender().track track := mt.Sender().track
@@ -267,7 +281,7 @@ type mediaSection struct {
} }
// populateSDP serializes a PeerConnections state into an SDP // populateSDP serializes a PeerConnections state into an SDP
func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState) (*sdp.SessionDescription, error) { func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState, extMaps map[SDPSectionType][]sdp.ExtMap) (*sdp.SessionDescription, error) {
var err error var err error
mediaDtlsFingerprints := []DTLSFingerprint{} mediaDtlsFingerprints := []DTLSFingerprint{}
@@ -293,7 +307,7 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL
if m.data { if m.data {
addDataMediaSection(d, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState) addDataMediaSection(d, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState)
} else { } else {
shouldAddID, err = addTransceiverSDP(d, isPlanB, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, m.transceivers...) shouldAddID, err = addTransceiverSDP(d, isPlanB, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, extMaps, m.transceivers...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -314,6 +328,14 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL
// RFC 5245 S15.3 // RFC 5245 S15.3
d = d.WithValueAttribute(sdp.AttrKeyICELite, sdp.AttrKeyICELite) d = d.WithValueAttribute(sdp.AttrKeyICELite, sdp.AttrKeyICELite)
} }
// Add global exts
if maps, ok := extMaps[SDPSectionGlobal]; ok {
for _, m := range maps {
d.WithPropertyAttribute(m.Marshal())
}
}
return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil
} }
@@ -446,3 +468,73 @@ func haveApplicationMediaSection(desc *sdp.SessionDescription) bool {
return false return false
} }
func matchedAnswerExt(descriptions *sdp.SessionDescription, localMaps map[SDPSectionType][]sdp.ExtMap) (map[SDPSectionType][]sdp.ExtMap, error) {
remoteExtMaps, err := remoteExts(descriptions)
if err != nil {
return nil, err
}
return answerExtMaps(remoteExtMaps, localMaps), nil
}
func answerExtMaps(remoteExtMaps map[SDPSectionType]map[int]sdp.ExtMap, localMaps map[SDPSectionType][]sdp.ExtMap) map[SDPSectionType][]sdp.ExtMap {
ret := map[SDPSectionType][]sdp.ExtMap{}
for mediaType, remoteExtMap := range remoteExtMaps {
if _, ok := ret[mediaType]; !ok {
ret[mediaType] = []sdp.ExtMap{}
}
for _, extItem := range remoteExtMap {
// add remote ext that match locally available ones
for _, extMap := range localMaps[mediaType] {
if extMap.URI.String() == extItem.URI.String() {
ret[mediaType] = append(ret[mediaType], extItem)
}
}
}
}
return ret
}
func remoteExts(session *sdp.SessionDescription) (map[SDPSectionType]map[int]sdp.ExtMap, error) {
remoteExtMaps := map[SDPSectionType]map[int]sdp.ExtMap{}
maybeAddExt := func(attr sdp.Attribute, mediaType SDPSectionType) error {
if attr.Key != "extmap" {
return nil
}
em := &sdp.ExtMap{}
if err := em.Unmarshal("extmap:" + attr.Value); err != nil {
return fmt.Errorf("failed to parse ExtMap: %v", err)
}
if remoteExtMap, ok := remoteExtMaps[mediaType][em.Value]; ok {
if remoteExtMap.Value != em.Value {
return fmt.Errorf("RemoteDescription changed some extmaps values")
}
} else {
remoteExtMaps[mediaType][em.Value] = *em
}
return nil
}
// populate the extmaps from the current remote description
for _, media := range session.MediaDescriptions {
mediaType := SDPSectionType(media.MediaName.Media)
// populate known remote extmap and handle conflicts.
if _, ok := remoteExtMaps[mediaType]; !ok {
remoteExtMaps[mediaType] = map[int]sdp.ExtMap{}
}
for _, attr := range media.Attributes {
if err := maybeAddExt(attr, mediaType); err != nil {
return nil, err
}
}
}
// Add global exts
for _, attr := range session.Attributes {
if err := maybeAddExt(attr, SDPSectionGlobal); err != nil {
return nil, err
}
}
return remoteExtMaps, nil
}

View File

@@ -6,6 +6,7 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"net/url"
"strings" "strings"
"testing" "testing"
@@ -340,7 +341,7 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
s, err = populateSDP(s, false, s, err = populateSDP(s, false,
dtlsFingerprints, dtlsFingerprints,
SDPMediaDescriptionFingerprints, SDPMediaDescriptionFingerprints,
false, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew) false, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew, nil)
assert.NoError(t, err) assert.NoError(t, err)
sdparray, err := s.Marshal() sdparray, err := s.Marshal()
@@ -353,3 +354,115 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
t.Run("Per-Media Description Fingerprints", fingerprintTest(true, 3)) t.Run("Per-Media Description Fingerprints", fingerprintTest(true, 3))
t.Run("Per-Session Description Fingerprints", fingerprintTest(false, 1)) t.Run("Per-Session Description Fingerprints", fingerprintTest(false, 1))
} }
func TestPopulateSDP(t *testing.T) {
t.Run("Offer", func(t *testing.T) {
transportCCURL, _ := url.Parse(sdp.TransportCCURI)
absSendURL, _ := url.Parse(sdp.ABSSendTimeURI)
globalExts := []sdp.ExtMap{
{
URI: transportCCURL,
},
}
videoExts := []sdp.ExtMap{
{
URI: absSendURL,
},
}
tr := &RTPTransceiver{kind: RTPCodecTypeVideo}
tr.setDirection(RTPTransceiverDirectionSendrecv)
mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
se := SettingEngine{}
se.AddSDPExtensions(SDPSectionGlobal, globalExts)
se.AddSDPExtensions(SDPSectionVideo, videoExts)
m := MediaEngine{}
m.RegisterDefaultCodecs()
d := &sdp.SessionDescription{}
offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, &m, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, se.getSDPExtensions())
assert.Nil(t, err)
// Check global extensions
var found bool
for _, a := range offerSdp.Attributes {
if strings.Contains(a.Key, transportCCURL.String()) {
found = true
break
}
}
assert.Equal(t, true, found, "Global extension should be present")
// Check video extension
found = false
var foundGlobal bool
for _, desc := range offerSdp.MediaDescriptions {
if desc.MediaName.Media != mediaNameVideo {
continue
}
for _, a := range desc.Attributes {
if strings.Contains(a.Key, absSendURL.String()) {
found = true
}
if strings.Contains(a.Key, transportCCURL.String()) {
foundGlobal = true
}
}
}
assert.Equal(t, true, found, "Video extension should be present")
// Test video does not contain global
assert.Equal(t, false, foundGlobal, "Global extension should not be present in video section")
})
}
func TestMatchedAnswerExt(t *testing.T) {
s := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{
{
MediaName: sdp.MediaName{
Media: "video",
},
Attributes: []sdp.Attribute{
{Key: "sendrecv"},
{Key: "ssrc", Value: "2000"},
{Key: "extmap", Value: "5 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"},
},
},
},
}
transportCCURL, _ := url.Parse(sdp.TransportCCURI)
absSendURL, _ := url.Parse(sdp.ABSSendTimeURI)
exts := []sdp.ExtMap{
{
URI: transportCCURL,
},
{
URI: absSendURL,
},
}
se := SettingEngine{}
se.AddSDPExtensions(SDPSectionVideo, exts)
ansMaps, err := matchedAnswerExt(s, se.getSDPExtensions())
if err != nil {
t.Fatalf("Ext parse error %v", err)
}
if maps := ansMaps[SDPSectionVideo]; maps != nil {
// Check answer contains intersect of remote and local
// Abs send time should be only extenstion
assert.Equal(t, 1, len(maps), "Only one extension should be active")
assert.Equal(t, absSendURL, maps[0].URI, "Only abs-send-time should be active")
// Check answer uses remote IDs
assert.Equal(t, 5, maps[0].Value, "Should use remote ext ID")
} else {
t.Fatal("No video ext maps found")
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/logging" "github.com/pion/logging"
"github.com/pion/sdp/v2"
"github.com/pion/transport/vnet" "github.com/pion/transport/vnet"
) )
@@ -49,6 +50,7 @@ type SettingEngine struct {
SRTCP *uint SRTCP *uint
} }
sdpMediaLevelFingerprints bool sdpMediaLevelFingerprints bool
sdpExtensions map[SDPSectionType][]sdp.ExtMap
answeringDTLSRole DTLSRole answeringDTLSRole DTLSRole
disableCertificateFingerprintVerification bool disableCertificateFingerprintVerification bool
disableSRTPReplayProtection bool disableSRTPReplayProtection bool
@@ -245,3 +247,65 @@ func (e *SettingEngine) DisableSRTCPReplayProtection(isDisabled bool) {
func (e *SettingEngine) SetSDPMediaLevelFingerprints(sdpMediaLevelFingerprints bool) { func (e *SettingEngine) SetSDPMediaLevelFingerprints(sdpMediaLevelFingerprints bool) {
e.sdpMediaLevelFingerprints = sdpMediaLevelFingerprints e.sdpMediaLevelFingerprints = sdpMediaLevelFingerprints
} }
// AddSDPExtensions adds available and offered extensions for media type.
//
// Ext IDs are optional and generated if you do not provide them
// SDP answers will only include extensions supported by both sides
func (e *SettingEngine) AddSDPExtensions(mediaType SDPSectionType, exts []sdp.ExtMap) {
if e.sdpExtensions == nil {
e.sdpExtensions = make(map[SDPSectionType][]sdp.ExtMap)
}
if _, ok := e.sdpExtensions[mediaType]; !ok {
e.sdpExtensions[mediaType] = []sdp.ExtMap{}
}
e.sdpExtensions[mediaType] = append(e.sdpExtensions[mediaType], exts...)
}
func (e *SettingEngine) getSDPExtensions() map[SDPSectionType][]sdp.ExtMap {
var lastID int
idMap := map[string]int{}
// Build provided ext id map
for _, extList := range e.sdpExtensions {
for _, ext := range extList {
if ext.Value != 0 {
idMap[ext.URI.String()] = ext.Value
}
}
}
// Find next available ID
nextID := func() {
var done bool
for !done {
lastID++
var found bool
for _, v := range idMap {
if lastID == v {
found = true
break
}
}
if !found {
done = true
}
}
}
// Assign missing IDs across all media types based on URI
for mType, extList := range e.sdpExtensions {
for i, ext := range extList {
if ext.Value == 0 {
if id, ok := idMap[ext.URI.String()]; ok {
e.sdpExtensions[mType][i].Value = id
} else {
nextID()
e.sdpExtensions[mType][i].Value = lastID
idMap[ext.URI.String()] = lastID
}
}
}
}
return e.sdpExtensions
}