Files
mediamtx/internal/protocols/webrtc/peer_connection_test.go
T

1172 lines
28 KiB
Go

package webrtc
import (
"net"
"regexp"
"sort"
"strings"
"testing"
"time"
"github.com/pion/ice/v4"
"github.com/pion/logging"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/test"
)
type nilWriter struct{}
func (nilWriter) Write(p []byte) (int, error) {
return len(p), nil
}
var webrtcNilLogger = logging.NewDefaultLeveledLoggerForScope("", 0, &nilWriter{})
func gatherCodecs(tracks []*InboundTrack) []webrtc.RTPCodecParameters {
codecs := make([]webrtc.RTPCodecParameters, len(tracks))
for i, track := range tracks {
codecs[i] = track.Codec()
}
return codecs
}
func gatherTrackIDs(tracks []*InboundTrack) []string {
ids := make([]string, len(tracks))
for i, track := range tracks {
ids[i] = track.track.ID()
}
return ids
}
func gatherTrackRIDs(tracks []*InboundTrack) []string {
rids := make([]string, len(tracks))
for i, track := range tracks {
rids[i] = track.track.RID()
}
return rids
}
func gatherTrackMIDIndexes(tracks []*InboundTrack) []int {
mids := make([]int, len(tracks))
for i, track := range tracks {
mids[i] = track.midIndex
}
return mids
}
func gatherTrackRIDIndexes(tracks []*InboundTrack) []int {
rids := make([]int, len(tracks))
for i, track := range tracks {
rids[i] = track.ridIndex
}
return rids
}
func senderHeaderExtensionID(params webrtc.RTPSendParameters, uri string) uint8 {
for _, ext := range params.HeaderExtensions {
if ext.URI == uri {
return uint8(ext.ID)
}
}
return 0
}
func TestPeerConnectionCloseImmediately(t *testing.T) {
pc := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err := pc.Start()
require.NoError(t, err)
defer pc.Close()
}
func TestPeerConnectionCloseImmediately2(t *testing.T) {
pc := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err := pc.Start()
require.NoError(t, err)
defer pc.Close()
_, err = pc.CreatePartialOffer(false)
require.NoError(t, err)
// wait for ICE candidates to be generated
time.Sleep(500 * time.Millisecond)
}
func TestPeerConnectionCandidates(t *testing.T) {
for _, ca := range []string{
"udp random",
"udp",
"tcp",
"stun",
"udp+stun",
"udp random+stun",
} {
t.Run(ca, func(t *testing.T) {
pc2, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc2.Close() //nolint:errcheck
track, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video",
"publisher",
)
require.NoError(t, err)
_, err = pc2.AddTrack(track)
require.NoError(t, err)
offer, err := pc2.CreateOffer(nil)
require.NoError(t, err)
var udpMux ice.UDPMux
if ca == "udp" || ca == "udp+stun" {
var ln net.PacketConn
ln, err = net.ListenPacket("udp", ":3454")
require.NoError(t, err)
defer ln.Close()
udpMux = webrtc.NewICEUDPMux(webrtcNilLogger, ln)
}
var tcpMux *TCPMuxWrapper
if ca == "tcp" {
var ln net.Listener
ln, err = net.Listen("tcp", ":3454")
require.NoError(t, err)
defer ln.Close()
tcpMux = &TCPMuxWrapper{
Mux: webrtc.NewICETCPMux(webrtcNilLogger, ln, 8),
Ln: ln,
}
}
pc := &PeerConnection{
LocalRandomUDP: (ca == "udp random" || ca == "udp random+stun"),
ICEUDPMux: udpMux,
ICETCPMux: tcpMux,
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{"lo"},
Log: test.NilLogger,
}
if ca == "stun" || ca == "udp+stun" || ca == "udp random+stun" {
pc.ICEServers = []webrtc.ICEServer{{
URLs: []string{"stun:stun.l.google.com:19302"},
}}
}
err = pc.Start()
require.NoError(t, err)
defer pc.Close()
answer, err := pc.CreateFullAnswer(&offer, false)
require.NoError(t, err)
n := len(regexp.MustCompile("(?m)^a=candidate:.+? udp .+? typ host").FindAllString(answer.SDP, -1))
if ca == "udp" || ca == "udp random" || ca == "udp+stun" || ca == "udp random+stun" {
require.Equal(t, 2, n)
} else {
require.Equal(t, 0, n)
}
n = len(regexp.MustCompile("(?m)^a=candidate:.+? tcp .+? typ host tcptype passive").FindAllString(answer.SDP, -1))
if ca == "tcp" {
require.Equal(t, 2, n)
} else {
require.Equal(t, 0, n)
}
n = len(regexp.MustCompile("(?m)^a=candidate:.+? udp .+? typ srflx").FindAllString(answer.SDP, -1))
if ca == "stun" || ca == "udp+stun" || ca == "udp random+stun" {
require.Equal(t, 2, n)
} else {
require.Equal(t, 0, n)
}
})
}
}
func TestPeerConnectionConnectivity(t *testing.T) {
for _, mode := range []string{
"passive udp",
"passive tcp",
"active udp",
"active udp + stun",
} {
for _, ip := range []string{
"from interfaces",
"additional hosts",
} {
// LocalRandomUDP doesn't work with AdditionalHosts
// we don't care since we are not currently using them together
if mode == "active udp" && ip == "additional hosts" {
continue
}
t.Run(mode+"_"+ip, func(t *testing.T) {
var iceServers []webrtc.ICEServer
if mode == "active udp + stun" {
iceServers = []webrtc.ICEServer{{
URLs: []string{"stun:stun.l.google.com:19302"},
}}
}
clientPC := &PeerConnection{
LocalRandomUDP: (mode == "passive udp" || mode == "active udp"),
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{"lo"},
ICEServers: iceServers,
Log: test.NilLogger,
}
err := clientPC.Start()
require.NoError(t, err)
defer clientPC.Close()
var udpMux ice.UDPMux
var tcpMux *TCPMuxWrapper
switch mode {
case "passive udp":
var ln net.PacketConn
ln, err = net.ListenPacket("udp4", ":4458")
require.NoError(t, err)
defer ln.Close()
udpMux = webrtc.NewICEUDPMux(webrtcNilLogger, ln)
case "passive tcp":
var ln net.Listener
ln, err = net.Listen("tcp4", ":4458")
require.NoError(t, err)
defer ln.Close()
tcpMux = &TCPMuxWrapper{
Mux: webrtc.NewICETCPMux(webrtcNilLogger, ln, 8),
Ln: ln,
}
}
serverPC := &PeerConnection{
LocalRandomUDP: (mode == "active udp"),
ICEUDPMux: udpMux,
ICETCPMux: tcpMux,
ICEServers: iceServers,
Publish: true,
OutboundTracks: []*OutboundTrack{{
Caps: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
},
}},
Log: test.NilLogger,
}
if ip == "from interfaces" {
serverPC.IPsFromInterfaces = true
serverPC.IPsFromInterfacesList = []string{"lo"}
} else {
serverPC.AdditionalHosts = []string{"127.0.0.1"}
}
err = serverPC.Start()
require.NoError(t, err)
defer serverPC.Close()
offer, err := clientPC.CreatePartialOffer(false)
require.NoError(t, err)
answer, err := serverPC.CreateFullAnswer(offer, false)
require.NoError(t, err)
require.Equal(t, 2, strings.Count(answer.SDP, "a=candidate:"))
err = clientPC.SetAnswer(answer)
require.NoError(t, err)
go func() {
for {
select {
case cd := <-clientPC.NewLocalCandidate():
err2 := serverPC.AddRemoteCandidate(cd)
require.NoError(t, err2)
case <-clientPC.Failed():
return
}
}
}()
err = serverPC.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
})
}
}
}
func TestPeerConnectionRead(t *testing.T) {
pub, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pub.Close() //nolint:errcheck
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video",
"publisher")
require.NoError(t, err)
videoSender, err := pub.AddTrack(videoTrack)
require.NoError(t, err)
audioTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
},
"audio",
"publisher")
require.NoError(t, err)
audioSender, err := pub.AddTrack(audioTrack)
require.NoError(t, err)
reader := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err = reader.Start()
require.NoError(t, err)
defer reader.Close()
offer, err := pub.CreateOffer(nil)
require.NoError(t, err)
err = pub.SetLocalDescription(offer)
require.NoError(t, err)
answer, err := reader.CreateFullAnswer(&offer, false)
require.NoError(t, err)
err = pub.SetRemoteDescription(*answer)
require.NoError(t, err)
err = reader.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
go func() {
time.Sleep(100 * time.Millisecond)
err2 := audioTrack.WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 111,
SequenceNumber: 1123,
Timestamp: 45343,
SSRC: 563424,
},
Payload: []byte{5, 2},
})
require.NoError(t, err2)
err2 = videoTrack.WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 111,
SequenceNumber: 1123,
Timestamp: 45343,
SSRC: 563424,
},
Payload: []byte{5, 2},
})
require.NoError(t, err2)
}()
err = reader.GatherInboundTracks(2 * time.Second)
require.NoError(t, err)
require.Equal(t, []string{"video", "audio"}, gatherTrackIDs(reader.InboundTracks()))
require.Equal(t, []int{0, 1}, gatherTrackMIDIndexes(reader.InboundTracks()))
codecs := gatherCodecs(reader.InboundTracks())
sort.Slice(codecs, func(i, j int) bool {
return codecs[i].PayloadType < codecs[j].PayloadType
})
require.Equal(t, []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
RTCPFeedback: codecs[0].RTCPFeedback,
},
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
RTCPFeedback: codecs[1].RTCPFeedback,
},
PayloadType: 111,
},
}, codecs)
reader.StartReading()
pkts, _, err := videoSender.ReadRTCP()
require.NoError(t, err)
require.Equal(t, []rtcp.Packet{
&rtcp.ReceiverReport{
SSRC: pkts[0].(*rtcp.ReceiverReport).SSRC,
Reports: []rtcp.ReceptionReport{{
SSRC: uint32(videoSender.GetParameters().Encodings[0].SSRC),
LastSequenceNumber: pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSequenceNumber,
LastSenderReport: pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSenderReport,
Delay: pkts[0].(*rtcp.ReceiverReport).Reports[0].Delay,
}},
ProfileExtensions: []byte{},
},
}, pkts)
pkts, _, err = audioSender.ReadRTCP()
require.NoError(t, err)
require.Equal(t, []rtcp.Packet{
&rtcp.ReceiverReport{
SSRC: pkts[0].(*rtcp.ReceiverReport).SSRC,
Reports: []rtcp.ReceptionReport{{
SSRC: uint32(audioSender.GetParameters().Encodings[0].SSRC),
LastSequenceNumber: pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSequenceNumber,
LastSenderReport: pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSenderReport,
Delay: pkts[0].(*rtcp.ReceiverReport).Reports[0].Delay,
}},
ProfileExtensions: []byte{},
},
}, pkts)
}
func TestPeerConnectionReadSimulcast(t *testing.T) {
pub, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pub.Close() //nolint:errcheck
videoTrackL, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video", "publisher",
webrtc.WithRTPStreamID("l"),
)
require.NoError(t, err)
videoTrackM, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video", "publisher",
webrtc.WithRTPStreamID("m"),
)
require.NoError(t, err)
videoTrackH, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video", "publisher",
webrtc.WithRTPStreamID("h"),
)
require.NoError(t, err)
transceiver, err := pub.AddTransceiverFromTrack(videoTrackL, webrtc.RTPTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
SendEncodings: []webrtc.RTPEncodingParameters{
{RTPCodingParameters: webrtc.RTPCodingParameters{RID: "l"}},
{RTPCodingParameters: webrtc.RTPCodingParameters{RID: "m"}},
{RTPCodingParameters: webrtc.RTPCodingParameters{RID: "h"}},
},
})
require.NoError(t, err)
err = transceiver.Sender().AddEncoding(videoTrackM)
require.NoError(t, err)
err = transceiver.Sender().AddEncoding(videoTrackH)
require.NoError(t, err)
reader := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err = reader.Start()
require.NoError(t, err)
defer reader.Close()
offer, err := pub.CreateOffer(nil)
require.NoError(t, err)
err = pub.SetLocalDescription(offer)
require.NoError(t, err)
answer, err := reader.CreateFullAnswer(&offer, false)
require.NoError(t, err)
err = pub.SetRemoteDescription(*answer)
require.NoError(t, err)
err = reader.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
params := transceiver.Sender().GetParameters()
midExtID := senderHeaderExtensionID(params, sdp.SDESMidURI)
ridExtID := senderHeaderExtensionID(params, sdp.SDESRTPStreamIDURI)
require.NotZero(t, midExtID)
require.NotZero(t, ridExtID)
mid := transceiver.Mid()
go func() {
time.Sleep(200 * time.Millisecond)
layers := []struct {
track *webrtc.TrackLocalStaticRTP
rid string
ssrc uint32
seqNum uint16
}{
{videoTrackL, "l", 100001, 1000},
{videoTrackM, "m", 100002, 2000},
{videoTrackH, "h", 100003, 3000},
}
for i, layer := range layers {
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: layer.seqNum,
Timestamp: 45343,
SSRC: layer.ssrc,
},
Payload: []byte{5, 2},
}
pkt.ExtensionProfile = 0xBEDE
require.NoError(t, pkt.SetExtension(midExtID, []byte(mid)))
require.NoError(t, pkt.SetExtension(ridExtID, []byte(layer.rid)))
err2 := layer.track.WriteRTP(pkt)
if err2 != nil {
return
}
layers[i].seqNum++
}
}()
err = reader.GatherInboundTracks(5 * time.Second)
require.NoError(t, err)
tracks := reader.InboundTracks()
codecs := gatherCodecs(tracks)
require.Equal(t, 3, len(codecs))
for _, codec := range codecs {
require.Equal(t, webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
RTCPFeedback: codec.RTCPFeedback,
}, codec.RTPCodecCapability)
}
require.Equal(t, []string{"l", "m", "h"}, gatherTrackRIDs(tracks))
require.Equal(t, []int{0, 1, 2}, gatherTrackRIDIndexes(tracks))
}
func TestPeerConnectionStripIncomingTWCC(t *testing.T) {
pub, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pub.Close() //nolint:errcheck
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video", "publisher",
webrtc.WithRTPStreamID("l"),
)
require.NoError(t, err)
videoSender, err := pub.AddTrack(videoTrack)
require.NoError(t, err)
reader := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err = reader.Start()
require.NoError(t, err)
defer reader.Close()
offer, err := pub.CreateOffer(nil)
require.NoError(t, err)
err = pub.SetLocalDescription(offer)
require.NoError(t, err)
answer, err := reader.CreateFullAnswer(&offer, false)
require.NoError(t, err)
err = pub.SetRemoteDescription(*answer)
require.NoError(t, err)
err = reader.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
params := videoSender.GetParameters()
twccExtID := senderHeaderExtensionID(params, twccExtensionURI)
require.NotZero(t, twccExtID)
go func() {
time.Sleep(200 * time.Millisecond)
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 55421,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{5, 2},
}
pkt.ExtensionProfile = 0xBEDE
require.NoError(t, pkt.SetExtension(twccExtID, []byte{0x12, 0x34}))
err2 := videoTrack.WriteRTP(pkt)
if err2 != nil {
return
}
}()
err = reader.GatherInboundTracks(5 * time.Second)
require.NoError(t, err)
tracks := reader.InboundTracks()
require.Len(t, tracks, 1)
done := make(chan struct{})
tracks[0].OnPacketRTP = func(p *rtp.Packet) {
require.False(t, p.Extension)
require.Empty(t, p.Extensions)
close(done)
}
reader.StartReading()
<-done
}
func TestPeerConnectionPublishRead(t *testing.T) {
pc1 := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err := pc1.Start()
require.NoError(t, err)
defer pc1.Close()
pc2 := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: true,
OutboundTracks: []*OutboundTrack{
{
Caps: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
},
},
{
Caps: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
},
},
},
Log: test.NilLogger,
}
err = pc2.Start()
require.NoError(t, err)
defer pc2.Close()
offer, err := pc1.CreatePartialOffer(false)
require.NoError(t, err)
answer, err := pc2.CreateFullAnswer(offer, false)
require.NoError(t, err)
err = pc1.SetAnswer(answer)
require.NoError(t, err)
err = pc1.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
err = pc2.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
for _, track := range pc2.OutboundTracks {
err = track.WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 111,
SequenceNumber: 1123,
Timestamp: 45343,
SSRC: 563424,
},
Payload: []byte{5, 2},
})
require.NoError(t, err)
}
err = pc1.GatherInboundTracks(2 * time.Second)
require.NoError(t, err)
codecs := gatherCodecs(pc1.InboundTracks())
sort.Slice(codecs, func(i, j int) bool {
return codecs[i].PayloadType < codecs[j].PayloadType
})
require.Equal(t, []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
RTCPFeedback: codecs[0].RTCPFeedback,
},
PayloadType: 105,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
RTCPFeedback: codecs[1].RTCPFeedback,
},
PayloadType: 111,
},
}, codecs)
}
// test that an audio codec is present regardless of the fact that an audio track is.
func TestPeerConnectionFallbackCodecs(t *testing.T) {
pc1 := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err := pc1.Start()
require.NoError(t, err)
defer pc1.Close()
pc2 := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: true,
OutboundTracks: []*OutboundTrack{{
Caps: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
},
}},
Log: test.NilLogger,
}
err = pc2.Start()
require.NoError(t, err)
defer pc2.Close()
offer, err := pc1.CreatePartialOffer(false)
require.NoError(t, err)
answer, err := pc2.CreateFullAnswer(offer, false)
require.NoError(t, err)
var s sdp.SessionDescription
err = s.Unmarshal([]byte(answer.SDP))
require.NoError(t, err)
require.Equal(t, []*sdp.MediaDescription{
{
MediaName: sdp.MediaName{
Media: "video",
Port: sdp.RangedPort{Value: 9},
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
Formats: []string{"97"},
},
ConnectionInformation: s.MediaDescriptions[0].ConnectionInformation,
Attributes: s.MediaDescriptions[0].Attributes,
},
{
MediaName: sdp.MediaName{
Media: "audio",
Port: sdp.RangedPort{Value: 9},
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
Formats: []string{"0"},
},
ConnectionInformation: s.MediaDescriptions[1].ConnectionInformation,
Attributes: s.MediaDescriptions[1].Attributes,
},
}, s.MediaDescriptions)
}
func TestPeerConnectionPublishDataChannel(t *testing.T) {
pc1, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc1.Close() //nolint:errcheck
_, err = pc1.CreateDataChannel("", nil)
require.NoError(t, err)
dataChanOpened := make(chan struct{})
dataReceived := make(chan struct{})
pc1.OnDataChannel(func(dc *webrtc.DataChannel) {
dc.OnOpen(func() {
close(dataChanOpened)
})
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
require.Equal(t, []byte("test data"), msg.Data)
close(dataReceived)
})
})
offer, err := pc1.CreateOffer(nil)
require.NoError(t, err)
err = pc1.SetLocalDescription(offer)
require.NoError(t, err)
pc2 := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: true,
OutboundDataChannels: []*OutboundDataChannel{
{
Label: "test-channel",
},
},
Log: test.NilLogger,
}
err = pc2.Start()
require.NoError(t, err)
defer pc2.Close()
outboundDataChanOpened := make(chan struct{})
pc2.OutboundDataChannels[0].dataChan.OnOpen(func() {
close(outboundDataChanOpened)
})
answer, err := pc2.CreateFullAnswer(&offer, false)
require.NoError(t, err)
err = pc1.SetRemoteDescription(*answer)
require.NoError(t, err)
err = pc2.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
<-outboundDataChanOpened
<-dataChanOpened
err = pc2.OutboundDataChannels[0].dataChan.Send([]byte("test data"))
require.NoError(t, err)
<-dataReceived
}
func TestPeerConnectionAdditionalHostsUnresolvable(t *testing.T) {
// A host in AdditionalHosts that can't be resolved server-side - for
// instance air-gapped networks without DNS, or split-horizon / overlay
// DNS names that only resolve on the client - must be skipped instead of
// aborting the session; the other (valid) entries must still produce
// candidates.
clientPC := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{"lo"},
Log: test.NilLogger,
}
err := clientPC.Start()
require.NoError(t, err)
defer clientPC.Close()
ln, err := net.ListenPacket("udp4", ":0")
require.NoError(t, err)
defer ln.Close()
udpMux := webrtc.NewICEUDPMux(webrtcNilLogger, ln)
serverPC := &PeerConnection{
ICEUDPMux: udpMux,
AdditionalHosts: []string{"127.0.0.1", "unresolvable.invalid"},
Publish: true,
OutboundTracks: []*OutboundTrack{{
Caps: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
},
}},
Log: test.NilLogger,
}
err = serverPC.Start()
require.NoError(t, err)
defer serverPC.Close()
offer, err := clientPC.CreatePartialOffer(false)
require.NoError(t, err)
answer, err := serverPC.CreateFullAnswer(offer, false)
require.NoError(t, err)
require.Contains(t, answer.SDP, "127.0.0.1")
require.NotContains(t, answer.SDP, "unresolvable.invalid")
}
func TestPeerConnectionRecomputeSequenceNumber(t *testing.T) {
for _, ca := range []struct {
name string
packets []*rtp.Packet
expectedCount int
check func(t *testing.T, seqNums []uint16)
}{
{
name: "skip empty packets",
packets: []*rtp.Packet{
{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1000,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{5, 2},
},
{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1001,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{},
},
{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1002,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{5, 2},
},
{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1003,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{},
},
{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1004,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{5, 2},
},
},
expectedCount: 3,
check: func(t *testing.T, seqNums []uint16) {
require.Len(t, seqNums, 3)
require.Equal(t, seqNums[1], seqNums[0]+1)
require.Equal(t, seqNums[2], seqNums[1]+1)
},
},
{
name: "preserve real gaps",
packets: func() []*rtp.Packet {
packets := []*rtp.Packet{{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1000,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{5, 2},
}}
for i := uint16(1002); i <= 1065; i++ {
packets = append(packets, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: i,
Timestamp: 45343,
SSRC: 124123,
},
Payload: []byte{5, 2},
})
}
return packets
}(),
expectedCount: 2,
check: func(t *testing.T, seqNums []uint16) {
require.Len(t, seqNums, 2)
require.Equal(t, seqNums[1], seqNums[0]+2)
},
},
} {
t.Run(ca.name, func(t *testing.T) {
pub, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pub.Close() //nolint:errcheck
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
"video", "publisher",
)
require.NoError(t, err)
_, err = pub.AddTrack(videoTrack)
require.NoError(t, err)
reader := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err = reader.Start()
require.NoError(t, err)
defer reader.Close()
offer, err := pub.CreateOffer(nil)
require.NoError(t, err)
err = pub.SetLocalDescription(offer)
require.NoError(t, err)
answer, err := reader.CreateFullAnswer(&offer, false)
require.NoError(t, err)
err = pub.SetRemoteDescription(*answer)
require.NoError(t, err)
err = reader.WaitUntilConnected(10 * time.Second)
require.NoError(t, err)
go func() {
time.Sleep(200 * time.Millisecond)
for _, pkt := range ca.packets {
err2 := videoTrack.WriteRTP(pkt)
if err2 != nil {
return
}
}
}()
err = reader.GatherInboundTracks(5 * time.Second)
require.NoError(t, err)
tracks := reader.InboundTracks()
require.Len(t, tracks, 1)
received := make(chan []uint16, 1)
var seqNums []uint16
tracks[0].OnPacketRTP = func(p *rtp.Packet) {
require.NotEmpty(t, p.Payload)
seqNums = append(seqNums, p.SequenceNumber)
if len(seqNums) == ca.expectedCount {
received <- append([]uint16(nil), seqNums...)
}
}
reader.StartReading()
select {
case seqs := <-received:
ca.check(t, seqs)
case <-time.After(2 * time.Second):
require.FailNow(t, "timeout waiting for packets")
}
})
}
}