Skip to content

Commit 210cd95

Browse files
committed
Add CanTrickleICECandidates
w3c CanTrickleICECandidates.
1 parent aac830d commit 210cd95

File tree

7 files changed

+205
-0
lines changed

7 files changed

+205
-0
lines changed

peerconnection.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ type PeerConnection struct {
6565

6666
lastOffer string
6767
lastAnswer string
68+
// Whether the remote endpoint can accept trickled ICE candidates.
69+
canTrickleICECandidates ICETrickleCapability
6870

6971
// a value containing the last known greater mid value
7072
// we internally generate mids as numbers. Needed since JSEP
@@ -1104,6 +1106,7 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
11041106
if _, err := desc.Unmarshal(); err != nil {
11051107
return err
11061108
}
1109+
11071110
if err := pc.setDescription(&desc, stateChangeOpSetRemote); err != nil {
11081111
return err
11091112
}
@@ -1112,6 +1115,20 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
11121115
return err
11131116
}
11141117

1118+
canTrickle := hasICETrickleOption(desc.parsed)
1119+
pc.mu.Lock()
1120+
switch desc.Type {
1121+
case SDPTypeOffer, SDPTypeAnswer, SDPTypePranswer:
1122+
if canTrickle {
1123+
pc.canTrickleICECandidates = ICETrickleCapabilitySupported
1124+
} else {
1125+
pc.canTrickleICECandidates = ICETrickleCapabilityUnsupported
1126+
}
1127+
default:
1128+
pc.canTrickleICECandidates = ICETrickleCapabilityUnknown
1129+
}
1130+
pc.mu.Unlock()
1131+
11151132
// Disable RTX/FEC on RTPSenders if the remote didn't support it
11161133
for _, sender := range pc.GetSenders() {
11171134
sender.configureRTXAndFEC()
@@ -2583,6 +2600,15 @@ func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription {
25832600
return pc.pendingRemoteDescription
25842601
}
25852602

2603+
// CanTrickleICECandidates reports whether the remote endpoint indicated
2604+
// support for receiving trickled ICE candidates.
2605+
func (pc *PeerConnection) CanTrickleICECandidates() ICETrickleCapability {
2606+
pc.mu.RLock()
2607+
defer pc.mu.RUnlock()
2608+
2609+
return pc.canTrickleICECandidates
2610+
}
2611+
25862612
// SignalingState attribute returns the signaling state of the
25872613
// PeerConnection instance.
25882614
func (pc *PeerConnection) SignalingState() SignalingState {

peerconnection_go_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2203,3 +2203,40 @@ func TestAddICECandidate__DroppingOldGenerationCandidates(t *testing.T) {
22032203

22042204
closePairNow(t, pc, remotePC)
22052205
}
2206+
2207+
func TestPeerConnectionCanTrickleICECandidatesGo(t *testing.T) {
2208+
offerPC, answerPC, wan := createVNetPair(t, nil)
2209+
var err error
2210+
defer func() {
2211+
assert.NoError(t, wan.Stop())
2212+
closePairNow(t, offerPC, answerPC)
2213+
}()
2214+
2215+
_, err = offerPC.CreateDataChannel("trickle", nil)
2216+
assert.NoError(t, err)
2217+
2218+
offer, err := offerPC.CreateOffer(&OfferOptions{
2219+
OfferAnswerOptions: OfferAnswerOptions{ICETricklingSupported: true},
2220+
})
2221+
assert.NoError(t, err)
2222+
assert.NoError(t, offerPC.SetLocalDescription(offer))
2223+
assert.Equal(t, ICETrickleCapabilityUnknown, answerPC.CanTrickleICECandidates())
2224+
assert.NoError(t, answerPC.SetRemoteDescription(offer))
2225+
assert.Equal(t, ICETrickleCapabilitySupported, answerPC.CanTrickleICECandidates())
2226+
2227+
noTrickleOfferPC, noTrickleAnswerPC, noTrickleWAN := createVNetPair(t, nil)
2228+
defer func() {
2229+
assert.NoError(t, noTrickleWAN.Stop())
2230+
closePairNow(t, noTrickleOfferPC, noTrickleAnswerPC)
2231+
}()
2232+
2233+
_, err = noTrickleOfferPC.CreateDataChannel("notrickle", nil)
2234+
assert.NoError(t, err)
2235+
2236+
noTrickleOffer, err := noTrickleOfferPC.CreateOffer(nil)
2237+
assert.NoError(t, err)
2238+
assert.NoError(t, noTrickleOfferPC.SetLocalDescription(noTrickleOffer))
2239+
assert.Equal(t, ICETrickleCapabilityUnknown, noTrickleAnswerPC.CanTrickleICECandidates())
2240+
assert.NoError(t, noTrickleAnswerPC.SetRemoteDescription(noTrickleOffer))
2241+
assert.Equal(t, ICETrickleCapabilityUnsupported, noTrickleAnswerPC.CanTrickleICECandidates())
2242+
}

peerconnection_js.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,21 @@ func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription {
463463
return valueToSessionDescription(desc)
464464
}
465465

466+
// CanTrickleICECandidates reports whether the remote endpoint indicated
467+
// support for receiving trickled ICE candidates.
468+
func (pc *PeerConnection) CanTrickleICECandidates() ICETrickleCapability {
469+
val := pc.underlying.Get("canTrickleIceCandidates")
470+
if val.IsNull() || val.IsUndefined() {
471+
return ICETrickleCapabilityUnknown
472+
}
473+
474+
if val.Bool() {
475+
return ICETrickleCapabilitySupported
476+
}
477+
478+
return ICETrickleCapabilityUnsupported
479+
}
480+
466481
// SignalingState returns the signaling state of the PeerConnection instance.
467482
func (pc *PeerConnection) SignalingState() SignalingState {
468483
rawState := pc.underlying.Get("signalingState").String()
@@ -714,6 +729,7 @@ func valueToSessionDescription(descValue js.Value) *SessionDescription {
714729
if descValue.IsNull() || descValue.IsUndefined() {
715730
return nil
716731
}
732+
717733
return &SessionDescription{
718734
Type: NewSDPType(descValue.Get("type").String()),
719735
SDP: descValue.Get("sdp").String(),

peerconnection_js_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,20 @@ func TestValueToICEServer(t *testing.T) {
104104
assert.Equal(t, testCase, s)
105105
}
106106
}
107+
108+
func TestPeerConnectionCanTrickleICECandidatesJS(t *testing.T) {
109+
pc := &PeerConnection{
110+
underlying: js.ValueOf(map[string]any{
111+
"canTrickleIceCandidates": true,
112+
}),
113+
}
114+
assert.Equal(t, ICETrickleCapabilitySupported, pc.CanTrickleICECandidates())
115+
116+
pc.underlying = js.ValueOf(map[string]any{
117+
"canTrickleIceCandidates": false,
118+
})
119+
assert.Equal(t, ICETrickleCapabilityUnsupported, pc.CanTrickleICECandidates())
120+
121+
pc.underlying = js.ValueOf(map[string]any{})
122+
assert.Equal(t, ICETrickleCapabilityUnknown, pc.CanTrickleICECandidates())
123+
}

peerconnection_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,3 +806,18 @@ func TestPeerConnection_SessionID(t *testing.T) {
806806
}
807807
closePairNow(t, pcOffer, pcAnswer)
808808
}
809+
810+
func TestICETrickleCapabilityString(t *testing.T) {
811+
tests := []struct {
812+
value ICETrickleCapability
813+
expected string
814+
}{
815+
{ICETrickleCapabilityUnknown, "unknown"},
816+
{ICETrickleCapabilitySupported, "supported"},
817+
{ICETrickleCapabilityUnsupported, "unsupported"},
818+
}
819+
820+
for _, tt := range tests {
821+
assert.Equal(t, tt.expected, tt.value.String())
822+
}
823+
}

sessiondescription.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,37 @@ package webrtc
55

66
import (
77
"fmt"
8+
"slices"
9+
"strings"
810

911
"github.com/pion/sdp/v3"
1012
)
1113

14+
// ICETrickleCapability represents whether the remote endpoint accepts
15+
// trickled ICE candidates.
16+
type ICETrickleCapability int
17+
18+
const (
19+
// ICETrickleCapabilityUnknown no remote peer has been established.
20+
ICETrickleCapabilityUnknown ICETrickleCapability = iota
21+
// ICETrickleCapabilitySupported remote peer can accept trickled ICE candidates.
22+
ICETrickleCapabilitySupported
23+
// ICETrickleCapabilitySupported remote peer didn't state that it can accept trickle ICE candidates.
24+
ICETrickleCapabilityUnsupported
25+
)
26+
27+
// String returns the string representation of ICETrickleCapability.
28+
func (t ICETrickleCapability) String() string {
29+
switch t {
30+
case ICETrickleCapabilitySupported:
31+
return "supported"
32+
case ICETrickleCapabilityUnsupported:
33+
return "unsupported"
34+
default:
35+
return "unknown"
36+
}
37+
}
38+
1239
// SessionDescription is used to expose local and remote session descriptions.
1340
type SessionDescription struct {
1441
Type SDPType `json:"type"`
@@ -28,3 +55,21 @@ func (sd *SessionDescription) Unmarshal() (*sdp.SessionDescription, error) {
2855

2956
return sd.parsed, nil
3057
}
58+
59+
func hasICETrickleOption(desc *sdp.SessionDescription) bool {
60+
if value, ok := desc.Attribute(sdp.AttrKeyICEOptions); ok && hasTrickleOptionValue(value) {
61+
return true
62+
}
63+
64+
for _, media := range desc.MediaDescriptions {
65+
if value, ok := media.Attribute(sdp.AttrKeyICEOptions); ok && hasTrickleOptionValue(value) {
66+
return true
67+
}
68+
}
69+
70+
return false
71+
}
72+
73+
func hasTrickleOptionValue(value string) bool {
74+
return slices.Contains(strings.Fields(value), "trickle")
75+
}

sessiondescription_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package webrtc
66
import (
77
"encoding/json"
88
"reflect"
9+
"strings"
910
"testing"
1011

1112
"github.com/stretchr/testify/assert"
@@ -95,3 +96,51 @@ func TestSessionDescription_UnmarshalError(t *testing.T) {
9596
_, err := desc.Unmarshal()
9697
assert.ErrorIs(t, err, ErrSDPUnmarshalling)
9798
}
99+
100+
func TestHasICETrickleOption(t *testing.T) {
101+
baseSession := strings.Join([]string{
102+
"v=0",
103+
"o=- 0 0 IN IP4 127.0.0.1",
104+
"s=-",
105+
"t=0 0",
106+
}, "\r\n") + "\r\n"
107+
108+
baseMedia := strings.Join([]string{
109+
"m=audio 9 UDP/TLS/RTP/SAVPF 111",
110+
"c=IN IP4 0.0.0.0",
111+
"a=mid:0",
112+
"a=rtpmap:111 opus/48000/2",
113+
}, "\r\n") + "\r\n"
114+
115+
testCases := []struct {
116+
name string
117+
sdp string
118+
expected bool
119+
}{
120+
{
121+
name: "session level",
122+
sdp: baseSession + "a=ice-options:trickle\r\n" + baseMedia,
123+
expected: true,
124+
},
125+
{
126+
name: "media level",
127+
sdp: baseSession + baseMedia + "a=ice-options:trickle\r\n",
128+
expected: true,
129+
},
130+
{
131+
name: "no trickle",
132+
sdp: baseSession + "a=ice-options:google-ice\r\n" + baseMedia,
133+
expected: false,
134+
},
135+
}
136+
137+
for _, tc := range testCases {
138+
tc := tc
139+
t.Run(tc.name, func(t *testing.T) {
140+
desc := SessionDescription{Type: SDPTypeOffer, SDP: tc.sdp}
141+
_, err := desc.Unmarshal()
142+
assert.NoError(t, err)
143+
assert.Equal(t, tc.expected, hasICETrickleOption(desc.parsed))
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)