Files
webrtc/peerconnection_test.go
Nils Ohlmeier 2fd3640fa3 Only collect single fingerprints/ICE credentials
The way currently DTLS fingerprints and ICE credentials
are picked is causing interop issues as described in #2621

Peers which don't use Bundle can use different fingerprints
and credentials in each media section. Even though is
not (yet) supported by Pion, receiving an SDP offer from
such a peer is valid.

Additionally if Bundle is being used the group attribute
determines which media section is the master bundle section,
which establishes the transport. Currently Pion always
just uses the first credentials/fingerprint it can find
in the SDP, which results in not spec compliant behavior.

This PR attempts to fix the above issues and make
Pion more spec compliant and interoperable.

Fixes #2621
2024-11-13 23:44:00 -05:00

792 lines
21 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package webrtc
import (
"reflect"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/pion/sdp/v3"
"github.com/pion/transport/v3/test"
"github.com/pion/webrtc/v4/pkg/rtcerr"
"github.com/stretchr/testify/assert"
)
// newPair creates two new peer connections (an offerer and an answerer)
// *without* using an api (i.e. using the default settings).
func newPair() (pcOffer *PeerConnection, pcAnswer *PeerConnection, err error) {
pca, err := NewPeerConnection(Configuration{})
if err != nil {
return nil, nil, err
}
pcb, err := NewPeerConnection(Configuration{})
if err != nil {
return nil, nil, err
}
return pca, pcb, nil
}
func signalPairWithModification(pcOffer *PeerConnection, pcAnswer *PeerConnection, modificationFunc func(string) string) error {
// Note(albrow): We need to create a data channel in order to trigger ICE
// candidate gathering in the background for the JavaScript/Wasm bindings. If
// we don't do this, the complete offer including ICE candidates will never be
// generated.
if _, err := pcOffer.CreateDataChannel("initial_data_channel", nil); err != nil {
return err
}
offer, err := pcOffer.CreateOffer(nil)
if err != nil {
return err
}
offerGatheringComplete := GatheringCompletePromise(pcOffer)
if err = pcOffer.SetLocalDescription(offer); err != nil {
return err
}
<-offerGatheringComplete
offer.SDP = modificationFunc(pcOffer.LocalDescription().SDP)
if err = pcAnswer.SetRemoteDescription(offer); err != nil {
return err
}
answer, err := pcAnswer.CreateAnswer(nil)
if err != nil {
return err
}
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
if err = pcAnswer.SetLocalDescription(answer); err != nil {
return err
}
<-answerGatheringComplete
return pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())
}
func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
return signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { return sessionDescription })
}
func offerMediaHasDirection(offer SessionDescription, kind RTPCodecType, direction RTPTransceiverDirection) bool {
parsed := &sdp.SessionDescription{}
if err := parsed.Unmarshal([]byte(offer.SDP)); err != nil {
return false
}
for _, media := range parsed.MediaDescriptions {
if media.MediaName.Media == kind.String() {
_, exists := media.Attribute(direction.String())
return exists
}
}
return false
}
func untilConnectionState(state PeerConnectionState, peers ...*PeerConnection) *sync.WaitGroup {
var triggered sync.WaitGroup
triggered.Add(len(peers))
for _, p := range peers {
var done atomic.Value
done.Store(false)
hdlr := func(p PeerConnectionState) {
if val, ok := done.Load().(bool); ok && (!val && p == state) {
done.Store(true)
triggered.Done()
}
}
p.OnConnectionStateChange(hdlr)
}
return &triggered
}
func TestNew(t *testing.T) {
pc, err := NewPeerConnection(Configuration{
ICEServers: []ICEServer{
{
URLs: []string{
"stun:stun.l.google.com:19302",
},
Username: "unittest",
},
},
ICETransportPolicy: ICETransportPolicyRelay,
BundlePolicy: BundlePolicyMaxCompat,
RTCPMuxPolicy: RTCPMuxPolicyNegotiate,
PeerIdentity: "unittest",
ICECandidatePoolSize: 5,
})
assert.NoError(t, err)
assert.NotNil(t, pc)
assert.NoError(t, pc.Close())
}
func TestPeerConnection_SetConfiguration(t *testing.T) {
// Note: These tests don't include ICEServer.Credential,
// ICEServer.CredentialType, or Certificates because those are not supported
// in the WASM bindings.
for _, test := range []struct {
name string
init func() (*PeerConnection, error)
config Configuration
wantErr error
}{
{
name: "valid",
init: func() (*PeerConnection, error) {
pc, err := NewPeerConnection(Configuration{
ICECandidatePoolSize: 5,
})
if err != nil {
return pc, err
}
err = pc.SetConfiguration(Configuration{
ICEServers: []ICEServer{
{
URLs: []string{
"stun:stun.l.google.com:19302",
},
Username: "unittest",
},
},
ICETransportPolicy: ICETransportPolicyAll,
BundlePolicy: BundlePolicyBalanced,
RTCPMuxPolicy: RTCPMuxPolicyRequire,
ICECandidatePoolSize: 5,
})
if err != nil {
return pc, err
}
return pc, nil
},
config: Configuration{},
wantErr: nil,
},
{
name: "closed connection",
init: func() (*PeerConnection, error) {
pc, err := NewPeerConnection(Configuration{})
assert.Nil(t, err)
err = pc.Close()
assert.Nil(t, err)
return pc, err
},
config: Configuration{},
wantErr: &rtcerr.InvalidStateError{Err: ErrConnectionClosed},
},
{
name: "update PeerIdentity",
init: func() (*PeerConnection, error) {
return NewPeerConnection(Configuration{})
},
config: Configuration{
PeerIdentity: "unittest",
},
wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity},
},
{
name: "update BundlePolicy",
init: func() (*PeerConnection, error) {
return NewPeerConnection(Configuration{})
},
config: Configuration{
BundlePolicy: BundlePolicyMaxCompat,
},
wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy},
},
{
name: "update RTCPMuxPolicy",
init: func() (*PeerConnection, error) {
return NewPeerConnection(Configuration{})
},
config: Configuration{
RTCPMuxPolicy: RTCPMuxPolicyNegotiate,
},
wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingRTCPMuxPolicy},
},
{
name: "update ICECandidatePoolSize",
init: func() (*PeerConnection, error) {
pc, err := NewPeerConnection(Configuration{
ICECandidatePoolSize: 0,
})
if err != nil {
return pc, err
}
offer, err := pc.CreateOffer(nil)
if err != nil {
return pc, err
}
err = pc.SetLocalDescription(offer)
if err != nil {
return pc, err
}
return pc, nil
},
config: Configuration{
ICECandidatePoolSize: 1,
},
wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingICECandidatePoolSize},
},
} {
pc, err := test.init()
if err != nil {
t.Errorf("SetConfiguration %q: init failed: %v", test.name, err)
}
err = pc.SetConfiguration(test.config)
if got, want := err, test.wantErr; !reflect.DeepEqual(got, want) {
t.Errorf("SetConfiguration %q: err = %v, want %v", test.name, got, want)
}
assert.NoError(t, pc.Close())
}
}
func TestPeerConnection_GetConfiguration(t *testing.T) {
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
expected := Configuration{
ICEServers: []ICEServer{},
ICETransportPolicy: ICETransportPolicyAll,
BundlePolicy: BundlePolicyBalanced,
RTCPMuxPolicy: RTCPMuxPolicyRequire,
ICECandidatePoolSize: 0,
}
actual := pc.GetConfiguration()
assert.True(t, &expected != &actual)
assert.Equal(t, expected.ICEServers, actual.ICEServers)
assert.Equal(t, expected.ICETransportPolicy, actual.ICETransportPolicy)
assert.Equal(t, expected.BundlePolicy, actual.BundlePolicy)
assert.Equal(t, expected.RTCPMuxPolicy, actual.RTCPMuxPolicy)
// nolint:godox
// TODO(albrow): Uncomment this after #513 is fixed.
// See: https://github.com/pion/webrtc/issues/513.
// assert.Equal(t, len(expected.Certificates), len(actual.Certificates))
assert.Equal(t, expected.ICECandidatePoolSize, actual.ICECandidatePoolSize)
assert.NoError(t, pc.Close())
}
const minimalOffer = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 47299 DTLS/SCTP 5000
c=IN IP4 192.168.20.129
a=candidate:1966762134 1 udp 2122260223 192.168.20.129 47299 typ host generation 0
a=candidate:1966762134 1 udp 2122262783 2001:db8::1 47199 typ host generation 0
a=candidate:211962667 1 udp 2122194687 10.0.3.1 40864 typ host generation 0
a=candidate:1002017894 1 tcp 1518280447 192.168.20.129 0 typ host tcptype active generation 0
a=candidate:1109506011 1 tcp 1518214911 10.0.3.1 0 typ host tcptype active generation 0
a=ice-ufrag:1/MvHwjAyVf27aLu
a=ice-pwd:3dBU7cFOBl120v33cynDvN1E
a=ice-options:google-ice
a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24
a=setup:actpass
a=mid:data
a=sctpmap:5000 webrtc-datachannel 1024
`
func TestSetRemoteDescription(t *testing.T) {
testCases := []struct {
desc SessionDescription
expectError bool
}{
{SessionDescription{Type: SDPTypeOffer, SDP: minimalOffer}, false},
{SessionDescription{Type: 0, SDP: ""}, true},
}
for i, testCase := range testCases {
peerConn, err := NewPeerConnection(Configuration{})
if err != nil {
t.Errorf("Case %d: got error: %v", i, err)
}
if testCase.expectError {
assert.Error(t, peerConn.SetRemoteDescription(testCase.desc))
} else {
assert.NoError(t, peerConn.SetRemoteDescription(testCase.desc))
}
assert.NoError(t, peerConn.Close())
}
}
func TestCreateOfferAnswer(t *testing.T) {
offerPeerConn, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
answerPeerConn, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
_, err = offerPeerConn.CreateDataChannel("test-channel", nil)
assert.NoError(t, err)
offer, err := offerPeerConn.CreateOffer(nil)
assert.NoError(t, err)
assert.NoError(t, offerPeerConn.SetLocalDescription(offer))
assert.NoError(t, answerPeerConn.SetRemoteDescription(offer))
answer, err := answerPeerConn.CreateAnswer(nil)
assert.NoError(t, err)
assert.NoError(t, answerPeerConn.SetLocalDescription(answer))
assert.NoError(t, offerPeerConn.SetRemoteDescription(answer))
// after setLocalDescription(answer), signaling state should be stable.
// so CreateAnswer should return an InvalidStateError
assert.Equal(t, answerPeerConn.SignalingState(), SignalingStateStable)
_, err = answerPeerConn.CreateAnswer(nil)
assert.Error(t, err)
closePairNow(t, offerPeerConn, answerPeerConn)
}
func TestPeerConnection_EventHandlers(t *testing.T) {
pcOffer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
// wasCalled is a list of event handlers that were called.
wasCalled := []string{}
wasCalledMut := &sync.Mutex{}
// wg is used to wait for all event handlers to be called.
wg := &sync.WaitGroup{}
wg.Add(6)
// Each sync.Once is used to ensure that we call wg.Done once for each event
// handler and don't add multiple entries to wasCalled. The event handlers can
// be called more than once in some cases.
onceOffererOnICEConnectionStateChange := &sync.Once{}
onceOffererOnConnectionStateChange := &sync.Once{}
onceOffererOnSignalingStateChange := &sync.Once{}
onceAnswererOnICEConnectionStateChange := &sync.Once{}
onceAnswererOnConnectionStateChange := &sync.Once{}
onceAnswererOnSignalingStateChange := &sync.Once{}
// Register all the event handlers.
pcOffer.OnICEConnectionStateChange(func(ICEConnectionState) {
onceOffererOnICEConnectionStateChange.Do(func() {
wasCalledMut.Lock()
defer wasCalledMut.Unlock()
wasCalled = append(wasCalled, "offerer OnICEConnectionStateChange")
wg.Done()
})
})
pcOffer.OnConnectionStateChange(func(PeerConnectionState) {
onceOffererOnConnectionStateChange.Do(func() {
wasCalledMut.Lock()
defer wasCalledMut.Unlock()
wasCalled = append(wasCalled, "offerer OnConnectionStateChange")
wg.Done()
})
})
pcOffer.OnSignalingStateChange(func(SignalingState) {
onceOffererOnSignalingStateChange.Do(func() {
wasCalledMut.Lock()
defer wasCalledMut.Unlock()
wasCalled = append(wasCalled, "offerer OnSignalingStateChange")
wg.Done()
})
})
pcAnswer.OnICEConnectionStateChange(func(ICEConnectionState) {
onceAnswererOnICEConnectionStateChange.Do(func() {
wasCalledMut.Lock()
defer wasCalledMut.Unlock()
wasCalled = append(wasCalled, "answerer OnICEConnectionStateChange")
wg.Done()
})
})
pcAnswer.OnConnectionStateChange(func(PeerConnectionState) {
onceAnswererOnConnectionStateChange.Do(func() {
wasCalledMut.Lock()
defer wasCalledMut.Unlock()
wasCalled = append(wasCalled, "answerer OnConnectionStateChange")
wg.Done()
})
})
pcAnswer.OnSignalingStateChange(func(SignalingState) {
onceAnswererOnSignalingStateChange.Do(func() {
wasCalledMut.Lock()
defer wasCalledMut.Unlock()
wasCalled = append(wasCalled, "answerer OnSignalingStateChange")
wg.Done()
})
})
// Use signalPair to establish a connection between pcOffer and pcAnswer. This
// process should trigger the above event handlers.
assert.NoError(t, signalPair(pcOffer, pcAnswer))
// Wait for all of the event handlers to be triggered.
done := make(chan struct{})
go func() {
wg.Wait()
done <- struct{}{}
}()
timeout := time.After(5 * time.Second)
select {
case <-done:
break
case <-timeout:
t.Fatalf("timed out waiting for one or more events handlers to be called (these *were* called: %+v)", wasCalled)
}
closePairNow(t, pcOffer, pcAnswer)
}
func TestMultipleOfferAnswer(t *testing.T) {
firstPeerConn, err := NewPeerConnection(Configuration{})
if err != nil {
t.Errorf("New PeerConnection: got error: %v", err)
}
if _, err = firstPeerConn.CreateOffer(nil); err != nil {
t.Errorf("First Offer: got error: %v", err)
}
if _, err = firstPeerConn.CreateOffer(nil); err != nil {
t.Errorf("Second Offer: got error: %v", err)
}
secondPeerConn, err := NewPeerConnection(Configuration{})
if err != nil {
t.Errorf("New PeerConnection: got error: %v", err)
}
secondPeerConn.OnICECandidate(func(*ICECandidate) {
})
if _, err = secondPeerConn.CreateOffer(nil); err != nil {
t.Errorf("First Offer: got error: %v", err)
}
if _, err = secondPeerConn.CreateOffer(nil); err != nil {
t.Errorf("Second Offer: got error: %v", err)
}
closePairNow(t, firstPeerConn, secondPeerConn)
}
func TestNoFingerprintInFirstMediaIfSetRemoteDescription(t *testing.T) {
const sdpNoFingerprintInFirstMedia = `v=0
o=- 143087887 1561022767 IN IP4 192.168.84.254
s=VideoRoom 404986692241682
t=0 0
a=group:BUNDLE audio
a=msid-semantic: WMS 2867270241552712
m=video 0 UDP/TLS/RTP/SAVPF 0
a=mid:video
c=IN IP4 192.168.84.254
a=inactive
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 192.168.84.254
a=recvonly
a=mid:audio
a=rtcp-mux
a=ice-ufrag:AS/w
a=ice-pwd:9NOgoAOMALYu/LOpA1iqg/
a=ice-options:trickle
a=fingerprint:sha-256 D2:B9:31:8F:DF:24:D8:0E:ED:D2:EF:25:9E:AF:6F:B8:34:AE:53:9C:E6:F3:8F:F2:64:15:FA:E8:7F:53:2D:38
a=setup:active
a=rtpmap:111 opus/48000/2
a=candidate:1 1 udp 2013266431 192.168.84.254 46492 typ host
a=end-of-candidates
`
report := test.CheckRoutines(t)
defer report()
pc, err := NewPeerConnection(Configuration{})
if err != nil {
t.Error(err.Error())
}
desc := SessionDescription{
Type: SDPTypeOffer,
SDP: sdpNoFingerprintInFirstMedia,
}
if err = pc.SetRemoteDescription(desc); err != nil {
t.Error(err.Error())
}
assert.NoError(t, pc.Close())
}
func TestNegotiationNeeded(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
pc, err := NewPeerConnection(Configuration{})
if err != nil {
t.Error(err.Error())
}
var wg sync.WaitGroup
wg.Add(1)
pc.OnNegotiationNeeded(wg.Done)
_, err = pc.CreateDataChannel("initial_data_channel", nil)
assert.NoError(t, err)
wg.Wait()
assert.NoError(t, pc.Close())
}
func TestMultipleCreateChannel(t *testing.T) {
var wg sync.WaitGroup
report := test.CheckRoutines(t)
defer report()
// Two OnDataChannel
// One OnNegotiationNeeded
wg.Add(3)
pcOffer, _ := NewPeerConnection(Configuration{})
pcAnswer, _ := NewPeerConnection(Configuration{})
pcAnswer.OnDataChannel(func(*DataChannel) {
wg.Done()
})
pcOffer.OnNegotiationNeeded(func() {
offer, err := pcOffer.CreateOffer(nil)
assert.NoError(t, err)
offerGatheringComplete := GatheringCompletePromise(pcOffer)
if err = pcOffer.SetLocalDescription(offer); err != nil {
t.Error(err)
}
<-offerGatheringComplete
if err = pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()); err != nil {
t.Error(err)
}
answer, err := pcAnswer.CreateAnswer(nil)
assert.NoError(t, err)
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
if err = pcAnswer.SetLocalDescription(answer); err != nil {
t.Error(err)
}
<-answerGatheringComplete
if err = pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()); err != nil {
t.Error(err)
}
wg.Done()
})
if _, err := pcOffer.CreateDataChannel("initial_data_channel_0", nil); err != nil {
t.Error(err)
}
if _, err := pcOffer.CreateDataChannel("initial_data_channel_1", nil); err != nil {
t.Error(err)
}
wg.Wait()
closePairNow(t, pcOffer, pcAnswer)
}
// Assert that candidates are gathered by calling SetLocalDescription, not SetRemoteDescription
func TestGatherOnSetLocalDescription(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
pcOfferGathered := make(chan SessionDescription)
pcAnswerGathered := make(chan SessionDescription)
s := SettingEngine{}
api := NewAPI(WithSettingEngine(s))
pcOffer, err := api.NewPeerConnection(Configuration{})
if err != nil {
t.Error(err.Error())
}
// We need to create a data channel in order to trigger ICE
if _, err = pcOffer.CreateDataChannel("initial_data_channel", nil); err != nil {
t.Error(err.Error())
}
pcOffer.OnICECandidate(func(i *ICECandidate) {
if i == nil {
close(pcOfferGathered)
}
})
offer, err := pcOffer.CreateOffer(nil)
if err != nil {
t.Error(err.Error())
} else if err = pcOffer.SetLocalDescription(offer); err != nil {
t.Error(err.Error())
}
<-pcOfferGathered
pcAnswer, err := api.NewPeerConnection(Configuration{})
if err != nil {
t.Error(err.Error())
}
pcAnswer.OnICECandidate(func(i *ICECandidate) {
if i == nil {
close(pcAnswerGathered)
}
})
if err = pcAnswer.SetRemoteDescription(offer); err != nil {
t.Error(err.Error())
}
select {
case <-pcAnswerGathered:
t.Fatal("pcAnswer started gathering with no SetLocalDescription")
// Gathering is async, not sure of a better way to catch this currently
case <-time.After(3 * time.Second):
}
answer, err := pcAnswer.CreateAnswer(nil)
if err != nil {
t.Error(err.Error())
} else if err = pcAnswer.SetLocalDescription(answer); err != nil {
t.Error(err.Error())
}
<-pcAnswerGathered
closePairNow(t, pcOffer, pcAnswer)
}
// Assert that SetRemoteDescription handles invalid states
func TestSetRemoteDescriptionInvalid(t *testing.T) {
t.Run("local-offer+SetRemoteDescription(Offer)", func(t *testing.T) {
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
offer, err := pc.CreateOffer(nil)
assert.NoError(t, err)
assert.NoError(t, pc.SetLocalDescription(offer))
assert.Error(t, pc.SetRemoteDescription(offer))
assert.NoError(t, pc.Close())
})
}
func TestAddTransceiver(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
for _, testCase := range []struct {
expectSender, expectReceiver bool
direction RTPTransceiverDirection
}{
{true, true, RTPTransceiverDirectionSendrecv},
// Go and WASM diverge
// {true, false, RTPTransceiverDirectionSendonly},
// {false, true, RTPTransceiverDirectionRecvonly},
} {
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
transceiver, err := pc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
Direction: testCase.direction,
})
assert.NoError(t, err)
if testCase.expectReceiver {
assert.NotNil(t, transceiver.Receiver())
} else {
assert.Nil(t, transceiver.Receiver())
}
if testCase.expectSender {
assert.NotNil(t, transceiver.Sender())
} else {
assert.Nil(t, transceiver.Sender())
}
offer, err := pc.CreateOffer(nil)
assert.NoError(t, err)
assert.True(t, offerMediaHasDirection(offer, RTPCodecTypeVideo, testCase.direction))
assert.NoError(t, pc.Close())
}
}
// Assert that SCTPTransport -> DTLSTransport -> ICETransport works after connected
func TestTransportChain(t *testing.T) {
offer, answer, err := newPair()
assert.NoError(t, err)
peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, offer, answer)
assert.NoError(t, signalPair(offer, answer))
peerConnectionsConnected.Wait()
assert.NotNil(t, offer.SCTP().Transport().ICETransport())
closePairNow(t, offer, answer)
}
// Assert that the PeerConnection closes via DTLS (and not ICE)
func TestDTLSClose(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
pcOffer, pcAnswer, err := newPair()
assert.NoError(t, err)
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo)
assert.NoError(t, err)
peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, pcOffer, pcAnswer)
offer, err := pcOffer.CreateOffer(nil)
assert.NoError(t, err)
offerGatheringComplete := GatheringCompletePromise(pcOffer)
assert.NoError(t, pcOffer.SetLocalDescription(offer))
<-offerGatheringComplete
assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()))
answer, err := pcAnswer.CreateAnswer(nil)
assert.NoError(t, err)
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
<-answerGatheringComplete
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
peerConnectionsConnected.Wait()
assert.NoError(t, pcOffer.Close())
}