mirror of
https://github.com/pion/webrtc.git
synced 2025-11-01 19:22:49 +08:00
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:
@@ -178,6 +178,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu
|
||||
* [Vitaliy F](https://github.com/funvit)
|
||||
* [Ivan Egorov](https://github.com/vany-egorov)
|
||||
* [Nick Mykins](https://github.com/nmyk)
|
||||
* [Jason Brady](https://github.com/jbrady42)
|
||||
|
||||
### License
|
||||
MIT License - see [LICENSE](LICENSE) for full text
|
||||
|
||||
@@ -1787,7 +1787,7 @@ func (pc *PeerConnection) generateUnmatchedSDP(useIdentity bool) (*sdp.SessionDe
|
||||
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
|
||||
@@ -1889,7 +1889,12 @@ func (pc *PeerConnection) generateMatchedSDP(useIdentity bool, includeUnmatched
|
||||
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()) {
|
||||
|
||||
104
sdp.go
104
sdp.go
@@ -20,6 +20,16 @@ type trackDetails struct {
|
||||
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.
|
||||
func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) 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 {
|
||||
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 {
|
||||
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 {
|
||||
@@ -237,6 +244,13 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints
|
||||
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 {
|
||||
if mt.Sender() != nil && mt.Sender().track != nil {
|
||||
track := mt.Sender().track
|
||||
@@ -267,7 +281,7 @@ type mediaSection struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
mediaDtlsFingerprints := []DTLSFingerprint{}
|
||||
|
||||
@@ -293,7 +307,7 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL
|
||||
if m.data {
|
||||
addDataMediaSection(d, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState)
|
||||
} 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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -314,6 +328,14 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL
|
||||
// RFC 5245 S15.3
|
||||
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
|
||||
}
|
||||
|
||||
@@ -446,3 +468,73 @@ func haveApplicationMediaSection(desc *sdp.SessionDescription) bool {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
115
sdp_test.go
115
sdp_test.go
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -340,7 +341,7 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
|
||||
s, err = populateSDP(s, false,
|
||||
dtlsFingerprints,
|
||||
SDPMediaDescriptionFingerprints,
|
||||
false, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew)
|
||||
false, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
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-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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/sdp/v2"
|
||||
"github.com/pion/transport/vnet"
|
||||
)
|
||||
|
||||
@@ -49,6 +50,7 @@ type SettingEngine struct {
|
||||
SRTCP *uint
|
||||
}
|
||||
sdpMediaLevelFingerprints bool
|
||||
sdpExtensions map[SDPSectionType][]sdp.ExtMap
|
||||
answeringDTLSRole DTLSRole
|
||||
disableCertificateFingerprintVerification bool
|
||||
disableSRTPReplayProtection bool
|
||||
@@ -245,3 +247,65 @@ func (e *SettingEngine) DisableSRTCPReplayProtection(isDisabled bool) {
|
||||
func (e *SettingEngine) SetSDPMediaLevelFingerprints(sdpMediaLevelFingerprints bool) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user