snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
This commit is contained in:
42
internal/fmtp/av1.go
Normal file
42
internal/fmtp/av1.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
type av1FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *av1FMTP) MimeType() string {
|
||||
return "video/av1"
|
||||
}
|
||||
|
||||
func (h *av1FMTP) Match(b FMTP) bool {
|
||||
c, ok := b.(*av1FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// RTP Payload Format For AV1 (v1.0)
|
||||
// https://aomediacodec.github.io/av1-rtp-spec/
|
||||
// If the profile parameter is not present, it MUST be inferred to be 0 (“Main” profile).
|
||||
hProfile, ok := h.parameters["profile"]
|
||||
if !ok {
|
||||
hProfile = "0"
|
||||
}
|
||||
cProfile, ok := c.parameters["profile"]
|
||||
if !ok {
|
||||
cProfile = "0"
|
||||
}
|
||||
if hProfile != cProfile {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *av1FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
|
||||
return v, ok
|
||||
}
|
||||
190
internal/fmtp/fmtp.go
Normal file
190
internal/fmtp/fmtp.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// SPDX-FileCopyrightText: 2026 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.SplitSeq(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/h265"):
|
||||
fmtp = &h265FMTP{
|
||||
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
|
||||
}
|
||||
768
internal/fmtp/fmtp_test.go
Normal file
768
internal/fmtp/fmtp_test.go
Normal file
@@ -0,0 +1,768 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseParameters(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
line string
|
||||
parameters map[string]string
|
||||
}{
|
||||
{
|
||||
"one param",
|
||||
"key-name=value",
|
||||
map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
{
|
||||
"one param with white spaces",
|
||||
"\tkey-name=value ",
|
||||
map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
{
|
||||
"two params",
|
||||
"key-name=value;key2=value2",
|
||||
map[string]string{
|
||||
"key-name": "value",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
{
|
||||
"two params with white spaces",
|
||||
"key-name=value; \n\tkey2=value2 ",
|
||||
map[string]string{
|
||||
"key-name": "value",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
parameters := parseParameters(ca.line)
|
||||
assert.Equal(t, ca.parameters, parameters)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
mimeType string
|
||||
clockRate uint32
|
||||
channels uint16
|
||||
line string
|
||||
expected FMTP
|
||||
}{
|
||||
{
|
||||
"generic",
|
||||
"generic",
|
||||
90000,
|
||||
2,
|
||||
"key-name=value",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
clockRate: 90000,
|
||||
channels: 2,
|
||||
parameters: map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"generic case normalization",
|
||||
"generic",
|
||||
90000,
|
||||
2,
|
||||
"Key=value",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
clockRate: 90000,
|
||||
channels: 2,
|
||||
parameters: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"h264",
|
||||
"video/h264",
|
||||
90000,
|
||||
0,
|
||||
"key-name=value",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"h265",
|
||||
"video/h265",
|
||||
90000,
|
||||
0,
|
||||
"key-name=value",
|
||||
&h265FMTP{
|
||||
parameters: map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"vp9",
|
||||
"video/vp9",
|
||||
90000,
|
||||
0,
|
||||
"key-name=value",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"av1",
|
||||
"video/av1",
|
||||
90000,
|
||||
0,
|
||||
"key-name=value",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"key-name": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
f := Parse(ca.mimeType, ca.clockRate, ca.channels, ca.line)
|
||||
|
||||
assert.Equal(t, ca.expected, f)
|
||||
assert.Equal(t, ca.mimeType, f.MimeType())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) { //nolint:maintidx
|
||||
consistString := map[bool]string{true: "consist", false: "inconsist"}
|
||||
parseH265 := func(line string) FMTP {
|
||||
return Parse("video/h265", 90000, 0, line)
|
||||
}
|
||||
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
a FMTP
|
||||
b FMTP
|
||||
consist bool
|
||||
}{
|
||||
{
|
||||
"generic equal",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"generic one extra param",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
"key4": "value4",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"generic inferred channels",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
channels: 1,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"generic inconsistent different kind",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&h264FMTP{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"generic inconsistent different mime type",
|
||||
&genericFMTP{
|
||||
mimeType: "generic1",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic2",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"generic inconsistent different clock rate",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
clockRate: 90000,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
clockRate: 48000,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"generic inconsistent different channels",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
clockRate: 90000,
|
||||
channels: 2,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
clockRate: 90000,
|
||||
channels: 1,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"generic inconsistent different parameters",
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "generic",
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "different_value",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h264 equal",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"level-asymmetry-allowed": "1",
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"level-asymmetry-allowed": "1",
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"h264 one extra param",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"level-asymmetry-allowed": "1",
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"h264 different profile level ids version",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e029",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"h264 inconsistent different kind",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "0",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&genericFMTP{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h264 inconsistent different parameters",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "0",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h264 inconsistent missing packetization mode",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "0",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h264 inconsistent missing profile level id",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e01f",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h264 inconsistent invalid profile level id",
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "42e029",
|
||||
},
|
||||
},
|
||||
&h264FMTP{
|
||||
parameters: map[string]string{
|
||||
"packetization-mode": "1",
|
||||
"profile-level-id": "41e029",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h265 downgraded level",
|
||||
parseH265("level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST"),
|
||||
parseH265("level-id=93;profile-id=1;tier-flag=0;tx-mode=SRST"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"h265 inferred defaults and ignored sprop parameter",
|
||||
parseH265(""),
|
||||
parseH265("profile-id=1;tier-flag=0;tx-mode=srst;sprop-vps=ignored"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"h265 inconsistent different kind",
|
||||
parseH265(""),
|
||||
&genericFMTP{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h265 inconsistent different profile",
|
||||
parseH265("profile-id=1"),
|
||||
parseH265("profile-id=2"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h265 inconsistent different tier",
|
||||
parseH265("tier-flag=0"),
|
||||
parseH265("tier-flag=1"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"h265 inconsistent different transmission mode",
|
||||
parseH265("tx-mode=SRST"),
|
||||
parseH265("tx-mode=MRST"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"vp9 equal",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "1",
|
||||
},
|
||||
},
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "1",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"vp9 missing profile",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"vp9 inferred profile",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "0",
|
||||
},
|
||||
},
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"vp9 inconsistent different kind",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "0",
|
||||
},
|
||||
},
|
||||
&genericFMTP{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"vp9 inconsistent different profile",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "0",
|
||||
},
|
||||
},
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "1",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"vp9 inconsistent different inferred profile",
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
&vp9FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile-id": "1",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"av1 equal",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "1",
|
||||
},
|
||||
},
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "1",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"av1 missing profile",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"av1 inferred profile",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "0",
|
||||
},
|
||||
},
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"av1 inconsistent different kind",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "0",
|
||||
},
|
||||
},
|
||||
&genericFMTP{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"av1 inconsistent different profile",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "0",
|
||||
},
|
||||
},
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "1",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"av1 inconsistent different inferred profile",
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{},
|
||||
},
|
||||
&av1FMTP{
|
||||
parameters: map[string]string{
|
||||
"profile": "1",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"pcmu channels",
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 8000,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 8000,
|
||||
channels: 1,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"pcmu inconsistent channels",
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 8000,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 8000,
|
||||
channels: 2,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"pcmu clockrate",
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 0,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 8000,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"pcmu inconsistent clockrate",
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 0,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "audio/pcmu",
|
||||
clockRate: 16000,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"opus clockrate",
|
||||
&genericFMTP{
|
||||
mimeType: "audio/opus",
|
||||
clockRate: 0,
|
||||
channels: 0,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
&genericFMTP{
|
||||
mimeType: "audio/opus",
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
parameters: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
c := ca.a.Match(ca.b)
|
||||
assert.Equal(t, ca.consist, c)
|
||||
assert.Equal(
|
||||
t, ca.consist, c,
|
||||
"'%s' and '%s' are expected to be %s, but treated as %s",
|
||||
ca.a, ca.b, consistString[ca.consist], consistString[c],
|
||||
)
|
||||
|
||||
c = ca.b.Match(ca.a)
|
||||
assert.Equalf(
|
||||
t, ca.consist, c,
|
||||
"'%s' and '%s' are expected to be %s, but treated as %s",
|
||||
ca.b, ca.a, consistString[ca.consist], consistString[c],
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
86
internal/fmtp/h264.go
Normal file
86
internal/fmtp/h264.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func profileLevelIDMatches(a, b string) bool {
|
||||
aa, err := hex.DecodeString(a)
|
||||
if err != nil || len(aa) < 2 {
|
||||
return false
|
||||
}
|
||||
bb, err := hex.DecodeString(b)
|
||||
if err != nil || len(bb) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return aa[0] == bb[0] && aa[1] == bb[1]
|
||||
}
|
||||
|
||||
type h264FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *h264FMTP) MimeType() string {
|
||||
return "video/h264"
|
||||
}
|
||||
|
||||
// Match returns true if h and b are compatible fmtp descriptions
|
||||
// Based on RFC6184 Section 8.2.2:
|
||||
//
|
||||
// The parameters identifying a media format configuration for H.264
|
||||
// are profile-level-id and packetization-mode. These media format
|
||||
// configuration parameters (except for the level part of profile-
|
||||
// level-id) MUST be used symmetrically; that is, the answerer MUST
|
||||
// either maintain all configuration parameters or remove the media
|
||||
// format (payload type) completely if one or more of the parameter
|
||||
// values are not supported.
|
||||
// Informative note: The requirement for symmetric use does not
|
||||
// apply for the level part of profile-level-id and does not apply
|
||||
// for the other stream properties and capability parameters.
|
||||
func (h *h264FMTP) Match(b FMTP) bool {
|
||||
fmtp, ok := b.(*h264FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// test packetization-mode
|
||||
hpmode, hok := h.parameters["packetization-mode"]
|
||||
if !hok {
|
||||
return false
|
||||
}
|
||||
cpmode, cok := fmtp.parameters["packetization-mode"]
|
||||
if !cok {
|
||||
return false
|
||||
}
|
||||
|
||||
if hpmode != cpmode {
|
||||
return false
|
||||
}
|
||||
|
||||
// test profile-level-id
|
||||
hplid, hok := h.parameters["profile-level-id"]
|
||||
if !hok {
|
||||
return false
|
||||
}
|
||||
|
||||
cplid, cok := fmtp.parameters["profile-level-id"]
|
||||
if !cok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !profileLevelIDMatches(hplid, cplid) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *h264FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
|
||||
return v, ok
|
||||
}
|
||||
57
internal/fmtp/h265.go
Normal file
57
internal/fmtp/h265.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
import "strings"
|
||||
|
||||
type h265FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *h265FMTP) MimeType() string {
|
||||
return "video/h265"
|
||||
}
|
||||
|
||||
// Match returns true if h and b describe compatible H.265 media formats.
|
||||
// draft-ietf-avtcore-hevc-webrtc-09 Section 2.1 defines profile-id,
|
||||
// tier-flag, and tx-mode as the H.265 media format parameters for WebRTC:
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-avtcore-hevc-webrtc-09#section-2.1
|
||||
//
|
||||
// level-id is intentionally not compared, as required by W3C WebRTC candidate
|
||||
// correction 52:
|
||||
// https://www.w3.org/TR/webrtc/#codec-match-without-level-id
|
||||
// RFC 7798 Section 7.2.2 permits an answer to lower it, so treating it as a
|
||||
// symmetric parameter would reject a valid offer/answer exchange:
|
||||
// https://www.rfc-editor.org/rfc/rfc7798.html#section-7.2.2
|
||||
func (h *h265FMTP) Match(b FMTP) bool {
|
||||
fmtp, ok := b.(*h265FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.EqualFold(
|
||||
h.parameterOrDefault("profile-id", "1"),
|
||||
fmtp.parameterOrDefault("profile-id", "1"),
|
||||
) && strings.EqualFold(
|
||||
h.parameterOrDefault("tier-flag", "0"),
|
||||
fmtp.parameterOrDefault("tier-flag", "0"),
|
||||
) && strings.EqualFold(
|
||||
h.parameterOrDefault("tx-mode", "SRST"),
|
||||
fmtp.parameterOrDefault("tx-mode", "SRST"),
|
||||
)
|
||||
}
|
||||
|
||||
func (h *h265FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (h *h265FMTP) parameterOrDefault(key, defaultValue string) string {
|
||||
if value, ok := h.Parameter(key); ok {
|
||||
return value
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
42
internal/fmtp/vp9.go
Normal file
42
internal/fmtp/vp9.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fmtp
|
||||
|
||||
type vp9FMTP struct {
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
func (h *vp9FMTP) MimeType() string {
|
||||
return "video/vp9"
|
||||
}
|
||||
|
||||
func (h *vp9FMTP) Match(b FMTP) bool {
|
||||
c, ok := b.(*vp9FMTP)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// RTP Payload Format for VP9 Video - draft-ietf-payload-vp9-16
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16
|
||||
// If no profile-id is present, Profile 0 MUST be inferred
|
||||
hProfileID, ok := h.parameters["profile-id"]
|
||||
if !ok {
|
||||
hProfileID = "0"
|
||||
}
|
||||
cProfileID, ok := c.parameters["profile-id"]
|
||||
if !ok {
|
||||
cProfileID = "0"
|
||||
}
|
||||
if hProfileID != cProfileID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *vp9FMTP) Parameter(key string) (string, bool) {
|
||||
v, ok := h.parameters[key]
|
||||
|
||||
return v, ok
|
||||
}
|
||||
Reference in New Issue
Block a user