Repository: pion/webrtc (go)

Issue:
PayloadType and Extension Header ID needs to be set at the Transceiver Level
Currently the `MediaEngine` stores these values at the PeerConnection level. We need to store them at the Transceiver level.


One use case where this is important is the fan-out use case. Without this change the SFU would have to choose either Header ID 1 or 2. You would then have to modify the headers as they flow through. This same restriction applies to PayloadType.
```
FireFox (Header ID 1) ->             ------> Track 1 (Header ID 1)
                           SFU  
Chrome (Header ID 2) ->            -------> Track 2 (Header ID 1)
```


By allowing values per media section we remove the need for users to modify/parse inside the SFU. So you could do the following.
```
FireFox (Header ID 1) ->              ------> Track 1 (Header ID 1)
                            SFU  
Chrome (Header ID 2) ->             -------> Track 2 (Header ID 2)
```

Pull request:
Allow extensions to be configured with direction
RegisterHeaderExtension now allows users to enable headers depending on
the type of transceiver that was created.

Also expose GetParameters on RTPSender and RTPReceiver

Co-authored-by: OrlandoCo <luisorlando.co@gmail.com>

Resolves #1554


Patch:
diff --git a/errors.go b/errors.go
index 8fb5b9a1..917192d3 100644
--- a/errors.go
+++ b/errors.go
@@ -141,6 +141,9 @@ var (
 	// ErrNoPayloaderForCodec indicates that the requested codec does not have a payloader
 	ErrNoPayloaderForCodec = errors.New("the requested codec does not have a payloader")
 
+	// ErrRegisterHeaderExtensionInvalidDirection indicates that a extension was registered with a direction besides `sendonly` or `recvonly`
+	ErrRegisterHeaderExtensionInvalidDirection = errors.New("a header extension must be registered as 'recvonly', 'sendonly' or both")
+
 	errDetachNotEnabled                 = errors.New("enable detaching by calling webrtc.DetachDataChannels()")
 	errDetachBeforeOpened               = errors.New("datachannel not opened yet, try calling Detach from OnOpen")
 	errDtlsTransportNotStarted          = errors.New("the DTLS transport has not started yet")
diff --git a/mediaengine.go b/mediaengine.go
index 5dac7cb5..12d740e5 100644
--- a/mediaengine.go
+++ b/mediaengine.go
@@ -26,6 +26,9 @@ const (
 type mediaEngineHeaderExtension struct {
 	uri              string
 	isAudio, isVideo bool
+
+	// If set only Transceivers of this direction are allowed
+	allowedDirections []RTPTransceiverDirection
 }
 
 // A MediaEngine defines the codecs supported by a PeerConnection, and the
@@ -205,11 +208,21 @@ func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType)
 
 // RegisterHeaderExtension adds a header extension to the MediaEngine
 // To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete
-func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType) error {
+func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType, allowedDirections ...RTPTransceiverDirection) error {
 	if m.negotiatedHeaderExtensions == nil {
 		m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
 	}
 
+	if len(allowedDirections) == 0 {
+		allowedDirections = []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly}
+	}
+
+	for _, direction := range allowedDirections {
+		if direction != RTPTransceiverDirectionRecvonly && direction != RTPTransceiverDirectionSendonly {
+			return ErrRegisterHeaderExtensionInvalidDirection
+		}
+	}
+
 	extensionIndex := -1
 	for i := range m.headerExtensions {
 		if extension.URI == m.headerExtensions[i].uri {
@@ -229,6 +242,7 @@ func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapabi
 	}
 
 	m.headerExtensions[extensionIndex].uri = extension.URI
+	m.headerExtensions[extensionIndex].allowedDirections = allowedDirections
 
 	return nil
 }
@@ -249,9 +263,9 @@ func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType)
 	}
 }
 
-// GetHeaderExtensionID returns the negotiated ID for a header extension.
+// getHeaderExtensionID returns the negotiated ID for a header extension.
 // If the Header Extension isn't enabled ok will be false
-func (m *MediaEngine) GetHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) {
+func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) {
 	if m.negotiatedHeaderExtensions == nil {
 		return 0, false, false
 	}
@@ -345,7 +359,7 @@ func (m *MediaEngine) updateHeaderExtension(id int, extension string, typ RTPCod
 
 	for _, localExtension := range m.headerExtensions {
 		if localExtension.uri == extension {
-			h := mediaEngineHeaderExtension{uri: extension}
+			h := mediaEngineHeaderExtension{uri: extension, allowedDirections: localExtension.allowedDirections}
 			if existingValue, ok := m.negotiatedHeaderExtensions[id]; ok {
 				h = existingValue
 			}
@@ -421,11 +435,21 @@ func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
 	return nil
 }
 
-func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType) RTPParameters {
+func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters {
 	headerExtensions := make([]RTPHeaderExtensionParameter, 0)
-	for id, e := range m.negotiatedHeaderExtensions {
-		if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
-			headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
+
+	if m.negotiatedVideo && typ == RTPCodecTypeVideo ||
+		m.negotiatedAudio && typ == RTPCodecTypeAudio {
+		for id, e := range m.negotiatedHeaderExtensions {
+			if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) {
+				headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
+			}
+		}
+	} else {
+		for id, e := range m.headerExtensions {
+			if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) {
+				headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id + 1, URI: e.uri})
+			}
 		}
 	}
 
@@ -454,17 +478,6 @@ func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RT
 	}, nil
 }
 
-func (m *MediaEngine) negotiatedHeaderExtensionsForType(typ RTPCodecType) map[int]mediaEngineHeaderExtension {
-	headerExtensions := map[int]mediaEngineHeaderExtension{}
-	for id, e := range m.negotiatedHeaderExtensions {
-		if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
-			headerExtensions[id] = e
-		}
-	}
-
-	return headerExtensions
-}
-
 func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
 	switch strings.ToLower(codec.MimeType) {
 	case mimeTypeH264:
diff --git a/peerconnection.go b/peerconnection.go
index 1171cef3..c966a0ab 100644
--- a/peerconnection.go
+++ b/peerconnection.go
@@ -1322,12 +1322,12 @@ func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc SSRC) e
 		return nil
 	}
 
-	midExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
+	midExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
 	if !audioSupported && !videoSupported {
 		return errPeerConnSimulcastMidRTPExtensionRequired
 	}
 
-	streamIDExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESRTPStreamIDURI})
+	streamIDExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESRTPStreamIDURI})
 	if !audioSupported && !videoSupported {
 		return errPeerConnSimulcastStreamIDRTPExtensionRequired
 	}
diff --git a/rtpcapabilities.go b/rtpcapabilities.go
index 6e018822..dc42230d 100644
--- a/rtpcapabilities.go
+++ b/rtpcapabilities.go
@@ -1,6 +1,8 @@
 package webrtc
 
 // RTPCapabilities represents the capabilities of a transceiver
+//
+// https://w3c.github.io/webrtc-pc/#rtcrtpcapabilities
 type RTPCapabilities struct {
 	Codecs           []RTPCodecCapability
 	HeaderExtensions []RTPHeaderExtensionCapability
diff --git a/rtpcodec.go b/rtpcodec.go
index 2d9b6d76..f1bb1c16 100644
--- a/rtpcodec.go
+++ b/rtpcodec.go
@@ -77,14 +77,6 @@ type RTPCodecParameters struct {
 	statsID string
 }
 
-// RTCRtpCapabilities is a list of supported codecs and header extensions
-//
-// https://w3c.github.io/webrtc-pc/#rtcrtpcapabilities
-type RTCRtpCapabilities struct {
-	HeaderExtensions []RTPHeaderExtensionCapability
-	Codecs           []RTPCodecCapability
-}
-
 // RTPParameters is a list of negotiated codecs and header extensions
 //
 // https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpparameters-members
diff --git a/rtpreceiver.go b/rtpreceiver.go
index b46657d9..4e2fa26f 100644
--- a/rtpreceiver.go
+++ b/rtpreceiver.go
@@ -63,6 +63,12 @@ func (r *RTPReceiver) Transport() *DTLSTransport {
 	return r.transport
 }
 
+// GetParameters describes the current configuration for the encoding and
+// transmission of media on the receiver's track.
+func (r *RTPReceiver) GetParameters() RTPParameters {
+	return r.api.mediaEngine.getRTPParametersByKind(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+}
+
 // Track returns the RtpTransceiver TrackRemote
 func (r *RTPReceiver) Track() *TrackRemote {
 	r.mu.RLock()
diff --git a/rtpsender.go b/rtpsender.go
index 61b3adc8..e60f680c 100644
--- a/rtpsender.go
+++ b/rtpsender.go
@@ -90,6 +90,12 @@ func (r *RTPSender) Transport() *DTLSTransport {
 	return r.transport
 }
 
+// GetParameters describes the current configuration for the encoding and
+// transmission of media on the sender's track.
+func (r *RTPSender) GetParameters() RTPParameters {
+	return r.api.mediaEngine.getRTPParametersByKind(r.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly})
+}
+
 // Track returns the RTCRtpTransceiver track, or nil
 func (r *RTPSender) Track() TrackLocal {
 	r.mu.RLock()
@@ -141,7 +147,7 @@ func (r *RTPSender) Send(parameters RTPSendParameters) error {
 
 	r.context = TrackLocalContext{
 		id:          r.id,
-		params:      r.api.mediaEngine.getRTPParametersByKind(r.track.Kind()),
+		params:      r.api.mediaEngine.getRTPParametersByKind(r.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}),
 		ssrc:        parameters.Encodings.SSRC,
 		writeStream: writeStream,
 	}
diff --git a/rtptransceiverdirection.go b/rtptransceiverdirection.go
index 1f3cb603..30730147 100644
--- a/rtptransceiverdirection.go
+++ b/rtptransceiverdirection.go
@@ -72,3 +72,14 @@ func (t RTPTransceiverDirection) Revers() RTPTransceiverDirection {
 		return t
 	}
 }
+
+func haveRTPTransceiverDirectionIntersection(haystack []RTPTransceiverDirection, needle []RTPTransceiverDirection) bool {
+	for _, n := range needle {
+		for _, h := range haystack {
+			if n == h {
+				return true
+			}
+		}
+	}
+	return false
+}
diff --git a/sdp.go b/sdp.go
index 61c19a93..5bdd6666 100644
--- a/sdp.go
+++ b/sdp.go
@@ -313,12 +313,21 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
 		return false, nil
 	}
 
-	for id, rtpExtension := range mediaEngine.negotiatedHeaderExtensionsForType(t.kind) {
-		extURL, err := url.Parse(rtpExtension.uri)
+	directions := []RTPTransceiverDirection{}
+	if t.Sender() != nil {
+		directions = append(directions, RTPTransceiverDirectionSendonly)
+	}
+	if t.Receiver() != nil {
+		directions = append(directions, RTPTransceiverDirectionRecvonly)
+	}
+
+	parameters := mediaEngine.getRTPParametersByKind(t.kind, directions)
+	for _, rtpExtension := range parameters.HeaderExtensions {
+		extURL, err := url.Parse(rtpExtension.URI)
 		if err != nil {
 			return false, err
 		}
-		media.WithExtMap(sdp.ExtMap{Value: id, URI: extURL})
+		media.WithExtMap(sdp.ExtMap{Value: rtpExtension.ID, URI: extURL})
 	}
 
 	if len(mediaSection.ridMap) > 0 {


Test patch:
diff --git a/mediaengine_test.go b/mediaengine_test.go
index b857b2e7..0e61df61 100644
--- a/mediaengine_test.go
+++ b/mediaengine_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/pion/sdp/v3"
+	"github.com/pion/transport/test"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -154,14 +155,66 @@ a=rtpmap:111 opus/48000/2
 		assert.False(t, m.negotiatedVideo)
 		assert.True(t, m.negotiatedAudio)
 
-		absID, absAudioEnabled, absVideoEnabled := m.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI})
+		absID, absAudioEnabled, absVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI})
 		assert.Equal(t, absID, 0)
 		assert.False(t, absAudioEnabled)
 		assert.False(t, absVideoEnabled)
 
-		midID, midAudioEnabled, midVideoEnabled := m.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
+		midID, midAudioEnabled, midVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
 		assert.Equal(t, midID, 7)
 		assert.True(t, midAudioEnabled)
 		assert.False(t, midVideoEnabled)
 	})
 }
+
+func TestMediaEngineHeaderExtensionDirection(t *testing.T) {
+	report := test.CheckRoutines(t)
+	defer report()
+
+	registerCodec := func(m *MediaEngine) {
+		assert.NoError(t, m.RegisterCodec(
+			RTPCodecParameters{
+				RTPCodecCapability: RTPCodecCapability{mimeTypeOpus, 48000, 0, "", nil},
+				PayloadType:        111,
+			}, RTPCodecTypeAudio))
+	}
+
+	t.Run("No Direction", func(t *testing.T) {
+		m := &MediaEngine{}
+		registerCodec(m)
+		assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio))
+
+		params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+
+		assert.Equal(t, 1, len(params.HeaderExtensions))
+	})
+
+	t.Run("Same Direction", func(t *testing.T) {
+		m := &MediaEngine{}
+		registerCodec(m)
+		assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionRecvonly))
+
+		params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+
+		assert.Equal(t, 1, len(params.HeaderExtensions))
+	})
+
+	t.Run("Different Direction", func(t *testing.T) {
+		m := &MediaEngine{}
+		registerCodec(m)
+		assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendonly))
+
+		params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+
+		assert.Equal(t, 0, len(params.HeaderExtensions))
+	})
+
+	t.Run("Invalid Direction", func(t *testing.T) {
+		m := &MediaEngine{}
+		registerCodec(m)
+
+		assert.Error(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv), ErrRegisterHeaderExtensionInvalidDirection)
+		assert.Error(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionInactive), ErrRegisterHeaderExtensionInvalidDirection)
+		assert.Error(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirection(0)), ErrRegisterHeaderExtensionInvalidDirection)
+	})
+}