Skip to content

Commit b31dde1

Browse files
committed
Match codecs with different rate or channels
Consider clock rate and channels when matching codecs. This allows to support codecs with the same MIME type but sample rate or channel count that might be different from the default ones, like PCMU, PCMA, LPCM and multiopus.
1 parent 47bde05 commit b31dde1

7 files changed

+207
-37
lines changed

internal/fmtp/fmtp.go

+83-20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,74 @@ func parseParameters(line string) map[string]string {
2424
return parameters
2525
}
2626

27+
// ClockRateEqual checks whether two clock rates are equal.
28+
func ClockRateEqual(mimeType string, valA, valB uint32) bool {
29+
// Clock rate and channel checks have been introduced quite recently.
30+
// Existing implementations often use VP8, H264 or Opus without setting clock rate or channels.
31+
// Keep compatibility with these situations.
32+
// It would be better to remove this exception in a future major release.
33+
switch {
34+
case strings.EqualFold(mimeType, "video/vp8"):
35+
if valA == 0 {
36+
valA = 90000
37+
}
38+
if valB == 0 {
39+
valB = 90000
40+
}
41+
42+
case strings.EqualFold(mimeType, "audio/opus"):
43+
if valA == 0 {
44+
valA = 48000
45+
}
46+
if valB == 0 {
47+
valB = 48000
48+
}
49+
}
50+
51+
return valA == valB
52+
}
53+
54+
// ChannelsEqual checks whether two channels are equal.
55+
func ChannelsEqual(mimeType string, valA, valB uint16) bool {
56+
// Clock rate and channel checks have been introduced quite recently.
57+
// Existing implementations often use VP8, H264 or Opus without setting clock rate or channels.
58+
// Keep compatibility with these situations.
59+
// It would be better to remove this exception in a future major release.
60+
if strings.EqualFold(mimeType, "audio/opus") {
61+
if valA == 0 {
62+
valA = 2
63+
}
64+
if valB == 0 {
65+
valB = 2
66+
}
67+
}
68+
69+
if valA == 0 {
70+
valA = 1
71+
}
72+
if valB == 0 {
73+
valB = 1
74+
}
75+
76+
return valA == valB
77+
}
78+
79+
func paramsEqual(valA, valB map[string]string) bool {
80+
for k, v := range valA {
81+
if vb, ok := valB[k]; ok && !strings.EqualFold(vb, v) {
82+
return false
83+
}
84+
}
85+
86+
for k, v := range valB {
87+
if va, ok := valA[k]; ok && !strings.EqualFold(va, v) {
88+
return false
89+
}
90+
}
91+
92+
return true
93+
}
94+
2795
// FMTP interface for implementing custom
2896
// FMTP parsers based on MimeType.
2997
type FMTP interface {
@@ -39,30 +107,36 @@ type FMTP interface {
39107
}
40108

41109
// Parse parses an fmtp string based on the MimeType.
42-
func Parse(mimeType, line string) FMTP {
110+
func Parse(mimeType string, clockRate uint32, channels uint16, line string) FMTP {
43111
var fmtp FMTP
44112

45113
parameters := parseParameters(line)
46114

47115
switch {
116+
// Clock rate and channel checks have been introduced quite recently.
117+
// Existing implementations often use VP8, H264 or Opus without setting clock rate or channels.
118+
// Keep compatibility with these situations.
119+
// It would be better to add a clock rate and channel check in a future major release.
48120
case strings.EqualFold(mimeType, "video/h264"):
49121
fmtp = &h264FMTP{
50122
parameters: parameters,
51123
}
52124

53-
case strings.EqualFold(mimeType, "video/vp9"):
125+
case strings.EqualFold(mimeType, "video/vp9") && clockRate == 90000 && channels == 0:
54126
fmtp = &vp9FMTP{
55127
parameters: parameters,
56128
}
57129

58-
case strings.EqualFold(mimeType, "video/av1"):
130+
case strings.EqualFold(mimeType, "video/av1") && clockRate == 90000 && channels == 0:
59131
fmtp = &av1FMTP{
60132
parameters: parameters,
61133
}
62134

63135
default:
64136
fmtp = &genericFMTP{
65137
mimeType: mimeType,
138+
clockRate: clockRate,
139+
channels: channels,
66140
parameters: parameters,
67141
}
68142
}
@@ -72,6 +146,8 @@ func Parse(mimeType, line string) FMTP {
72146

73147
type genericFMTP struct {
74148
mimeType string
149+
clockRate uint32
150+
channels uint16
75151
parameters map[string]string
76152
}
77153

@@ -87,23 +163,10 @@ func (g *genericFMTP) Match(b FMTP) bool {
87163
return false
88164
}
89165

90-
if !strings.EqualFold(g.mimeType, fmtp.MimeType()) {
91-
return false
92-
}
93-
94-
for k, v := range g.parameters {
95-
if vb, ok := fmtp.parameters[k]; ok && !strings.EqualFold(vb, v) {
96-
return false
97-
}
98-
}
99-
100-
for k, v := range fmtp.parameters {
101-
if va, ok := g.parameters[k]; ok && !strings.EqualFold(va, v) {
102-
return false
103-
}
104-
}
105-
106-
return true
166+
return strings.EqualFold(g.mimeType, fmtp.MimeType()) &&
167+
ClockRateEqual(g.mimeType, g.clockRate, fmtp.clockRate) &&
168+
ChannelsEqual(g.mimeType, g.channels, fmtp.channels) &&
169+
paramsEqual(g.parameters, fmtp.parameters)
107170
}
108171

109172
func (g *genericFMTP) Parameter(key string) (string, bool) {

internal/fmtp/fmtp_test.go

+90-7
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,23 @@ func TestParseParameters(t *testing.T) {
5656

5757
func TestParse(t *testing.T) {
5858
for _, ca := range []struct {
59-
name string
60-
mimeType string
61-
line string
62-
expected FMTP
59+
name string
60+
mimeType string
61+
clockRate uint32
62+
channels uint16
63+
line string
64+
expected FMTP
6365
}{
6466
{
6567
"generic",
6668
"generic",
69+
90000,
70+
2,
6771
"key-name=value",
6872
&genericFMTP{
69-
mimeType: "generic",
73+
mimeType: "generic",
74+
clockRate: 90000,
75+
channels: 2,
7076
parameters: map[string]string{
7177
"key-name": "value",
7278
},
@@ -75,9 +81,13 @@ func TestParse(t *testing.T) {
7581
{
7682
"generic case normalization",
7783
"generic",
84+
90000,
85+
2,
7886
"Key=value",
7987
&genericFMTP{
80-
mimeType: "generic",
88+
mimeType: "generic",
89+
clockRate: 90000,
90+
channels: 2,
8191
parameters: map[string]string{
8292
"key": "value",
8393
},
@@ -86,6 +96,8 @@ func TestParse(t *testing.T) {
8696
{
8797
"h264",
8898
"video/h264",
99+
90000,
100+
0,
89101
"key-name=value",
90102
&h264FMTP{
91103
parameters: map[string]string{
@@ -96,6 +108,8 @@ func TestParse(t *testing.T) {
96108
{
97109
"vp9",
98110
"video/vp9",
111+
90000,
112+
0,
99113
"key-name=value",
100114
&vp9FMTP{
101115
parameters: map[string]string{
@@ -106,6 +120,8 @@ func TestParse(t *testing.T) {
106120
{
107121
"av1",
108122
"video/av1",
123+
90000,
124+
0,
109125
"key-name=value",
110126
&av1FMTP{
111127
parameters: map[string]string{
@@ -115,7 +131,7 @@ func TestParse(t *testing.T) {
115131
},
116132
} {
117133
t.Run(ca.name, func(t *testing.T) {
118-
f := Parse(ca.mimeType, ca.line)
134+
f := Parse(ca.mimeType, ca.clockRate, ca.channels, ca.line)
119135
if !reflect.DeepEqual(ca.expected, f) {
120136
t.Errorf("expected '%v', got '%v'", ca.expected, f)
121137
}
@@ -177,6 +193,27 @@ func TestMatch(t *testing.T) { //nolint:maintidx
177193
},
178194
true,
179195
},
196+
{
197+
"generic inferred channels",
198+
&genericFMTP{
199+
mimeType: "generic",
200+
channels: 1,
201+
parameters: map[string]string{
202+
"key1": "value1",
203+
"key2": "value2",
204+
"key3": "value3",
205+
},
206+
},
207+
&genericFMTP{
208+
mimeType: "generic",
209+
parameters: map[string]string{
210+
"key1": "value1",
211+
"key2": "value2",
212+
"key3": "value3",
213+
},
214+
},
215+
true,
216+
},
180217
{
181218
"generic inconsistent different kind",
182219
&genericFMTP{
@@ -210,6 +247,52 @@ func TestMatch(t *testing.T) { //nolint:maintidx
210247
},
211248
false,
212249
},
250+
{
251+
"generic inconsistent different clock rate",
252+
&genericFMTP{
253+
mimeType: "generic",
254+
clockRate: 90000,
255+
parameters: map[string]string{
256+
"key1": "value1",
257+
"key2": "value2",
258+
"key3": "value3",
259+
},
260+
},
261+
&genericFMTP{
262+
mimeType: "generic",
263+
clockRate: 48000,
264+
parameters: map[string]string{
265+
"key1": "value1",
266+
"key2": "value2",
267+
"key3": "value3",
268+
},
269+
},
270+
false,
271+
},
272+
{
273+
"generic inconsistent different channels",
274+
&genericFMTP{
275+
mimeType: "generic",
276+
clockRate: 90000,
277+
channels: 2,
278+
parameters: map[string]string{
279+
"key1": "value1",
280+
"key2": "value2",
281+
"key3": "value3",
282+
},
283+
},
284+
&genericFMTP{
285+
mimeType: "generic",
286+
clockRate: 90000,
287+
channels: 1,
288+
parameters: map[string]string{
289+
"key1": "value1",
290+
"key2": "value2",
291+
"key3": "value3",
292+
},
293+
},
294+
false,
295+
},
213296
{
214297
"generic inconsistent different parameters",
215298
&genericFMTP{

mediaengine.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,10 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
246246
// addCodec will append codec if it not exists.
247247
func (m *MediaEngine) addCodec(codecs []RTPCodecParameters, codec RTPCodecParameters) []RTPCodecParameters {
248248
for _, c := range codecs {
249-
if c.MimeType == codec.MimeType && c.PayloadType == codec.PayloadType {
249+
if c.MimeType == codec.MimeType &&
250+
fmtp.ClockRateEqual(c.MimeType, c.ClockRate, codec.ClockRate) &&
251+
fmtp.ChannelsEqual(c.MimeType, c.Channels, codec.Channels) &&
252+
c.PayloadType == codec.PayloadType {
250253
return codecs
251254
}
252255
}
@@ -459,7 +462,12 @@ func (m *MediaEngine) matchRemoteCodec(
459462
codecs = m.audioCodecs
460463
}
461464

462-
remoteFmtp := fmtp.Parse(remoteCodec.RTPCodecCapability.MimeType, remoteCodec.RTPCodecCapability.SDPFmtpLine)
465+
remoteFmtp := fmtp.Parse(
466+
remoteCodec.RTPCodecCapability.MimeType,
467+
remoteCodec.RTPCodecCapability.ClockRate,
468+
remoteCodec.RTPCodecCapability.Channels,
469+
remoteCodec.RTPCodecCapability.SDPFmtpLine)
470+
463471
if apt, hasApt := remoteFmtp.Parameter("apt"); hasApt { //nolint:nestif
464472
payloadType, err := strconv.ParseUint(apt, 10, 8)
465473
if err != nil {

peerconnection_media_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1823,7 +1823,8 @@ func TestPeerConnection_Zero_PayloadType(t *testing.T) {
18231823
pcOffer, pcAnswer, err := newPair()
18241824
require.NoError(t, err)
18251825

1826-
audioTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypePCMU}, "audio", "audio")
1826+
audioTrack, err := NewTrackLocalStaticSample(
1827+
RTPCodecCapability{MimeType: MimeTypePCMU, ClockRate: 8000}, "audio", "audio")
18271828
require.NoError(t, err)
18281829

18291830
_, err = pcOffer.AddTrack(audioTrack)

rtpcodec.go

+20-5
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,34 @@ func codecParametersFuzzySearch(
108108
needle RTPCodecParameters,
109109
haystack []RTPCodecParameters,
110110
) (RTPCodecParameters, codecMatchType) {
111-
needleFmtp := fmtp.Parse(needle.RTPCodecCapability.MimeType, needle.RTPCodecCapability.SDPFmtpLine)
111+
needleFmtp := fmtp.Parse(
112+
needle.RTPCodecCapability.MimeType,
113+
needle.RTPCodecCapability.ClockRate,
114+
needle.RTPCodecCapability.Channels,
115+
needle.RTPCodecCapability.SDPFmtpLine)
112116

113-
// First attempt to match on MimeType + SDPFmtpLine
117+
// First attempt to match on MimeType + Channels + SDPFmtpLine
114118
for _, c := range haystack {
115-
cfmtp := fmtp.Parse(c.RTPCodecCapability.MimeType, c.RTPCodecCapability.SDPFmtpLine)
119+
cfmtp := fmtp.Parse(
120+
c.RTPCodecCapability.MimeType,
121+
c.RTPCodecCapability.ClockRate,
122+
c.RTPCodecCapability.Channels,
123+
c.RTPCodecCapability.SDPFmtpLine)
124+
116125
if needleFmtp.Match(cfmtp) {
117126
return c, codecMatchExact
118127
}
119128
}
120129

121-
// Fallback to just MimeType
130+
// Fallback to just MimeType + Channels
122131
for _, c := range haystack {
123-
if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) {
132+
if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) &&
133+
fmtp.ClockRateEqual(c.RTPCodecCapability.MimeType,
134+
c.RTPCodecCapability.ClockRate,
135+
needle.RTPCodecCapability.ClockRate) &&
136+
fmtp.ChannelsEqual(c.RTPCodecCapability.MimeType,
137+
c.RTPCodecCapability.Channels,
138+
needle.RTPCodecCapability.Channels) {
124139
return c, codecMatchPartial
125140
}
126141
}

rtpsender_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func Test_RTPSender_ReplaceTrack(t *testing.T) { //nolint:cyclop
3636
trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
3737
assert.NoError(t, err)
3838

39-
trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264}, "video", "pion")
39+
trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, ClockRate: 90000}, "video", "pion")
4040
assert.NoError(t, err)
4141

4242
rtpSender, err := sender.AddTrack(trackA)

0 commit comments

Comments
 (0)