Skip to content

Commit 7f1ace7

Browse files
committed
Fix codec matching with different rate or channels
Consider clock rate and channels when matching codecs. This allows to support codecs with the same MIME type but different sample rate or channel count, like PCMU, PCMA, LPCM and multiopus.
1 parent 47bde05 commit 7f1ace7

7 files changed

+213
-45
lines changed

internal/fmtp/fmtp.go

+94-28
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,39 @@ 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 {
48-
case strings.EqualFold(mimeType, "video/h264"):
49-
fmtp = &h264FMTP{
50-
parameters: parameters,
51-
}
52-
53-
case strings.EqualFold(mimeType, "video/vp9"):
54-
fmtp = &vp9FMTP{
55-
parameters: parameters,
116+
case clockRate == 90000 && (channels == 0 || channels == 1):
117+
switch {
118+
case strings.EqualFold(mimeType, "video/vp9"):
119+
fmtp = &vp9FMTP{
120+
parameters: parameters,
121+
}
122+
123+
case strings.EqualFold(mimeType, "video/av1"):
124+
fmtp = &av1FMTP{
125+
parameters: parameters,
126+
}
56127
}
57128

58-
case strings.EqualFold(mimeType, "video/av1"):
59-
fmtp = &av1FMTP{
129+
// Clock rate and channel checks have been introduced quite recently.
130+
// Existing implementations often use VP8, H264 or Opus without setting clock rate or channels.
131+
// Keep compatibility with these situations.
132+
// It would be better to add a clock rate and channel check in a future major release.
133+
case strings.EqualFold(mimeType, "video/h264"):
134+
fmtp = &h264FMTP{
60135
parameters: parameters,
61136
}
62137

63138
default:
64139
fmtp = &genericFMTP{
65140
mimeType: mimeType,
141+
clockRate: clockRate,
142+
channels: channels,
66143
parameters: parameters,
67144
}
68145
}
@@ -72,6 +149,8 @@ func Parse(mimeType, line string) FMTP {
72149

73150
type genericFMTP struct {
74151
mimeType string
152+
clockRate uint32
153+
channels uint16
75154
parameters map[string]string
76155
}
77156

@@ -87,23 +166,10 @@ func (g *genericFMTP) Match(b FMTP) bool {
87166
return false
88167
}
89168

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
169+
return strings.EqualFold(g.mimeType, fmtp.MimeType()) &&
170+
ClockRateEqual(g.mimeType, g.clockRate, fmtp.clockRate) &&
171+
ChannelsEqual(g.mimeType, g.channels, fmtp.channels) &&
172+
paramsEqual(g.parameters, fmtp.parameters)
107173
}
108174

109175
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1823,7 +1823,7 @@ 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(RTPCodecCapability{MimeType: MimeTypePCMU, ClockRate: 8000}, "audio", "audio")
18271827
require.NoError(t, err)
18281828

18291829
_, err = pcOffer.AddTrack(audioTrack)

rtpcodec.go

+16-5
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,30 @@ 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, c.RTPCodecCapability.ClockRate, needle.RTPCodecCapability.ClockRate) &&
134+
fmtp.ChannelsEqual(c.RTPCodecCapability.MimeType, c.RTPCodecCapability.Channels, needle.RTPCodecCapability.Channels) {
124135
return c, codecMatchPartial
125136
}
126137
}

0 commit comments

Comments
 (0)