Repository: pion/webrtc (go)

Issue:
Enable user to register custom codec implementation
<!--
  Come join our community on Slack https://pion.ly/slack for quicker support and more resources

  FAQ of common issues and questions https://github.com/pion/webrtc/wiki/FAQ

  Free book that covers general WebRTC https://webrtcforthecurious.com/

  Please answer these questions before submitting your issue. Thanks!
-->

## Summary

Allow user to register custom codec implementation other than provided one.

## Motivation

Right now we have a way to register custom codec via MediaEngine.RegisterCodec, but it'll throw "the requested codec does not have a payloader" error when the codec implementation is not found. I would like to bind a custom codec to a particular mime type. The matching of codec is private and couldn't be modified by user

```
func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
	switch strings.ToLower(codec.MimeType) {
	case strings.ToLower(MimeTypeH264):
		return &codecs.H264Payloader{}, nil
	case strings.ToLower(MimeTypeOpus):
		return &codecs.OpusPayloader{}, nil
	case strings.ToLower(MimeTypeVP8):
		return &codecs.VP8Payloader{
			EnablePictureID: true,
		}, nil
	case strings.ToLower(MimeTypeVP9):
		return &codecs.VP9Payloader{}, nil
	case strings.ToLower(MimeTypeAV1):
		return &codecs.AV1Payloader{}, nil
	case strings.ToLower(MimeTypeG722):
		return &codecs.G722Payloader{}, nil
	case strings.ToLower(MimeTypePCMU), strings.ToLower(MimeTypePCMA):
		return &codecs.G711Payloader{}, nil
	default:
		return nil, ErrNoPayloaderForCodec
	}
}
```
In the above code, pion should try to match a custom user implemented codec before throwing an error

Pull request description:
feat: allow media engine to register custom payloader on mimetype
#### Description

Allow media engine to register custom payloader depending on the mime type

#### Reference issue
Fixes #2928


Patch:
diff --git a/peerconnection.go b/peerconnection.go
index 35823660..4b2a16e3 100644
--- a/peerconnection.go
+++ b/peerconnection.go
@@ -1245,6 +1245,10 @@ func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPRece
 			return
 		}
 
+		if pc.api.settingEngine.fireOnTrackBeforeFirstRTP {
+			pc.onTrack(t, receiver)
+			return
+		}
 		go func(track *TrackRemote) {
 			b := make([]byte, pc.api.settingEngine.getReceiveMTU())
 			n, _, err := track.peek(b)
diff --git a/settingengine.go b/settingengine.go
index f3c70707..2e37b4e7 100644
--- a/settingengine.go
+++ b/settingengine.go
@@ -101,6 +101,7 @@ type SettingEngine struct {
 	srtpProtectionProfiles                    []dtls.SRTPProtectionProfile
 	receiveMTU                                uint
 	iceMaxBindingRequests                     *uint16
+	fireOnTrackBeforeFirstRTP                 bool
 }
 
 // getReceiveMTU returns the configured MTU. If SettingEngine's MTU is configured to 0 it returns the default
@@ -491,3 +492,12 @@ func (e *SettingEngine) SetSCTPRTOMax(rtoMax time.Duration) {
 func (e *SettingEngine) SetICEBindingRequestHandler(bindingRequestHandler func(m *stun.Message, local, remote ice.Candidate, pair *ice.CandidatePair) bool) {
 	e.iceBindingRequestHandler = bindingRequestHandler
 }
+
+// SetFireOnTrackBeforeFirstRTP sets if firing the OnTrack event should happen
+// before any RTP packets are received. Setting this to true will
+// have the Track's Codec and PayloadTypes be initially set to their
+// zero values in the OnTrack handler.
+// Note: This does not yet affect simulcast tracks.
+func (e *SettingEngine) SetFireOnTrackBeforeFirstRTP(fireOnTrackBeforeFirstRTP bool) {
+	e.fireOnTrackBeforeFirstRTP = fireOnTrackBeforeFirstRTP
+}
diff --git a/track_local_static.go b/track_local_static.go
index d4ca1ed5..2dd665cd 100644
--- a/track_local_static.go
+++ b/track_local_static.go
@@ -31,6 +31,7 @@ type TrackLocalStaticRTP struct {
 	mu                sync.RWMutex
 	bindings          []trackBinding
 	codec             RTPCodecCapability
+	payloader         func(RTPCodecCapability) (rtp.Payloader, error)
 	id, rid, streamID string
 }
 
@@ -57,6 +58,13 @@ func WithRTPStreamID(rid string) func(*TrackLocalStaticRTP) {
 	}
 }
 
+// WithPayloader allows the user to override the Payloader
+func WithPayloader(h func(RTPCodecCapability) (rtp.Payloader, error)) func(*TrackLocalStaticRTP) {
+	return func(s *TrackLocalStaticRTP) {
+		s.payloader = h
+	}
+}
+
 // Bind is called by the PeerConnection after negotiation is complete
 // This asserts that the code requested is supported by the remote peer.
 // If so it sets up all the state (SSRC and PayloadType) to have a call
@@ -250,7 +258,12 @@ func (s *TrackLocalStaticSample) Bind(t TrackLocalContext) (RTPCodecParameters,
 		return codec, nil
 	}
 
-	payloader, err := payloaderForCodec(codec.RTPCodecCapability)
+	payloadHandler := s.rtpTrack.payloader
+	if payloadHandler == nil {
+		payloadHandler = payloaderForCodec
+	}
+
+	payloader, err := payloadHandler(codec.RTPCodecCapability)
 	if err != nil {
 		return codec, err
 	}


Test patch:
diff --git a/settingengine_test.go b/settingengine_test.go
index 4a5714e1..6912694f 100644
--- a/settingengine_test.go
+++ b/settingengine_test.go
@@ -340,3 +340,57 @@ func TestSetHooks(t *testing.T) {
 		t.Errorf("Failed to set DTLS Certificate Request Hook")
 	}
 }
+
+func TestSetFireOnTrackBeforeFirstRTP(t *testing.T) {
+	lim := test.TimeOut(time.Second * 30)
+	defer lim.Stop()
+
+	report := test.CheckRoutines(t)
+	defer report()
+
+	s := SettingEngine{}
+	s.SetFireOnTrackBeforeFirstRTP(true)
+
+	mediaEngineOne := &MediaEngine{}
+	assert.NoError(t, mediaEngineOne.RegisterCodec(RTPCodecParameters{
+		RTPCodecCapability: RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
+		PayloadType:        100,
+	}, RTPCodecTypeVideo))
+
+	mediaEngineTwo := &MediaEngine{}
+	assert.NoError(t, mediaEngineTwo.RegisterCodec(RTPCodecParameters{
+		RTPCodecCapability: RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
+		PayloadType:        200,
+	}, RTPCodecTypeVideo))
+
+	offerer, err := NewAPI(WithMediaEngine(mediaEngineOne), WithSettingEngine(s)).NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+
+	answerer, err := NewAPI(WithMediaEngine(mediaEngineTwo)).NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+
+	track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+	assert.NoError(t, err)
+
+	_, err = offerer.AddTransceiverFromKind(RTPCodecTypeVideo)
+	assert.NoError(t, err)
+
+	_, err = answerer.AddTrack(track)
+	assert.NoError(t, err)
+
+	onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
+	offerer.OnTrack(func(track *TrackRemote, _ *RTPReceiver) {
+		_, _, err = track.Read(make([]byte, 1500))
+		assert.NoError(t, err)
+		assert.Equal(t, track.PayloadType(), PayloadType(100))
+		assert.Equal(t, track.Codec().RTPCodecCapability.MimeType, "video/VP8")
+
+		onTrackFiredFunc()
+	})
+
+	assert.NoError(t, signalPair(offerer, answerer))
+
+	sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{track})
+
+	closePairNow(t, offerer, answerer)
+}
diff --git a/track_local_static_test.go b/track_local_static_test.go
index b3c3c076..db4e1855 100644
--- a/track_local_static_test.go
+++ b/track_local_static_test.go
@@ -9,12 +9,14 @@ package webrtc
 import (
 	"context"
 	"errors"
+	"sync/atomic"
 	"testing"
 	"time"
 
 	"github.com/pion/rtp"
 	"github.com/pion/transport/v3/test"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 // If a remote doesn't support a Codec used by a `TrackLocalStatic`
@@ -336,3 +338,49 @@ func Test_TrackLocalStatic_RTX(t *testing.T) {
 
 	closePairNow(t, offerer, answerer)
 }
+
+type customCodecPayloader struct {
+	invokeCount atomic.Int32
+}
+
+func (c *customCodecPayloader) Payload(_ uint16, payload []byte) [][]byte {
+	c.invokeCount.Add(1)
+	return [][]byte{payload}
+}
+
+func Test_TrackLocalStatic_Payloader(t *testing.T) {
+	const mimeTypeCustomCodec = "video/custom-codec"
+
+	mediaEngine := &MediaEngine{}
+	assert.NoError(t, mediaEngine.RegisterCodec(RTPCodecParameters{
+		RTPCodecCapability: RTPCodecCapability{MimeType: mimeTypeCustomCodec, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
+		PayloadType:        96,
+	}, RTPCodecTypeVideo))
+
+	offerer, err := NewAPI(WithMediaEngine(mediaEngine)).NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+
+	answerer, err := NewAPI(WithMediaEngine(mediaEngine)).NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+
+	customPayloader := &customCodecPayloader{}
+	track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: mimeTypeCustomCodec}, "video", "pion", WithPayloader(func(c RTPCodecCapability) (rtp.Payloader, error) {
+		require.Equal(t, c.MimeType, mimeTypeCustomCodec)
+		return customPayloader, nil
+	}))
+	assert.NoError(t, err)
+
+	_, err = offerer.AddTrack(track)
+	assert.NoError(t, err)
+
+	assert.NoError(t, signalPair(offerer, answerer))
+
+	onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
+	answerer.OnTrack(func(*TrackRemote, *RTPReceiver) {
+		onTrackFiredFunc()
+	})
+
+	sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{track})
+
+	closePairNow(t, offerer, answerer)
+}