snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
This commit is contained in:
48
pkg/testutils/timeout.go
Normal file
48
pkg/testutils/timeout.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ConnectTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func WithTimeout(t *testing.T, f func() string, timeouts ...time.Duration) {
|
||||
timeout := ConnectTimeout
|
||||
if len(timeouts) > 0 {
|
||||
timeout = timeouts[0]
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
lastErr := ""
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if lastErr != "" {
|
||||
t.Fatalf("did not reach expected state after %v: %s", timeout, lastErr)
|
||||
}
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
lastErr = f()
|
||||
if lastErr == "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
pkg/testutils/vnettest/vnettest.go
Normal file
231
pkg/testutils/vnettest/vnettest.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user