mirror of
https://github.com/livekit/livekit.git
synced 2026-08-04 19:48:40 +00:00
* Register h264 main profile if enabled explicitly We don't support the h264 main profile for compatibility, user can enabled it by set fmtp explicitly in codec config to enable it if want to use it in special scenario. * go mod
221 lines
8.6 KiB
Go
221 lines
8.6 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package rtc
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/livekit/protocol/codecs/mime"
|
|
"github.com/livekit/protocol/livekit"
|
|
|
|
"github.com/livekit/livekit-server/pkg/config"
|
|
)
|
|
|
|
const (
|
|
h264MainProfilePacketizationMode0Fmtp = "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f"
|
|
h264MainProfilePacketizationMode1Fmtp = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f"
|
|
)
|
|
|
|
func TestIsCodecEnabled(t *testing.T) {
|
|
t.Run("empty fmtp requirement should match all", func(t *testing.T) {
|
|
enabledCodecs := []*livekit.Codec{{Mime: "video/h264"}}
|
|
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String(), SDPFmtpLine: "special"}))
|
|
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String()}))
|
|
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}))
|
|
})
|
|
|
|
t.Run("when fmtp is provided, require match", func(t *testing.T) {
|
|
enabledCodecs := []*livekit.Codec{{Mime: "video/h264", FmtpLine: "special"}}
|
|
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String(), SDPFmtpLine: "special"}))
|
|
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String()}))
|
|
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}))
|
|
})
|
|
|
|
t.Run("strict fmtp requires an exact fmtp, mime alone is not enough", func(t *testing.T) {
|
|
cap := webrtc.RTPCodecCapability{
|
|
MimeType: mime.MimeTypeH264.String(),
|
|
SDPFmtpLine: h264MainProfilePacketizationMode1Fmtp,
|
|
}
|
|
|
|
// mime-only config opts into every non-strict codec, but never a strict one
|
|
mimeOnly := []*livekit.Codec{{Mime: "video/h264"}}
|
|
require.True(t, isCodecEnabledWithFmtp(mimeOnly, cap, false))
|
|
require.False(t, isCodecEnabledWithFmtp(mimeOnly, cap, true))
|
|
|
|
// spelling the fmtp out enables it
|
|
explicit := []*livekit.Codec{{Mime: "video/h264", FmtpLine: h264MainProfilePacketizationMode1Fmtp}}
|
|
require.True(t, isCodecEnabledWithFmtp(explicit, cap, true))
|
|
|
|
// ...but only for the profile that was asked for
|
|
require.False(t, isCodecEnabledWithFmtp(explicit, webrtc.RTPCodecCapability{
|
|
MimeType: mime.MimeTypeH264.String(),
|
|
SDPFmtpLine: h264MainProfilePacketizationMode0Fmtp,
|
|
}, true))
|
|
})
|
|
}
|
|
|
|
type offeredCodec struct {
|
|
payloadType webrtc.PayloadType
|
|
name string
|
|
fmtp string
|
|
}
|
|
|
|
func (o offeredCodec) String() string {
|
|
return strconv.Itoa(int(o.payloadType)) + " " + o.name + " " + o.fmtp
|
|
}
|
|
|
|
// offeredVideoCodecs returns the video codecs an offer built from enabledCodecs carries.
|
|
// It goes through a real offer rather than inspecting the MediaEngine so that payload
|
|
// type assignment, which is what collides, is covered too.
|
|
func offeredVideoCodecs(t *testing.T, enabledCodecs []*livekit.Codec, filterOutH264HighProfile bool) []offeredCodec {
|
|
t.Helper()
|
|
|
|
me, err := createMediaEngine(enabledCodecs, DirectionConfig{}, filterOutH264HighProfile)
|
|
require.NoError(t, err)
|
|
|
|
pc, err := webrtc.NewAPI(webrtc.WithMediaEngine(me)).NewPeerConnection(webrtc.Configuration{})
|
|
require.NoError(t, err)
|
|
defer pc.Close()
|
|
|
|
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
|
|
require.NoError(t, err)
|
|
|
|
offer, err := pc.CreateOffer(nil)
|
|
require.NoError(t, err)
|
|
|
|
var codecs []offeredCodec
|
|
indexByPayloadType := make(map[webrtc.PayloadType]int)
|
|
for _, line := range strings.Split(offer.SDP, "\r\n") {
|
|
attr, value, found := strings.Cut(line, ":")
|
|
if !found || (attr != "a=rtpmap" && attr != "a=fmtp") {
|
|
continue
|
|
}
|
|
|
|
rawPayloadType, rest, _ := strings.Cut(value, " ")
|
|
payloadType, err := strconv.Atoi(rawPayloadType)
|
|
require.NoError(t, err)
|
|
|
|
if attr == "a=rtpmap" {
|
|
indexByPayloadType[webrtc.PayloadType(payloadType)] = len(codecs)
|
|
codecs = append(codecs, offeredCodec{payloadType: webrtc.PayloadType(payloadType), name: rest})
|
|
continue
|
|
}
|
|
|
|
i, ok := indexByPayloadType[webrtc.PayloadType(payloadType)]
|
|
require.True(t, ok, "fmtp for payload type %d without an rtpmap", payloadType)
|
|
codecs[i].fmtp = rest
|
|
}
|
|
|
|
require.NotEmpty(t, codecs)
|
|
return codecs
|
|
}
|
|
|
|
func hasOfferedFmtp(codecs []offeredCodec, mimeType mime.MimeType, fmtp string) bool {
|
|
for _, c := range codecs {
|
|
name, _, _ := strings.Cut(c.name, "/")
|
|
if mime.NormalizeMimeType("video/"+name) == mimeType && c.fmtp == fmtp {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// defaultEnabledCodecs mirrors what roomallocator derives from config, so these tests
|
|
// track the shipped default codec set instead of a hand-maintained copy of it.
|
|
func defaultEnabledCodecs() []*livekit.Codec {
|
|
codecs := make([]*livekit.Codec, 0, len(config.DefaultConfig.Room.EnabledCodecs))
|
|
for _, c := range config.DefaultConfig.Room.EnabledCodecs {
|
|
codecs = append(codecs, &livekit.Codec{Mime: c.Mime, FmtpLine: c.FmtpLine})
|
|
}
|
|
return codecs
|
|
}
|
|
|
|
func TestRegisterCodecsH264Profiles(t *testing.T) {
|
|
t.Run("baseline and constrained baseline are offered by default", func(t *testing.T) {
|
|
codecs := offeredVideoCodecs(t, defaultEnabledCodecs(), false)
|
|
|
|
for _, fmtp := range []string{
|
|
"level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
|
|
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
|
"level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f",
|
|
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
|
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032",
|
|
} {
|
|
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, fmtp), "missing %q in %v", fmtp, codecs)
|
|
}
|
|
})
|
|
|
|
t.Run("main profile is not offered unless explicitly enabled", func(t *testing.T) {
|
|
codecs := offeredVideoCodecs(t, defaultEnabledCodecs(), false)
|
|
|
|
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264MainProfilePacketizationMode0Fmtp), "%v", codecs)
|
|
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264MainProfilePacketizationMode1Fmtp), "%v", codecs)
|
|
})
|
|
|
|
t.Run("main profile is offered when explicitly enabled", func(t *testing.T) {
|
|
for _, fmtp := range []string{
|
|
h264MainProfilePacketizationMode0Fmtp,
|
|
h264MainProfilePacketizationMode1Fmtp,
|
|
} {
|
|
t.Run(fmtp, func(t *testing.T) {
|
|
enabled := append(defaultEnabledCodecs(), &livekit.Codec{
|
|
Mime: mime.MimeTypeH264.String(),
|
|
FmtpLine: fmtp,
|
|
})
|
|
|
|
codecs := offeredVideoCodecs(t, enabled, false)
|
|
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, fmtp), "missing %q in %v", fmtp, codecs)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("high profile is filtered out for the answerer", func(t *testing.T) {
|
|
highProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
|
|
|
|
codecs := offeredVideoCodecs(t, defaultEnabledCodecs(), true)
|
|
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, highProfileFmtp), "%v", codecs)
|
|
|
|
// the other profiles survive the filter
|
|
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264,
|
|
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"), "%v", codecs)
|
|
})
|
|
}
|
|
|
|
// Every video codec claims its own payload type plus the next one for its RTX stream,
|
|
// so a codec whose payload type lands on another's RTX slot makes pion refuse to build
|
|
// the MediaEngine at all.
|
|
func TestRegisterCodecsPayloadTypesAreUnique(t *testing.T) {
|
|
enabled := append(defaultEnabledCodecs(),
|
|
&livekit.Codec{Mime: mime.MimeTypeH264.String(), FmtpLine: h264MainProfilePacketizationMode0Fmtp},
|
|
&livekit.Codec{Mime: mime.MimeTypeH264.String(), FmtpLine: h264MainProfilePacketizationMode1Fmtp},
|
|
)
|
|
|
|
for _, filterOutH264HighProfile := range []bool{false, true} {
|
|
codecs := offeredVideoCodecs(t, enabled, filterOutH264HighProfile)
|
|
|
|
seen := make(map[webrtc.PayloadType]offeredCodec, len(codecs))
|
|
for _, c := range codecs {
|
|
previous, duplicate := seen[c.payloadType]
|
|
require.False(t, duplicate, "payload type %d claimed by both %v and %v", c.payloadType, previous, c)
|
|
seen[c.payloadType] = c
|
|
}
|
|
}
|
|
}
|