mirror of
https://github.com/livekit/livekit.git
synced 2026-08-15 22:09:43 +00:00
Users can explicitly enable this profile if they are certain that all device support it.
225 lines
9.0 KiB
Go
225 lines
9.0 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"
|
|
h264BaselineProfilePacketizationMode0Fmtp = "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f"
|
|
h264BaselineProfilePacketizationMode1Fmtp = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"
|
|
)
|
|
|
|
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("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=1;profile-level-id=640032",
|
|
} {
|
|
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, fmtp), "missing %q in %v", fmtp, codecs)
|
|
}
|
|
})
|
|
|
|
t.Run("main/baseline 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)
|
|
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264BaselineProfilePacketizationMode0Fmtp), "%v", codecs)
|
|
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264BaselineProfilePacketizationMode1Fmtp), "%v", codecs)
|
|
})
|
|
|
|
t.Run("main/baseline profile is offered when explicitly enabled", func(t *testing.T) {
|
|
for _, fmtp := range []string{
|
|
h264MainProfilePacketizationMode0Fmtp,
|
|
h264MainProfilePacketizationMode1Fmtp,
|
|
h264BaselineProfilePacketizationMode0Fmtp,
|
|
h264BaselineProfilePacketizationMode1Fmtp,
|
|
} {
|
|
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=42e01f"), "%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
|
|
}
|
|
}
|
|
}
|