diff --git a/go.mod b/go.mod index 9c4a238..7aecf8d 100644 --- a/go.mod +++ b/go.mod @@ -10,15 +10,15 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.6.0 - github.com/pion/dtls/v3 v3.1.8 - github.com/pion/ice/v4 v4.4.2 - github.com/pion/interceptor v0.1.48 + github.com/pion/dtls/v4 v4.0.0-rc.1 + github.com/pion/ice/v4 v4.4.4 + github.com/pion/interceptor v0.1.49 github.com/pion/logging v0.2.4 - github.com/pion/rtcp v1.2.17 + github.com/pion/rtcp v1.2.18 github.com/pion/rtp v1.10.5 - github.com/pion/sdp/v3 v3.0.19 - github.com/pion/transport/v4 v4.1.0 - github.com/pion/webrtc/v4 v4.2.20 + github.com/pion/sdp/v3 v3.0.20 + github.com/pion/transport/v5 v5.1.1 + github.com/pion/webrtc/v4 v4.2.22 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.12.1 github.com/thesyncim/gopus v0.1.1 @@ -32,13 +32,14 @@ require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/pion/datachannel v1.6.2 // indirect - github.com/pion/mdns/v2 v2.2.0 // indirect + github.com/pion/datachannel v1.6.3 // indirect + github.com/pion/dtls/v3 v3.1.9 // indirect + github.com/pion/mdns/v2 v2.2.1 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.11.1 // indirect - github.com/pion/srtp/v3 v3.0.13 // indirect - github.com/pion/stun/v4 v4.0.0 // indirect - github.com/pion/turn/v5 v5.1.0 // indirect + github.com/pion/sctp v1.11.3 // indirect + github.com/pion/srtp/v3 v3.1.0 // indirect + github.com/pion/stun/v4 v4.0.1 // indirect + github.com/pion/turn/v5 v5.1.2 // indirect github.com/wlynxg/anet v0.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.48.0 // indirect @@ -46,3 +47,11 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect ) + +replace github.com/pion/webrtc/v4 => github.com/GetStream/pion-webrtc/v4 v4.2.22-warp.3 + +replace github.com/pion/ice/v4 => github.com/GetStream/pion-ice/v4 v4.4.4-warp.3 + +replace github.com/pion/dtls/v4 => github.com/GetStream/pion-dtls/v4 v4.0.0-rc.1-warp.1 + +replace github.com/pion/sctp => github.com/GetStream/pion-sctp v1.11.3-warp.1 diff --git a/go.sum b/go.sum index e46c594..4e93568 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ github.com/GetStream/getstream-go/v5 v5.2.0 h1:nrZkIL0mzHtByMeAeNFD4jpGIT5XPa0P3ItFL9bCIr8= github.com/GetStream/getstream-go/v5 v5.2.0/go.mod h1:qnod4MVOSwxfpqtXLpxjbMo7lDcepOlg8X2GrpBs/cw= +github.com/GetStream/pion-dtls/v4 v4.0.0-rc.1-warp.1 h1:MpkUBXJU3E6JJ2SkxxxZtiFi3he11Dh1K6zKOz0yn/8= +github.com/GetStream/pion-dtls/v4 v4.0.0-rc.1-warp.1/go.mod h1:P8msRRyK3bGwG3aZccN0Vk8chAMp2nQiBR9O29b86Io= +github.com/GetStream/pion-ice/v4 v4.4.4-warp.3 h1:ptB/HQG0ZAwn8uE+ZihJGNdolGcp/mVjQWDysQyTr14= +github.com/GetStream/pion-ice/v4 v4.4.4-warp.3/go.mod h1:vkrbLwF98P/DJuCKoAmOQY4FJ7dJPI5VcBFOO3kyYx0= +github.com/GetStream/pion-sctp v1.11.3-warp.1 h1:/PcOWTVe1jATCPIz32EP3XmKkYOUE7mfZ4BJhEVDmh4= +github.com/GetStream/pion-sctp v1.11.3-warp.1/go.mod h1:7v/MXGBROc8fu9g2TVwCXRasPISCMd3j8wSH+o45uCI= +github.com/GetStream/pion-webrtc/v4 v4.2.22-warp.3 h1:VlwJR+ESym/KHUGnppmvWGRB21rcsN84s94M4bSuW0Q= +github.com/GetStream/pion-webrtc/v4 v4.2.22-warp.3/go.mod h1:SZqPA/FyKvBzjbV2FgTnJQZrcLmhTXgeXOG1dDbL8Ac= github.com/GetStream/protocol v1.49.0 h1:AzTRH6+PRNHjFjBlcymbjZsVE+wZBp/wPG6w+/9FCw4= github.com/GetStream/protocol v1.49.0/go.mod h1:jZALur2IoWnpcLnFzLtnFomgItt4SBHj+U1TjPYC114= github.com/GetStream/protocol v1.50.0-3rtt.1 h1:kqsE1ZBf3gpO47lc2JLkyNVhVR9JNGFRYUtn83nLgIo= @@ -31,38 +39,32 @@ github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/ github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/pion/datachannel v1.6.2 h1:7EXQ8TH3vTouBUdRWYbcX2edSx9Yj6k5zl5P+qyxEPc= -github.com/pion/datachannel v1.6.2/go.mod h1:pzbdAZvyGtXbcHM1hBbsFaOTf40lZizU/dNlvVOak6E= -github.com/pion/dtls/v3 v3.1.8 h1:aLcgjZqzrYn5AbjSds4LvK2WI5VzJc1PencExyDjYis= -github.com/pion/dtls/v3 v3.1.8/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU= -github.com/pion/ice/v4 v4.4.2 h1:asS17nbHJrzlVQl8fiSJaipxrxSY3Dq6DWgmB+0VwpI= -github.com/pion/ice/v4 v4.4.2/go.mod h1:YZgNFOyJWXpLpLj0mb4ccqNAWRo9C2RtqKwuVTEz0Sc= -github.com/pion/interceptor v0.1.48 h1:FF4gZ6Yh+N75gKMYpC7rYR8DdkiMBFtA7V2OBUKo7XQ= -github.com/pion/interceptor v0.1.48/go.mod h1:5mg/N5xXMAa4codCUdrJYY9I1y4tVQhlpd7rfwAJpvI= +github.com/pion/datachannel v1.6.3 h1:HZAYLpunVI0xi057ZiANl558d0uSpomcWw4UEPz3qXs= +github.com/pion/datachannel v1.6.3/go.mod h1:oRmkXVVXxkUbqa8MatgdNHW29kvtQY3HUdjJQsfv2GE= +github.com/pion/dtls/v3 v3.1.9 h1:rpeycmLIkc4krpk1IxP7+39o11QdCXbmV9+FGi9yZJ8= +github.com/pion/dtls/v3 v3.1.9/go.mod h1:iKFQNYrjsN2TiA2YKKMqB9MOZaFpjFULBI/A4sW0eyc= +github.com/pion/interceptor v0.1.49 h1:iyBsNHRoLNNXZiJA/DdGvJgdyfS8YWDCzxIAlVjt5nY= +github.com/pion/interceptor v0.1.49/go.mod h1:MZ6PJkja/TCo350HAnBrs/rUIyad9mWjpcvytrf3ViQ= 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.2.0 h1:AlAZ9MTUKtWgO+4itk35JdNak4sk5k7G/X4xnIBWHyA= -github.com/pion/mdns/v2 v2.2.0/go.mod h1:IJddx58QMlojqhQYjHcOUmvuBQ5MnLNetkb80VMvk2Y= +github.com/pion/mdns/v2 v2.2.1 h1:lQPWv6Mo5jG1sKwqkYgfeXGX7/seban4x7/j0n485/M= +github.com/pion/mdns/v2 v2.2.1/go.mod h1:ZX5f0AAH1D6TOjdjvcBORZZHaZuG0t9+br78lHEwiJ4= 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.17 h1:PxiT6L79yPZKtXIsXdG1eakBl6dtBj4x+4oVEL0DlSw= -github.com/pion/rtcp v1.2.17/go.mod h1:7kBpuBJaWwax4hzc/pgexY8vkOpvh8atgYDbaKZq0iU= +github.com/pion/rtcp v1.2.18 h1:ZPgMZMCtBoReTixtFAyzcOkuJiebyB94qxtBAyvSooM= +github.com/pion/rtcp v1.2.18/go.mod h1:vX7Es6skWYm4oVvOSC5+4WHLYUTZHcbGNmk5wjzMGH4= github.com/pion/rtp v1.10.5 h1:ip0HhO/wYZqQ4bKS+R99KnZh/GRCmIT0jDXikub7vlE= github.com/pion/rtp v1.10.5/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= -github.com/pion/sctp v1.11.1 h1:O4dIFyURw1KTST7w+gtD4gLeYXkhPa0xXLHMMoe/OSA= -github.com/pion/sctp v1.11.1/go.mod h1:7KFmTwLcoYgJs/Z+99nJvsWL0qDpuyloSI0RbAqlrz0= -github.com/pion/sdp/v3 v3.0.19 h1:1VMKs3gIkTQV5M3hNKfTAPrDXSNrYtOlmOD8+mSZUGQ= -github.com/pion/sdp/v3 v3.0.19/go.mod h1:dE5WOSlzXrtiE/iuZqe9n+AcEbOjtAd3k5m5NtlV/qU= -github.com/pion/srtp/v3 v3.0.13 h1:FmQaqgNbN1vUtMhEsmj8trldc3lNZr1xmN7nl8CyX+Q= -github.com/pion/srtp/v3 v3.0.13/go.mod h1:7qR3L69t8RX0EPVQwGNwCa1Gy9keKKNDpWwQzZbeXDY= -github.com/pion/stun/v4 v4.0.0 h1:UuQy2q6iZR4EnMl/+G8kAtaWhf7jx/u8ZyN9oD4+YEQ= -github.com/pion/stun/v4 v4.0.0/go.mod h1:JAojPsPtDH4iPeNQb7kvxBdzypmkWAROxy0coApKH2E= -github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ= -github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= -github.com/pion/turn/v5 v5.1.0 h1:OSzLub7q4GssG1P4BEVrz39MnnJlxLy1LOvEkP9f46o= -github.com/pion/turn/v5 v5.1.0/go.mod h1:6HJQO7UAe7pEPMrTtBrmj+tTfp+Ai8KAV2+GXHFi1nQ= -github.com/pion/webrtc/v4 v4.2.20 h1:NYiNhBTFArA8aoP18a30y4LN0dyqSrF65HxU7KnhNOo= -github.com/pion/webrtc/v4 v4.2.20/go.mod h1:aLGXbekuN0tHOu7IPX1o9Y/uN29Ovfjaaz4bL2P9ND8= +github.com/pion/sdp/v3 v3.0.20 h1:TS6DViqcmp+49f0+mjw9anbr9xY3vJtsZewxAvlMCRQ= +github.com/pion/sdp/v3 v3.0.20/go.mod h1:slIMXDK5OKj0nhISwjfeN18AzTBCt2LYZq9uPw0cU5Q= +github.com/pion/srtp/v3 v3.1.0 h1:keugrBOxtvAv5Urf4lBMfso8OCAX3mm7Brdl26JSvik= +github.com/pion/srtp/v3 v3.1.0/go.mod h1:RlTlj08MtRReQiVm/PMmMFUDNjZkoKuKJMHkl9+M7a8= +github.com/pion/stun/v4 v4.0.1 h1:S2ggK4hsUJJKoZfpL90uIqIRTj6xJ0NOIEkXgcpQif0= +github.com/pion/stun/v4 v4.0.1/go.mod h1:byktbPA8U7HjJ2H8w3moP7nzq0Q+kYNulvMLPaubQL4= +github.com/pion/transport/v5 v5.1.1 h1:Sauq8YJqRopgRfIgnpfKpdpKG14g0N4jWlRgpWsEfN8= +github.com/pion/transport/v5 v5.1.1/go.mod h1:Qxw6fCEjFWQkRDZOhS4Vf+neJBcihauvA3uyEa1J1F0= +github.com/pion/turn/v5 v5.1.2 h1:acEOO+D5txJ52vKfcilmSMItfP23Q9K6pJ1LM5P2VV0= +github.com/pion/turn/v5 v5.1.2/go.mod h1:57Oadc8fHnac15VFyCd6nEaBTVZBxQe2POUMD4lsHRA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/netdelay/netdelay.go b/internal/netdelay/netdelay.go index 3ca82d5..4a5f80f 100644 --- a/internal/netdelay/netdelay.go +++ b/internal/netdelay/netdelay.go @@ -17,7 +17,7 @@ import ( "sync" "time" - "github.com/pion/transport/v4/deadline" + "github.com/pion/transport/v5/deadline" ) // DialFunc opens a network connection, like net.Dialer.DialContext. diff --git a/internal/netdelay/packet.go b/internal/netdelay/packet.go index a2340bd..0990be5 100644 --- a/internal/netdelay/packet.go +++ b/internal/netdelay/packet.go @@ -5,8 +5,8 @@ import ( "net" "time" - "github.com/pion/transport/v4" - "github.com/pion/transport/v4/stdnet" + "github.com/pion/transport/v5" + "github.com/pion/transport/v5/stdnet" ) // PacketConn is a datagram socket with delayed reads and writes. diff --git a/pc/hello_verify_test.go b/pc/hello_verify_test.go index b3c24c8..5181bb1 100644 --- a/pc/hello_verify_test.go +++ b/pc/hello_verify_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/pion/dtls/v3/pkg/protocol/handshake" + "github.com/pion/dtls/v4/pkg/protocol/handshake" "github.com/pion/webrtc/v4" "github.com/stretchr/testify/require" ) diff --git a/pc/pc.go b/pc/pc.go index d065c5c..0a74b8e 100644 --- a/pc/pc.go +++ b/pc/pc.go @@ -8,8 +8,9 @@ import ( "time" "github.com/GetStream/protocol/protobuf/video/sfu/models" - "github.com/pion/dtls/v3/pkg/crypto/elliptic" - "github.com/pion/dtls/v3/pkg/protocol/handshake" + "github.com/pion/dtls/v4/pkg/crypto/elliptic" + "github.com/pion/dtls/v4/pkg/protocol" + "github.com/pion/dtls/v4/pkg/protocol/handshake" "github.com/pion/ice/v4" "github.com/pion/interceptor" "github.com/pion/rtcp" @@ -230,6 +231,18 @@ func newPeerConnection(params TransportParams) (*webrtc.PeerConnection, error) { // for, and browsers never send one either. se.SetDTLSInsecureSkipHelloVerify(true) se.SetICETimeouts(params.iceTimeouts()) + // Offer the WARP connection setup (draft-uberti-tsvwg-warp) that the SFU negotiates in + // band: DTLS 1.3, and DTLS in the ICE checks (SPED). An SFU without them answers as + // before, so the connection falls back to DTLS 1.2 after ICE. + if err := se.SetDTLSVersionRange(protocol.Version1_2, protocol.Version1_3); err != nil { + return nil, xerr.Wrap(err) + } + se.EnableSped(true) + // On the subscriber, which the ICE-lite SFU offers, pion would answer passive and make + // the SFU the DTLS client, whose ClientHello can only ride our checks' responses, one + // datagram each. Answering active to an offer with SPED puts our one-datagram ClientHello + // in our first check, as browsers do. + se.SetAnsweringDTLSRoleWithSPED(webrtc.DTLSRoleClient) // One line per handshake message we send pins down which direction a // stalled handshake lost. @@ -934,6 +947,13 @@ func (t *Transport) Timing() Timing { // SelectedPairRTT is the round-trip time ICE measured on the selected candidate pair, or // zero before it has one. func (t *Transport) SelectedPairRTT() time.Duration { + // pion reads the ICE transport's gatherer without its lock, while ICETransport.Start + // sets it. With SPED, Start runs inside the DTLS start, concurrently with callers here, + // so wait until ICE has connected, which happens after Start. + switch t.PC.ICEConnectionState() { + case webrtc.ICEConnectionStateNew, webrtc.ICEConnectionStateChecking: + return 0 + } dtls := dtlsTransportOf(t.PC) if dtls == nil || dtls.ICETransport() == nil { return 0 diff --git a/pc/warp_test.go b/pc/warp_test.go new file mode 100644 index 0000000..60cbd8d --- /dev/null +++ b/pc/warp_test.go @@ -0,0 +1,135 @@ +package pc + +import ( + "testing" + "time" + + "github.com/pion/dtls/v4/pkg/protocol" + "github.com/pion/ice/v4" + "github.com/pion/webrtc/v4" + "github.com/stretchr/testify/require" + + "github.com/GetStream/getstream-go-webrtc/logger" + "github.com/GetStream/protocol/protobuf/video/sfu/models" +) + +// TestWARPNegotiation connects a publisher transport, which offers, to a pion peer +// configured like an SFU. A WARP SFU (DTLS 1.2 to 1.3, SPED, the DTLS server role when +// the offer has SPED) gets DTLS 1.3 and SPED; an SFU without WARP gets DTLS 1.2 without +// SPED, as before. The selected pair's RTT is read throughout, as the join trace does. +func TestWARPNegotiation(t *testing.T) { + for _, tc := range []struct { + name string + warp bool + wantVersion protocol.Version + }{ + {name: "WARPSFU", warp: true, wantVersion: protocol.Version1_3}, + {name: "LegacySFU", warp: false, wantVersion: protocol.Version1_2}, + } { + t.Run(tc.name, func(t *testing.T) { + st := newPCTestWithTransportParams(t, TransportParams{ + Transport: models.PeerType_PEER_TYPE_PUBLISHER_UNSPECIFIED, + Logger: logger.Noop{}, + IsOfferer: true, + PeerConfig: newPCTestPeerConfig(t), + }) + + sfu := newPCTestPeerConfig(t) + sfu.SettingEngine.SetLite(true) + if tc.warp { + require.NoError(t, sfu.SettingEngine.SetDTLSVersionRange(protocol.Version1_2, protocol.Version1_3)) + sfu.SettingEngine.EnableSped(true) + sfu.SettingEngine.SetAnsweringDTLSRoleWithSPED(webrtc.DTLSRoleServer) + } + remote := newRemotePeerWithConfig(t, st.tr, sfu) + st.handler.onICECandidateSender = remote.ICECandidateSender + + done := make(chan struct{}) + polled := make(chan struct{}) + go func() { + defer close(polled) + for { + select { + case <-done: + return + default: + _ = st.tr.SelectedPairRTT() + } + } + }() + defer func() { close(done); <-polled }() + + st.tr.Negotiate(true) + st.tr.HandleRemoteDescription(remote.Answer(st.waitForOffer())) + st.waitForPCState(webrtc.PeerConnectionStateConnected, 5*time.Second) + + // The two ends finish the handshake one flight apart. + require.Eventually(t, func() bool { + return remote.PC.ConnectionState() == webrtc.PeerConnectionStateConnected + }, 5*time.Second, 10*time.Millisecond) + local, sfuState := st.tr.PC.WARPState(), remote.PC.WARPState() + require.Equal(t, tc.wantVersion, local.DTLSVersion) + require.Equal(t, tc.wantVersion, sfuState.DTLSVersion) + if !tc.warp { + require.Equal(t, ice.SPEDStateDisabled, local.SPED) + require.Equal(t, ice.SPEDStateDisabled, sfuState.SPED) + return + } + // SPED completes once everything sent is acknowledged or media arrives. + for _, state := range []ice.SPEDState{local.SPED, sfuState.SPED} { + require.Contains(t, []ice.SPEDState{ice.SPEDStatePending, ice.SPEDStateComplete}, state) + } + }) + } +} + +// TestWARPSubscriberAnswersActive has a pion peer configured like a WARP SFU (ICE-lite, DTLS +// 1.2 to 1.3, SPED) offer to a subscriber transport. The transport must answer active, so +// that it is the DTLS client and its ClientHello rides its first check, and get DTLS 1.3 with +// SPED. +func TestWARPSubscriberAnswersActive(t *testing.T) { + st := newPCTestWithTransportParams(t, TransportParams{ + Transport: models.PeerType_PEER_TYPE_SUBSCRIBER, + Logger: logger.Noop{}, + IsOfferer: false, + PeerConfig: newPCTestPeerConfig(t), + }) + + sfu := newPCTestPeerConfig(t) + sfu.SettingEngine.SetLite(true) + require.NoError(t, sfu.SettingEngine.SetDTLSVersionRange(protocol.Version1_2, protocol.Version1_3)) + sfu.SettingEngine.EnableSped(true) + remote := newRemotePeerWithConfig(t, st.tr, sfu) + st.handler.onICECandidateSender = remote.ICECandidateSender + _, err := remote.PC.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionSendonly, + }) + require.NoError(t, err) + + offer, err := remote.PC.CreateOffer(nil) + require.NoError(t, err) + require.NoError(t, remote.PC.SetLocalDescription(offer)) + st.tr.HandleRemoteDescription(*remote.PC.LocalDescription()) + answer, _ := st.waitForAnswerWithID() + require.Contains(t, answer.SDP, "a=setup:active") + require.NoError(t, remote.PC.SetRemoteDescription(answer)) + remote.mu.Lock() + remote.ready = true + pending := remote.pending + remote.pending = nil + remote.mu.Unlock() + for _, c := range pending { + require.NoError(t, remote.PC.AddICECandidate(c)) + } + + st.waitForPCState(webrtc.PeerConnectionStateConnected, 5*time.Second) + require.Eventually(t, func() bool { + return remote.PC.ConnectionState() == webrtc.PeerConnectionStateConnected + }, 5*time.Second, 10*time.Millisecond) + local, sfuState := st.tr.PC.WARPState(), remote.PC.WARPState() + require.Equal(t, protocol.Version1_3, local.DTLSVersion) + require.Equal(t, protocol.Version1_3, sfuState.DTLSVersion) + for _, state := range []ice.SPEDState{local.SPED, sfuState.SPED} { + require.Contains(t, []ice.SPEDState{ice.SPEDStatePending, ice.SPEDStateComplete}, state) + } +}