snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
This commit is contained in:
546
datachannel_test.go
Normal file
546
datachannel_test.go
Normal file
@@ -0,0 +1,546 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/transport/v4/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// expectedLabel represents the label of the data channel we are trying to test.
|
||||
// Some other channels may have been created during initialization (in the Wasm
|
||||
// bindings this is a requirement).
|
||||
const expectedLabel = "data"
|
||||
|
||||
func closePairNow(tb testing.TB, pc1, pc2 io.Closer) {
|
||||
tb.Helper()
|
||||
|
||||
var fail bool
|
||||
if err := pc1.Close(); err != nil {
|
||||
tb.Errorf("Failed to close PeerConnection: %v", err)
|
||||
fail = true
|
||||
}
|
||||
if err := pc2.Close(); err != nil {
|
||||
tb.Errorf("Failed to close PeerConnection: %v", err)
|
||||
fail = true
|
||||
}
|
||||
if fail {
|
||||
tb.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func closePair(t *testing.T, pc1, pc2 io.Closer, done <-chan bool) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
assert.Fail(t, "closePair timed out waiting for done signal")
|
||||
case <-done:
|
||||
closePairNow(t, pc1, pc2)
|
||||
}
|
||||
}
|
||||
|
||||
func setUpDataChannelParametersTest(
|
||||
t *testing.T,
|
||||
options *DataChannelInit,
|
||||
) (*PeerConnection, *PeerConnection, *DataChannel, chan bool) {
|
||||
t.Helper()
|
||||
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
done := make(chan bool)
|
||||
|
||||
dc, err := offerPC.CreateDataChannel(expectedLabel, options)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return offerPC, answerPC, dc, done
|
||||
}
|
||||
|
||||
func closeReliabilityParamTest(t *testing.T, pc1, pc2 *PeerConnection, done chan bool) {
|
||||
t.Helper()
|
||||
|
||||
err := signalPair(pc1, pc2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
closePair(t, pc1, pc2, done)
|
||||
}
|
||||
|
||||
func BenchmarkDataChannelSend2(b *testing.B) { benchmarkDataChannelSend(b, 2) }
|
||||
func BenchmarkDataChannelSend4(b *testing.B) { benchmarkDataChannelSend(b, 4) }
|
||||
func BenchmarkDataChannelSend8(b *testing.B) { benchmarkDataChannelSend(b, 8) }
|
||||
func BenchmarkDataChannelSend16(b *testing.B) { benchmarkDataChannelSend(b, 16) }
|
||||
func BenchmarkDataChannelSend32(b *testing.B) { benchmarkDataChannelSend(b, 32) }
|
||||
|
||||
// See https://github.com/pion/webrtc/issues/1516
|
||||
func benchmarkDataChannelSend(b *testing.B, numChannels int) { //nolint:cyclop
|
||||
b.Helper()
|
||||
|
||||
offerPC, answerPC, err := newPair()
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create a PC pair for testing")
|
||||
}
|
||||
defer closePairNow(b, offerPC, answerPC)
|
||||
|
||||
type openSignal struct {
|
||||
local chan struct{}
|
||||
remote chan struct{}
|
||||
}
|
||||
|
||||
open := make(map[string]openSignal, numChannels)
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
signal, ok := open[d.Label()]
|
||||
if !ok {
|
||||
// Ignore anything unknown channel label.
|
||||
return
|
||||
}
|
||||
d.OnOpen(func() { close(signal.remote) })
|
||||
})
|
||||
|
||||
dataChannels := make([]*DataChannel, 0, numChannels)
|
||||
for i := range numChannels {
|
||||
label := fmt.Sprintf("dc-%d", i)
|
||||
signal := openSignal{
|
||||
local: make(chan struct{}),
|
||||
remote: make(chan struct{}),
|
||||
}
|
||||
open[label] = signal
|
||||
|
||||
var dc *DataChannel
|
||||
dc, err = offerPC.CreateDataChannel(label, nil)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create data channel %q: %v", label, err)
|
||||
}
|
||||
|
||||
dc.OnOpen(func() { close(signal.local) })
|
||||
dataChannels = append(dataChannels, dc)
|
||||
}
|
||||
|
||||
err = signalPairWithOptions(offerPC, answerPC, withDisableInitialDataChannel(true))
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to signal PeerConnection pair: %v", err)
|
||||
}
|
||||
|
||||
for label, signal := range open {
|
||||
waitForBenchmarkDataChannelOpen(b, label, "local", signal.local)
|
||||
waitForBenchmarkDataChannelOpen(b, label, "remote", signal.remote)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
errCh := make(chan error, numChannels)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(dataChannels))
|
||||
for _, dc := range dataChannels {
|
||||
go func(dc *DataChannel) {
|
||||
defer wg.Done()
|
||||
|
||||
for n := 0; n < b.N/numChannels; n++ {
|
||||
if dc.ReadyState() != DataChannelStateOpen {
|
||||
return
|
||||
}
|
||||
|
||||
if err := dc.SendText("Ping"); err != nil {
|
||||
if dc.ReadyState() != DataChannelStateOpen ||
|
||||
strings.Contains(err.Error(), "non-established state") {
|
||||
return
|
||||
}
|
||||
|
||||
errCh <- fmt.Errorf("unexpected error sending data (label=%q): %w", dc.Label(), err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}(dc)
|
||||
}
|
||||
wg.Wait()
|
||||
b.StopTimer()
|
||||
close(errCh)
|
||||
|
||||
for err := range errCh {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForBenchmarkDataChannelOpen(b *testing.B, label, side string, open <-chan struct{}) {
|
||||
b.Helper()
|
||||
|
||||
select {
|
||||
case <-open:
|
||||
case <-time.After(10 * time.Second):
|
||||
b.Fatalf("Timed out waiting for %s data channel %q to open", side, label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataChannel_Open(t *testing.T) {
|
||||
const openOnceChannelCapacity = 2
|
||||
|
||||
t.Run("handler should be called once", func(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
|
||||
done := make(chan bool)
|
||||
openCalls := make(chan bool, openOnceChannelCapacity)
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
if d.Label() != expectedLabel {
|
||||
return
|
||||
}
|
||||
d.OnOpen(func() {
|
||||
openCalls <- true
|
||||
})
|
||||
d.OnMessage(func(DataChannelMessage) {
|
||||
go func() {
|
||||
// Wait a little bit to ensure all messages are processed.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
done <- true
|
||||
}()
|
||||
})
|
||||
})
|
||||
|
||||
dc, err := offerPC.CreateDataChannel(expectedLabel, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dc.OnOpen(func() {
|
||||
assert.NoError(t, dc.SendText("Ping"), "Failed to send string on data channel")
|
||||
})
|
||||
|
||||
assert.NoError(t, signalPair(offerPC, answerPC))
|
||||
|
||||
closePair(t, offerPC, answerPC, done)
|
||||
|
||||
assert.Len(t, openCalls, 1)
|
||||
})
|
||||
|
||||
t.Run("handler should be called once when already negotiated", func(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
|
||||
done := make(chan bool)
|
||||
answerOpenCalls := make(chan bool, openOnceChannelCapacity)
|
||||
offerOpenCalls := make(chan bool, openOnceChannelCapacity)
|
||||
|
||||
negotiated := true
|
||||
ordered := true
|
||||
dataChannelID := uint16(0)
|
||||
|
||||
answerDC, err := answerPC.CreateDataChannel(expectedLabel, &DataChannelInit{
|
||||
ID: &dataChannelID,
|
||||
Negotiated: &negotiated,
|
||||
Ordered: &ordered,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
offerDC, err := offerPC.CreateDataChannel(expectedLabel, &DataChannelInit{
|
||||
ID: &dataChannelID,
|
||||
Negotiated: &negotiated,
|
||||
Ordered: &ordered,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
answerDC.OnMessage(func(DataChannelMessage) {
|
||||
go func() {
|
||||
// Wait a little bit to ensure all messages are processed.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
done <- true
|
||||
}()
|
||||
})
|
||||
answerDC.OnOpen(func() {
|
||||
answerOpenCalls <- true
|
||||
})
|
||||
|
||||
offerDC.OnOpen(func() {
|
||||
offerOpenCalls <- true
|
||||
assert.NoError(t, offerDC.SendText("Ping"), "Failed to send string on data channel")
|
||||
})
|
||||
|
||||
assert.NoError(t, signalPair(offerPC, answerPC))
|
||||
|
||||
closePair(t, offerPC, answerPC, done)
|
||||
|
||||
assert.Len(t, answerOpenCalls, 1)
|
||||
assert.Len(t, offerOpenCalls, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDataChannel_Send(t *testing.T) { //nolint:cyclop
|
||||
t.Run("before signaling", func(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
// Make sure this is the data channel we were looking for. (Not the one
|
||||
// created in signalPair).
|
||||
if d.Label() != expectedLabel {
|
||||
return
|
||||
}
|
||||
d.OnMessage(func(DataChannelMessage) {
|
||||
assert.NoError(t, d.Send([]byte("Pong")), "Failed to send string on data channel")
|
||||
})
|
||||
assert.True(t, d.Ordered(), "Ordered should be set to true")
|
||||
})
|
||||
|
||||
dc, err := offerPC.CreateDataChannel(expectedLabel, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, dc.Ordered(), "Ordered should be set to true")
|
||||
|
||||
dc.OnOpen(func() {
|
||||
assert.NoError(t, dc.SendText("Ping"), "Failed to send string on data channel")
|
||||
})
|
||||
dc.OnMessage(func(DataChannelMessage) {
|
||||
done <- true
|
||||
})
|
||||
|
||||
err = signalPair(offerPC, answerPC)
|
||||
assert.NoError(t, err)
|
||||
|
||||
closePair(t, offerPC, answerPC, done)
|
||||
})
|
||||
|
||||
t.Run("after connected", func(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
// Make sure this is the data channel we were looking for. (Not the one
|
||||
// created in signalPair).
|
||||
if d.Label() != expectedLabel {
|
||||
return
|
||||
}
|
||||
d.OnMessage(func(DataChannelMessage) {
|
||||
assert.NoError(t, d.Send([]byte("Pong")), "Failed to send string on data channel")
|
||||
})
|
||||
assert.True(t, d.Ordered(), "Ordered should be set to true")
|
||||
})
|
||||
|
||||
once := &sync.Once{}
|
||||
offerPC.OnICEConnectionStateChange(func(state ICEConnectionState) {
|
||||
if state == ICEConnectionStateConnected || state == ICEConnectionStateCompleted {
|
||||
// wasm fires completed state multiple times
|
||||
once.Do(func() {
|
||||
dc, createErr := offerPC.CreateDataChannel(expectedLabel, nil)
|
||||
assert.NoError(t, createErr)
|
||||
|
||||
assert.True(t, dc.Ordered(), "Ordered should be set to true")
|
||||
|
||||
dc.OnMessage(func(DataChannelMessage) {
|
||||
done <- true
|
||||
})
|
||||
|
||||
if e := dc.SendText("Ping"); e != nil {
|
||||
// wasm binding doesn't fire OnOpen (we probably already missed it)
|
||||
dc.OnOpen(func() {
|
||||
assert.NoError(t, dc.SendText("Ping"), "Failed to send string on data channel")
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
err = signalPair(offerPC, answerPC)
|
||||
assert.NoError(t, err)
|
||||
|
||||
closePair(t, offerPC, answerPC, done)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDataChannel_Close(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
t.Run("Close after PeerConnection Closed", func(t *testing.T) {
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dc, err := offerPC.CreateDataChannel(expectedLabel, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
closePairNow(t, offerPC, answerPC)
|
||||
assert.NoError(t, dc.Close())
|
||||
})
|
||||
|
||||
t.Run("Close before connected", func(t *testing.T) {
|
||||
offerPC, answerPC, err := newPair()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dc, err := offerPC.CreateDataChannel(expectedLabel, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, dc.Close())
|
||||
closePairNow(t, offerPC, answerPC)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDataChannelParameters(t *testing.T) { //nolint:cyclop
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
t.Run("MaxPacketLifeTime exchange", func(t *testing.T) {
|
||||
ordered := true
|
||||
maxPacketLifeTime := uint16(3)
|
||||
options := &DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
MaxPacketLifeTime: &maxPacketLifeTime,
|
||||
}
|
||||
|
||||
offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options)
|
||||
|
||||
// Check if parameters are correctly set
|
||||
assert.Equal(t, dc.Ordered(), ordered, "Ordered should be same value as set in DataChannelInit")
|
||||
if assert.NotNil(t, dc.MaxPacketLifeTime(), "should not be nil") {
|
||||
assert.Equal(t, maxPacketLifeTime, *dc.MaxPacketLifeTime(), "should match")
|
||||
}
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
if d.Label() != expectedLabel {
|
||||
return
|
||||
}
|
||||
// Check if parameters are correctly set
|
||||
assert.Equal(t, d.Ordered(), ordered, "Ordered should be same value as set in DataChannelInit")
|
||||
if assert.NotNil(t, d.MaxPacketLifeTime(), "should not be nil") {
|
||||
assert.Equal(t, maxPacketLifeTime, *d.MaxPacketLifeTime(), "should match")
|
||||
}
|
||||
done <- true
|
||||
})
|
||||
|
||||
closeReliabilityParamTest(t, offerPC, answerPC, done)
|
||||
})
|
||||
|
||||
t.Run("MaxRetransmits exchange", func(t *testing.T) {
|
||||
ordered := false
|
||||
maxRetransmits := uint16(3000)
|
||||
options := &DataChannelInit{
|
||||
Ordered: &ordered,
|
||||
MaxRetransmits: &maxRetransmits,
|
||||
}
|
||||
|
||||
offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options)
|
||||
|
||||
// Check if parameters are correctly set
|
||||
assert.False(t, dc.Ordered(), "Ordered should be set to false")
|
||||
if assert.NotNil(t, dc.MaxRetransmits(), "should not be nil") {
|
||||
assert.Equal(t, maxRetransmits, *dc.MaxRetransmits(), "should match")
|
||||
}
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
// Make sure this is the data channel we were looking for. (Not the one
|
||||
// created in signalPair).
|
||||
if d.Label() != expectedLabel {
|
||||
return
|
||||
}
|
||||
// Check if parameters are correctly set
|
||||
assert.False(t, d.Ordered(), "Ordered should be set to false")
|
||||
if assert.NotNil(t, d.MaxRetransmits(), "should not be nil") {
|
||||
assert.Equal(t, maxRetransmits, *d.MaxRetransmits(), "should match")
|
||||
}
|
||||
done <- true
|
||||
})
|
||||
|
||||
closeReliabilityParamTest(t, offerPC, answerPC, done)
|
||||
})
|
||||
|
||||
t.Run("Protocol exchange", func(t *testing.T) {
|
||||
protocol := "json"
|
||||
options := &DataChannelInit{
|
||||
Protocol: &protocol,
|
||||
}
|
||||
|
||||
offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options)
|
||||
|
||||
// Check if parameters are correctly set
|
||||
assert.Equal(t, protocol, dc.Protocol(), "Protocol should match DataChannelInit")
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
// Make sure this is the data channel we were looking for. (Not the one
|
||||
// created in signalPair).
|
||||
if d.Label() != expectedLabel {
|
||||
return
|
||||
}
|
||||
// Check if parameters are correctly set
|
||||
assert.Equal(t, protocol, d.Protocol(), "Protocol should match what channel creator declared")
|
||||
done <- true
|
||||
})
|
||||
|
||||
closeReliabilityParamTest(t, offerPC, answerPC, done)
|
||||
})
|
||||
|
||||
t.Run("Negotiated exchange", func(t *testing.T) {
|
||||
const expectedMessage = "Hello World"
|
||||
|
||||
negotiated := true
|
||||
var id uint16 = 500
|
||||
options := &DataChannelInit{
|
||||
Negotiated: &negotiated,
|
||||
ID: &id,
|
||||
}
|
||||
|
||||
offerPC, answerPC, offerDatachannel, done := setUpDataChannelParametersTest(t, options)
|
||||
answerDatachannel, err := answerPC.CreateDataChannel(expectedLabel, options)
|
||||
assert.NoError(t, err)
|
||||
|
||||
answerPC.OnDataChannel(func(d *DataChannel) {
|
||||
// Ignore our default channel, exists to force ICE candidates. See signalPair for more info
|
||||
assert.Equal(t, "initial_data_channel", d.Label(), "OnDataChannel must not be fired when negotiated == true")
|
||||
})
|
||||
offerPC.OnDataChannel(func(*DataChannel) {
|
||||
assert.Fail(t, "OnDataChannel must not be fired when negotiated == true")
|
||||
})
|
||||
|
||||
seenAnswerMessage := &atomic.Bool{}
|
||||
seenOfferMessage := &atomic.Bool{}
|
||||
|
||||
answerDatachannel.OnMessage(func(msg DataChannelMessage) {
|
||||
if msg.IsString && string(msg.Data) == expectedMessage {
|
||||
seenAnswerMessage.Store(true)
|
||||
}
|
||||
})
|
||||
|
||||
offerDatachannel.OnMessage(func(msg DataChannelMessage) {
|
||||
if msg.IsString && string(msg.Data) == expectedMessage {
|
||||
seenOfferMessage.Store(true)
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
for seenAnswerMessage.Load() && seenOfferMessage.Load() {
|
||||
if offerDatachannel.ReadyState() == DataChannelStateOpen {
|
||||
assert.NoError(t, offerDatachannel.SendText(expectedMessage))
|
||||
}
|
||||
if answerDatachannel.ReadyState() == DataChannelStateOpen {
|
||||
assert.NoError(t, answerDatachannel.SendText(expectedMessage))
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
closeReliabilityParamTest(t, offerPC, answerPC, done)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user