Files
webrtc/internal/fmtp/fmtp.go
Alessandro Ros 969ab684e3 Fix matching codecs with different rate or channels
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.
2025-02-15 21:16:47 -05:00

186 lines
4.0 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package fmtp implements per codec parsing of fmtp lines
package fmtp
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)
for _, p := range strings.Split(line, ";") {
pp := strings.SplitN(strings.TrimSpace(p), "=", 2)
key := strings.ToLower(pp[0])
var value string
if len(pp) > 1 {
value = pp[1]
}
parameters[key] = value
}
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 {
// MimeType returns the MimeType associated with
// the fmtp
MimeType() string
// Match compares two fmtp descriptions for
// compatibility based on the MimeType
Match(f FMTP) bool
// Parameter returns a value for the associated key
// if contained in the parsed fmtp string
Parameter(key string) (string, bool)
}
// Parse parses an fmtp string based on the MimeType.
func Parse(mimeType string, clockRate uint32, channels uint16, line string) FMTP {
var fmtp FMTP
parameters := parseParameters(line)
switch {
case strings.EqualFold(mimeType, "video/h264"):
fmtp = &h264FMTP{
parameters: parameters,
}
case strings.EqualFold(mimeType, "video/vp9"):
fmtp = &vp9FMTP{
parameters: parameters,
}
case strings.EqualFold(mimeType, "video/av1"):
fmtp = &av1FMTP{
parameters: parameters,
}
default:
fmtp = &genericFMTP{
mimeType: mimeType,
clockRate: clockRate,
channels: channels,
parameters: parameters,
}
}
return fmtp
}
type genericFMTP struct {
mimeType string
clockRate uint32
channels uint16
parameters map[string]string
}
func (g *genericFMTP) MimeType() string {
return g.mimeType
}
// Match returns true if g and b are compatible fmtp descriptions
// The generic implementation is used for MimeTypes that are not defined.
func (g *genericFMTP) Match(b FMTP) bool {
fmtp, ok := b.(*genericFMTP)
if !ok {
return false
}
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) {
v, ok := g.parameters[key]
return v, ok
}