Skip to content

Commit

Permalink
Fix matching codecs with different rate or channels
Browse files Browse the repository at this point in the history
Currently codecs are matched regardless of the clock
rate and the channel count, and this makes impossible to fully support
codecs that might have a clock rate or channel count different than the
default one, in particular LPCM, PCMU, PCMA and multiopus (the last one
is a custom Opus variant present in the Chrome source code to support
multichannel Opus).

For instance, let's suppose a peer (receiver) wants to receive an audio
track encoded with LPCM, 48khz sample rate and 2 channels. This receiver
doesn't know the audio codec yet, therefore it advertises all supported
sample rates in the SDP:

```
LPCM/44100
LPCM/48000
LPCM/44100/2
LPCM/48000/2
```

The other peer (sender) receives the SDP, but since the clock rate and
channel count are not taken into consideration when matching codecs, the
sender codec `LPCM/48000/2` is wrongly associated with the receiver
codec `LPCM/44100`. The result is that the audio track cannot be decoded
correctly from the receiver side.

This patch fixes the issue and has been running smoothly in MediaMTX for
almost a year.

Unfortunately, in lots of examples and tests, clock rate and/or channels
are not present (and in fact they are producing horrible SDPs that
contain `VP8/0` instead of `VP8/90000` and are incompatible with lots of
servers) therefore this new check causes troubles in existing code. In
order to maintain compatibility, default clock rates and channels are
provided for most codecs.

In the future, it might be better to update examples (i can do it in a
future patch) and remove the exception.
  • Loading branch information
aler9 authored Feb 16, 2025
1 parent 70d06fd commit 969ab68
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 32 deletions.
108 changes: 90 additions & 18 deletions internal/fmtp/fmtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ import (
"strings"
)

func defaultClockRate(mimeType string) uint32 {
defaults := map[string]uint32{
"audio/opus": 48000,
"audio/pcmu": 8000,
"audio/pcma": 8000,
}

if def, ok := defaults[strings.ToLower(mimeType)]; ok {
return def
}

return 90000
}

func defaultChannels(mimeType string) uint16 {
defaults := map[string]uint16{
"audio/opus": 2,
}

if def, ok := defaults[strings.ToLower(mimeType)]; ok {
return def
}

return 0
}

func parseParameters(line string) map[string]string {
parameters := make(map[string]string)

Expand All @@ -24,6 +50,61 @@ func parseParameters(line string) map[string]string {
return parameters
}

// ClockRateEqual checks whether two clock rates are equal.
func ClockRateEqual(mimeType string, valA, valB uint32) bool {
// Lots of users use formats without setting clock rate or channels.
// In this case, use default values.
// It would be better to remove this exception in a future major release.
if valA == 0 {
valA = defaultClockRate(mimeType)
}
if valB == 0 {
valB = defaultClockRate(mimeType)
}

return valA == valB
}

// ChannelsEqual checks whether two channels are equal.
func ChannelsEqual(mimeType string, valA, valB uint16) bool {
// Lots of users use formats without setting clock rate or channels.
// In this case, use default values.
// It would be better to remove this exception in a future major release.
if valA == 0 {
valA = defaultChannels(mimeType)
}
if valB == 0 {
valB = defaultChannels(mimeType)
}

// RFC8866: channel count "is OPTIONAL and may be omitted
// if the number of channels is one".
if valA == 0 {
valA = 1
}
if valB == 0 {
valB = 1
}

return valA == valB
}

func paramsEqual(valA, valB map[string]string) bool {
for k, v := range valA {
if vb, ok := valB[k]; ok && !strings.EqualFold(vb, v) {
return false
}
}

for k, v := range valB {
if va, ok := valA[k]; ok && !strings.EqualFold(va, v) {
return false
}
}

return true
}

// FMTP interface for implementing custom
// FMTP parsers based on MimeType.
type FMTP interface {
Expand All @@ -39,7 +120,7 @@ type FMTP interface {
}

// Parse parses an fmtp string based on the MimeType.
func Parse(mimeType, line string) FMTP {
func Parse(mimeType string, clockRate uint32, channels uint16, line string) FMTP {
var fmtp FMTP

parameters := parseParameters(line)
Expand All @@ -63,6 +144,8 @@ func Parse(mimeType, line string) FMTP {
default:
fmtp = &genericFMTP{
mimeType: mimeType,
clockRate: clockRate,
channels: channels,
parameters: parameters,
}
}
Expand All @@ -72,6 +155,8 @@ func Parse(mimeType, line string) FMTP {

type genericFMTP struct {
mimeType string
clockRate uint32
channels uint16
parameters map[string]string
}

Expand All @@ -87,23 +172,10 @@ func (g *genericFMTP) Match(b FMTP) bool {
return false
}

if !strings.EqualFold(g.mimeType, fmtp.MimeType()) {
return false
}

for k, v := range g.parameters {
if vb, ok := fmtp.parameters[k]; ok && !strings.EqualFold(vb, v) {
return false
}
}

for k, v := range fmtp.parameters {
if va, ok := g.parameters[k]; ok && !strings.EqualFold(va, v) {
return false
}
}

return true
return strings.EqualFold(g.mimeType, fmtp.MimeType()) &&
ClockRateEqual(g.mimeType, g.clockRate, fmtp.clockRate) &&
ChannelsEqual(g.mimeType, g.channels, fmtp.channels) &&
paramsEqual(g.parameters, fmtp.parameters)
}

func (g *genericFMTP) Parameter(key string) (string, bool) {
Expand Down
Loading

0 comments on commit 969ab68

Please sign in to comment.