Files
upstream-livekit/pkg/testutils/vnettest/vnettest.go
upstream-mirror 78704103fc
Some checks failed
Release to Docker / docker (push) Failing after 2s
Release Test Server to Docker / docker (push) Failing after 3s
Test / test (push) Failing after 16s
snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
2026-09-15 16:14:22 +05:00

232 lines
6.8 KiB
Go

// Copyright 2026 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 vnettest sets up real pion peer connections on an in-memory virtual
// network, for integration tests that exercise media paths without a server.
//
// It depends only on pion, so it can be imported both by tests inside pkg/... and by
// the top level test package.
package vnettest
import (
"fmt"
"io"
"sync"
"testing"
"time"
"github.com/pion/interceptor"
"github.com/pion/logging"
"github.com/pion/sdp/v3"
"github.com/pion/transport/v4/packetio"
"github.com/pion/transport/v4/vnet"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
)
const (
VP8PayloadType = 96
RTXPayloadType = VP8PayloadType + 1
OpusPayloadType = 111
)
// Hosts are the two ends of a started virtual network.
type Hosts struct {
OfferNet *vnet.Net
AnswerNet *vnet.Net
}
// NewHosts returns two hosts on a started virtual network, torn down with the test.
func NewHosts(t *testing.T) *Hosts {
t.Helper()
wan, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "1.2.3.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
offerNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{"1.2.3.4"}})
require.NoError(t, err)
require.NoError(t, wan.AddNet(offerNet))
answerNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{"1.2.3.5"}})
require.NoError(t, err)
require.NoError(t, wan.AddNet(answerNet))
require.NoError(t, wan.Start())
t.Cleanup(func() { _ = wan.Stop() })
return &Hosts{OfferNet: offerNet, AnswerNet: answerNet}
}
// NewSettingEngine returns a setting engine bound to net, with ICE timeouts short
// enough to keep tests quick.
func NewSettingEngine(net *vnet.Net) webrtc.SettingEngine {
se := webrtc.SettingEngine{}
se.SetNet(net)
se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4})
se.SetICETimeouts(5*time.Second, 5*time.Second, 500*time.Millisecond)
return se
}
// MediaEngineConfig describes what to register on a media engine.
type MediaEngineConfig struct {
Video bool // VP8 and its RTX codec; otherwise opus
HeaderExtensions bool // abs-send-time + transport-cc
SimulcastExtensions bool // mid + rid + rsid
}
func VideoRTCPFeedback() []webrtc.RTCPFeedback {
return []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
{Type: webrtc.TypeRTCPFBTransportCC},
{Type: webrtc.TypeRTCPFBGoogREMB},
}
}
func NewMediaEngine(t *testing.T, cfg MediaEngineConfig) *webrtc.MediaEngine {
t.Helper()
me := &webrtc.MediaEngine{}
kind := webrtc.RTPCodecTypeAudio
if cfg.Video {
kind = webrtc.RTPCodecTypeVideo
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, RTCPFeedback: VideoRTCPFeedback(),
},
PayloadType: VP8PayloadType,
}, kind))
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeRTX,
ClockRate: 90000,
SDPFmtpLine: fmt.Sprintf("apt=%d", VP8PayloadType),
},
PayloadType: RTXPayloadType,
}, kind))
} else {
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2,
},
PayloadType: OpusPayloadType,
}, kind))
}
if cfg.HeaderExtensions {
require.NoError(t, me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: sdp.ABSSendTimeURI}, kind))
require.NoError(t, me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, kind))
}
if cfg.SimulcastExtensions {
require.NoError(t, webrtc.ConfigureSimulcastExtensionHeaders(me))
}
return me
}
// PCConfig describes a peer connection on the virtual network.
type PCConfig struct {
Net *vnet.Net
MediaEngine MediaEngineConfig
// BufferFactory is SettingEngine.BufferFactory, e. g. buffer.Factory.GetOrNew.
// Optional.
BufferFactory func(packetType packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser
}
// NewPeerConnection builds a peer connection on the virtual network with no
// interceptors, so nothing rewrites what a test puts on the wire.
func NewPeerConnection(t *testing.T, cfg PCConfig) *webrtc.PeerConnection {
t.Helper()
se := NewSettingEngine(cfg.Net)
se.BufferFactory = cfg.BufferFactory
api := webrtc.NewAPI(
webrtc.WithMediaEngine(NewMediaEngine(t, cfg.MediaEngine)),
webrtc.WithSettingEngine(se),
webrtc.WithInterceptorRegistry(&interceptor.Registry{}),
)
pc, err := api.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
t.Cleanup(func() { _ = pc.Close() })
return pc
}
// GatheredOffer creates an offer and waits for gathering, so the SDP carries every
// candidate and the caller needs no trickle.
func GatheredOffer(t *testing.T, pc *webrtc.PeerConnection) webrtc.SessionDescription {
t.Helper()
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
gathered := webrtc.GatheringCompletePromise(pc)
require.NoError(t, pc.SetLocalDescription(offer))
<-gathered
return *pc.LocalDescription()
}
// SignalPair performs a full offer/answer exchange between two peer connections and
// waits for both to connect.
func SignalPair(t *testing.T, offerer, answerer *webrtc.PeerConnection) {
t.Helper()
connected := UntilConnected(offerer, answerer)
require.NoError(t, answerer.SetRemoteDescription(GatheredOffer(t, offerer)))
answer, err := answerer.CreateAnswer(nil)
require.NoError(t, err)
gathered := webrtc.GatheringCompletePromise(answerer)
require.NoError(t, answerer.SetLocalDescription(answer))
<-gathered
require.NoError(t, offerer.SetRemoteDescription(*answerer.LocalDescription()))
select {
case <-connected:
case <-time.After(30 * time.Second):
t.Fatal("timed out waiting for peer connections to connect")
}
}
// UntilConnected closes the returned channel once every peer connection is connected.
func UntilConnected(pcs ...*webrtc.PeerConnection) <-chan struct{} {
var wg sync.WaitGroup
wg.Add(len(pcs))
for _, pc := range pcs {
var once sync.Once
pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
if s == webrtc.PeerConnectionStateConnected {
once.Do(wg.Done)
}
})
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
return done
}