diff --git a/.github/.ci.conf b/.github/.ci.conf new file mode 100644 index 0000000..e6d4d23 --- /dev/null +++ b/.github/.ci.conf @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 The Pion community +# SPDX-License-Identifier: MIT + +GO_MOD_VERSION_EXPECTED=1.24 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4a9132 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module github.com/pion/bwe + +go 1.24 + +require ( + github.com/pion/interceptor v0.1.41-0.20250918133005-ab70b00249ad + github.com/pion/logging v0.2.4 + github.com/pion/rtcp v1.2.15 + github.com/pion/rtp v1.8.22 + github.com/pion/transport/v3 v3.0.8 + github.com/pion/webrtc/v4 v4.1.4 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.0.7 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.15 // indirect + github.com/pion/srtp/v3 v3.0.7 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/turn/v4 v4.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7b6d911 --- /dev/null +++ b/go.sum @@ -0,0 +1,57 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.41-0.20250918133005-ab70b00249ad h1:9Md9jf21oboaul3cm0ss/hn6KG0xsJ7CzPJjdDnpJqk= +github.com/pion/interceptor v0.1.41-0.20250918133005-ab70b00249ad/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.22 h1:8NCVDDF+uSJmMUkjLJVnIr/HX7gPesyMV1xFt5xozXc= +github.com/pion/rtp v1.8.22/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.15 h1:F0I1zds+K/+37ZrzdADmx2Q44OFDOPRLhPnNTaUX9hk= +github.com/pion/sdp/v3 v3.0.15/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs= +github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= +github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= +github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= +github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M= +github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/simulation/log_format_test.go b/simulation/log_format_test.go new file mode 100644 index 0000000..8ddb7af --- /dev/null +++ b/simulation/log_format_test.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js + +package simulation + +import ( + "fmt" + "log/slog" + "time" + + "github.com/pion/interceptor" + "github.com/pion/rtcp" + "github.com/pion/rtp" +) + +type packetLogger struct { + vantagePoint string + direction string +} + +func (l *packetLogger) LogRTPPacket(header *rtp.Header, payload []byte, attributes interceptor.Attributes) { + ts := time.Now() + slog.Info( + "rtp", + "vantage-point", l.vantagePoint, + "direction", l.direction, + "ts", ts, + "pt", header.PayloadType, + "ssrc", header.SSRC, + "sequence-number", header.SequenceNumber, + "rtp-timestamp", header.Timestamp, + "marker", header.Marker, + "payload-size", len(payload), + ) +} + +func (l *packetLogger) LogRTCPPackets(pkts []rtcp.Packet, attributes interceptor.Attributes) { + for _, pkt := range pkts { + slog.Info("rtcp", "vantage-point", l.vantagePoint, "direction", l.direction, "type", fmt.Sprintf("%T", pkt)) + } +} diff --git a/simulation/peer_test.go b/simulation/peer_test.go new file mode 100644 index 0000000..b601eee --- /dev/null +++ b/simulation/peer_test.go @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js + +package simulation + +import ( + "github.com/pion/interceptor" + "github.com/pion/interceptor/pkg/packetdump" + "github.com/pion/interceptor/pkg/rfc8888" + "github.com/pion/interceptor/pkg/rtpfb" + "github.com/pion/logging" + "github.com/pion/transport/v3/vnet" + "github.com/pion/webrtc/v4" +) + +type option func(*peer) error + +func setVNet(vnet *vnet.Net, publicIPs []string) option { + return func(p *peer) error { + p.settingEngine.SetNet(vnet) + p.settingEngine.SetNAT1To1IPs(publicIPs, webrtc.ICECandidateTypeHost) + + return nil + } +} + +func onRemoteTrack(handler func(*webrtc.TrackRemote)) option { + return func(p *peer) error { + p.onRemoteTrack = handler + + return nil + } +} + +func onConnected(handler func()) option { + return func(p *peer) error { + p.onConnected = handler + + return nil + } +} + +func registerDefaultCodecs() option { + return func(p *peer) error { + return p.mediaEngine.RegisterDefaultCodecs() + } +} + +func registerPacketLogger(vantagePoint string) option { + return func(p *peer) error { + ipl := &packetLogger{vantagePoint: vantagePoint, direction: "in"} + rd, err := packetdump.NewReceiverInterceptor(packetdump.PacketLog(ipl)) + if err != nil { + return err + } + opl := &packetLogger{vantagePoint: vantagePoint, direction: "out"} + sd, err := packetdump.NewSenderInterceptor(packetdump.PacketLog(opl)) + if err != nil { + return err + } + p.interceptorRegistry.Add(rd) + p.interceptorRegistry.Add(sd) + + return nil + } +} + +func registerRTPFB() option { + return func(p *peer) error { + rtpfb, err := rtpfb.NewInterceptor() + if err != nil { + return err + } + p.interceptorRegistry.Add(rtpfb) + + return nil + } +} + +// func registerTWCC() option { +// return func(p *peer) error { +// twcc, err := twcc.NewSenderInterceptor() +// if err != nil { +// return err +// } +// p.interceptorRegistry.Add(twcc) +// +// return nil +// } +// } +// +// func registerTWCCHeaderExtension() option { +// return func(p *peer) error { +// twccHdrExt, err := twcc.NewHeaderExtensionInterceptor() +// if err != nil { +// return err +// } +// p.interceptorRegistry.Add(twccHdrExt) +// +// return nil +// } +// } + +func registerCCFB() option { + return func(p *peer) error { + ccfb, err := rfc8888.NewSenderInterceptor() + if err != nil { + return err + } + p.interceptorRegistry.Add(ccfb) + + return nil + } +} + +type peer struct { + logger logging.LeveledLogger + pc *webrtc.PeerConnection + + settingEngine *webrtc.SettingEngine + mediaEngine *webrtc.MediaEngine + interceptorRegistry *interceptor.Registry + + onRemoteTrack func(*webrtc.TrackRemote) + onConnected func() +} + +func newPeer(opts ...option) (*peer, error) { + peer := &peer{ + logger: logging.NewDefaultLoggerFactory().NewLogger("bwe_test_peer"), + pc: nil, + settingEngine: &webrtc.SettingEngine{}, + mediaEngine: &webrtc.MediaEngine{}, + interceptorRegistry: &interceptor.Registry{}, + onRemoteTrack: nil, + onConnected: nil, + } + for _, opt := range opts { + if err := opt(peer); err != nil { + return nil, err + } + } + pc, err := webrtc.NewAPI( + webrtc.WithMediaEngine(peer.mediaEngine), + webrtc.WithSettingEngine(*peer.settingEngine), + webrtc.WithInterceptorRegistry(peer.interceptorRegistry), + ).NewPeerConnection(webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + }) + if err != nil { + return nil, err + } + + pc.OnNegotiationNeeded(peer.onNegotiationNeeded) + pc.OnSignalingStateChange(peer.onSignalingStateChange) + pc.OnICECandidate(peer.onICECandidate) + pc.OnICEGatheringStateChange(peer.onICEGatheringStateChange) + pc.OnICEConnectionStateChange(peer.onICEConnectionStateChange) + pc.OnConnectionStateChange(peer.onConnectionStateChange) + pc.OnDataChannel(peer.onDataChannel) + pc.OnTrack(peer.onTrack) + + peer.pc = pc + + return peer, nil +} + +// Callbacks + +func (p *peer) onNegotiationNeeded() { + p.logger.Infof("negotiation needed") +} + +func (p *peer) onSignalingStateChange(s webrtc.SignalingState) { + p.logger.Infof("new signaling state: %v", s) +} + +func (p *peer) onICECandidate(c *webrtc.ICECandidate) { + p.logger.Infof("got new ICE candidate: %v", c) +} + +func (p *peer) onICEGatheringStateChange(s webrtc.ICEGatheringState) { + p.logger.Infof("new ICE gathering state: %v", s) +} + +func (p *peer) onICEConnectionStateChange(s webrtc.ICEConnectionState) { + p.logger.Infof("new ICE connection state: %v", s) +} + +func (p *peer) onConnectionStateChange(s webrtc.PeerConnectionState) { + p.logger.Infof("new connection state: %v", s) + if s == webrtc.PeerConnectionStateConnected && p.onConnected != nil { + p.onConnected() + } +} + +func (p *peer) onDataChannel(dc *webrtc.DataChannel) { + p.logger.Infof("got new data channel: id=%v, label=%v", dc.ID(), dc.Label()) +} + +func (p *peer) onTrack(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { + if p.onRemoteTrack != nil { + p.onRemoteTrack(track) + } +} + +// Signaling helpers + +func (p *peer) createOffer() (*webrtc.SessionDescription, error) { + offer, err := p.pc.CreateOffer(nil) + if err != nil { + return nil, err + } + gc := webrtc.GatheringCompletePromise(p.pc) + if err = p.pc.SetLocalDescription(offer); err != nil { + return nil, err + } + <-gc + + return p.pc.LocalDescription(), nil +} + +func (p *peer) createAnswer() (*webrtc.SessionDescription, error) { + answer, err := p.pc.CreateAnswer(nil) + if err != nil { + return nil, err + } + gc := webrtc.GatheringCompletePromise(p.pc) + if err = p.pc.SetLocalDescription(answer); err != nil { + return nil, err + } + <-gc + + return p.pc.LocalDescription(), nil +} + +func (p *peer) setRemoteDescription(description *webrtc.SessionDescription) error { + return p.pc.SetRemoteDescription(*description) +} + +// Track management + +func (p *peer) addLocalTrack() (*webrtc.TrackLocalStaticSample, error) { + track, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeH264, + ClockRate: 0, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: []webrtc.RTCPFeedback{}, + }, "video", "pion") + if err != nil { + return nil, err + } + s, err := p.pc.AddTrack(track) + if err != nil { + return nil, err + } + go p.readRTCP(s) + + return track, err +} + +func (p *peer) addRemoteTrack() error { + _, err := p.pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo) + + return err +} + +func (p *peer) readRTCP(r *webrtc.RTPSender) { + for { + _, _, err := r.ReadRTCP() + if err != nil { + return + } + } +} diff --git a/simulation/perfect_codec_test.go b/simulation/perfect_codec_test.go new file mode 100644 index 0000000..7c93fa5 --- /dev/null +++ b/simulation/perfect_codec_test.go @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js + +package simulation + +import ( + "crypto/rand" + "sync" + "time" + + "github.com/pion/logging" + "github.com/pion/webrtc/v4/pkg/media" +) + +type sampleWriter interface { + WriteSample(media.Sample) error +} + +// perfectCodec implements a simple codec that produces frames at a constant rate +// with sizes exactly matching the target bitrate. +type perfectCodec struct { + logger logging.LeveledLogger + + writer sampleWriter + + targetBitrateBps int + fps int + bitrateUpdateCh chan int + + done chan struct{} + wg sync.WaitGroup +} + +// newPerfectCodec creates a new PerfectCodec with the specified frame writer and target bitrate. +func newPerfectCodec(writer sampleWriter, targetBitrateBps int) *perfectCodec { + return &perfectCodec{ + logger: logging.NewDefaultLoggerFactory().NewLogger("perfect_codec"), + writer: writer, + targetBitrateBps: targetBitrateBps, + fps: 30, + bitrateUpdateCh: make(chan int), + done: make(chan struct{}), + } +} + +// setTargetBitrate sets the target bitrate to r bits per second. +// func (c *perfectCodec) setTargetBitrate(r int) { +// c.wg.Go(func() { +// select { +// case c.bitrateUpdateCh <- r: +// case <-c.done: +// } +// }) +// } + +// start begins the codec operation, generating frames at the configured frame rate. +func (c *perfectCodec) start() { + c.wg.Add(1) + go func() { + defer c.wg.Done() + msToNextFrame := time.Duration((1.0/float64(c.fps))*1000.0) * time.Millisecond + ticker := time.NewTicker(msToNextFrame) + for { + select { + case <-ticker.C: + size := c.targetBitrateBps / (8.0 * c.fps) + buf := make([]byte, size) + if _, err := rand.Read(buf); err != nil { + c.logger.Errorf("failed to read random bytes: %v", err) + + continue + } + if err := c.writer.WriteSample(media.Sample{ + Data: buf, + Duration: msToNextFrame, + }); err != nil { + c.logger.Errorf("failed to write sample: %v", err) + + continue + } + case nextRate := <-c.bitrateUpdateCh: + c.targetBitrateBps = nextRate + case <-c.done: + return + } + } + }() +} + +// Close stops the codec and cleans up resources. +func (c *perfectCodec) Close() error { + close(c.done) + c.wg.Wait() + + return nil +} diff --git a/simulation/simulation.go b/simulation/simulation.go new file mode 100644 index 0000000..7f17136 --- /dev/null +++ b/simulation/simulation.go @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +// Package simulation implements bandwidth estimation tests using the synctest +// package. +package simulation diff --git a/simulation/vnet_test.go b/simulation/vnet_test.go new file mode 100644 index 0000000..ec8c342 --- /dev/null +++ b/simulation/vnet_test.go @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js && go1.25 + +package simulation + +import ( + "errors" + "io" + "testing" + "testing/synctest" + "time" + + "github.com/pion/logging" + "github.com/pion/transport/v3/vnet" + "github.com/pion/webrtc/v4" + "github.com/stretchr/testify/assert" +) + +type network struct { + wan *vnet.Router + left *vnet.Net + right *vnet.Net +} + +func (n *network) Close() error { + return n.wan.Stop() +} + +func createVirtualNetwork(t *testing.T) *network { + t.Helper() + + wan, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "0.0.0.0/0", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + assert.NoError(t, err) + + leftRouter, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "10.0.1.0/24", + StaticIPs: []string{ + "10.0.1.1/10.0.1.101", + }, + LoggerFactory: logging.NewDefaultLoggerFactory(), + NATType: &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + }, + }) + assert.NoError(t, err) + err = wan.AddRouter(leftRouter) + assert.NoError(t, err) + + rightRouter, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "10.0.2.0/24", + StaticIPs: []string{ + "10.0.2.1/10.0.2.101", + }, + LoggerFactory: logging.NewDefaultLoggerFactory(), + NATType: &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + }, + }) + assert.NoError(t, err) + err = wan.AddRouter(rightRouter) + assert.NoError(t, err) + + err = wan.Start() + assert.NoError(t, err) + + leftNet, err := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{"10.0.1.101"}, + StaticIP: "", + }) + assert.NoError(t, err) + err = leftRouter.AddNet(leftNet) + assert.NoError(t, err) + + rightNet, err := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{"10.0.2.101"}, + StaticIP: "", + }) + assert.NoError(t, err) + err = rightRouter.AddNet(rightNet) + assert.NoError(t, err) + + return &network{ + wan: wan, + left: leftNet, + right: rightNet, + } +} + +func TestVnet(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + t.Helper() + + onTrack := make(chan struct{}) + connected := make(chan struct{}) + done := make(chan struct{}) + + network := createVirtualNetwork(t) + receiver, err := newPeer( + registerDefaultCodecs(), + setVNet(network.left, []string{"10.0.1.1"}), + onRemoteTrack(func(track *webrtc.TrackRemote) { + close(onTrack) + go func() { + buf := make([]byte, 1500) + for { + select { + case <-done: + return + default: + _, _, err := track.Read(buf) + if errors.Is(err, io.EOF) { + return + } + assert.NoError(t, err) + } + } + }() + }), + registerPacketLogger("receiver"), + registerCCFB(), + ) + assert.NoError(t, err) + + err = receiver.addRemoteTrack() + assert.NoError(t, err) + + sender, err := newPeer( + registerDefaultCodecs(), + onConnected(func() { close(connected) }), + setVNet(network.right, []string{"10.0.2.1"}), + registerPacketLogger("sender"), + registerRTPFB(), + ) + assert.NoError(t, err) + + track, err := sender.addLocalTrack() + assert.NoError(t, err) + + codec := newPerfectCodec(track, 1_000_000) + go func() { + <-connected + codec.start() + }() + + offer, err := sender.createOffer() + assert.NoError(t, err) + + err = receiver.setRemoteDescription(offer) + assert.NoError(t, err) + + answer, err := receiver.createAnswer() + assert.NoError(t, err) + + err = sender.setRemoteDescription(answer) + assert.NoError(t, err) + + synctest.Wait() + select { + case <-onTrack: + case <-time.After(time.Second): + assert.Fail(t, "on track not called") + } + time.Sleep(10 * time.Second) + close(done) + + err = codec.Close() + assert.NoError(t, err) + + err = sender.pc.Close() + assert.NoError(t, err) + + err = receiver.pc.Close() + assert.NoError(t, err) + + err = network.Close() + assert.NoError(t, err) + + synctest.Wait() + }) +}