Skip to content

Commit f294146

Browse files
committed
Implement ICE Restarts
Resolves pion#1251
1 parent 4009686 commit f294146

File tree

10 files changed

+364
-34
lines changed

10 files changed

+364
-34
lines changed

examples/ice-restart/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ice-restart
2+
ice-restart demonstrates Pion WebRTC's ICE Restart abilities.
3+
4+
## Instructions
5+
6+
### Download ice-restart
7+
This example requires you to clone the repo since it is serving static HTML.
8+
9+
```
10+
mkdir -p $GOPATH/src/github.com/pion
11+
cd $GOPATH/src/github.com/pion
12+
git clone https://github.com/pion/webrtc.git
13+
cd webrtc/examples/ice-restart
14+
```
15+
16+
### Run ice-restart
17+
Execute `go run *.go`
18+
19+
### Open the Web UI
20+
Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection
21+
and allow you to do an ICE Restart at anytime.
22+
23+
* `ICE Restart` is the button that causes a new offer to be made wih `iceRestart: true`.
24+
* `ICE Connection States` will contain all the connection states the PeerConnection moves through.
25+
* `ICE Selected Pairs` will print the selected pair every 3 seconds. Note how the uFrag/uPwd/Port change everytime you start the Restart process.
26+
* `Inbound DataChannel Messages` containing the current time sent by the Pion process every 3 seconds.
27+
28+
Congrats, you have used Pion WebRTC! Now start building something cool

examples/ice-restart/index.html

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<html>
2+
<head>
3+
<title>ice-restart</title>
4+
</head>
5+
6+
<body>
7+
<button onclick="window.doSignaling(true)"> ICE Restart </button><br />
8+
9+
10+
<h3> ICE Connection States </h3>
11+
<div id="iceConnectionStates"></div> <br />
12+
13+
<h3> ICE Selected Pairs </h3>
14+
<div id="iceSelectedPairs"></div> <br />
15+
16+
<h3> Inbound DataChannel Messages </h3>
17+
<div id="inboundDataChannelMessages"></div>
18+
</body>
19+
20+
<script>
21+
let pc = new RTCPeerConnection()
22+
let dc = pc.createDataChannel('data')
23+
24+
dc.onopen = () => {
25+
setInterval(function() {
26+
let el = document.createElement('template')
27+
let selectedPair = pc.sctp.transport.iceTransport.getSelectedCandidatePair()
28+
29+
el.innerHTML = `<div>
30+
<ul>
31+
<li> <i> Local</i> - ${selectedPair.local.candidate}</li>
32+
<li> <i> Remote</i> - ${selectedPair.remote.candidate} </li>
33+
</ul>
34+
</div>`
35+
36+
document.getElementById('iceSelectedPairs').appendChild(el.content.firstChild);
37+
}, 3000);
38+
}
39+
40+
dc.onmessage = event => {
41+
let el = document.createElement('p')
42+
el.appendChild(document.createTextNode(event.data))
43+
44+
document.getElementById('inboundDataChannelMessages').appendChild(el);
45+
}
46+
47+
pc.oniceconnectionstatechange = () => {
48+
let el = document.createElement('p')
49+
el.appendChild(document.createTextNode(pc.iceConnectionState))
50+
51+
document.getElementById('iceConnectionStates').appendChild(el);
52+
}
53+
54+
55+
window.doSignaling = iceRestart => {
56+
pc.createOffer({iceRestart})
57+
.then(offer => {
58+
pc.setLocalDescription(offer)
59+
60+
return fetch(`/doSignaling`, {
61+
method: 'post',
62+
headers: {
63+
'Accept': 'application/json, text/plain, */*',
64+
'Content-Type': 'application/json'
65+
},
66+
body: JSON.stringify(offer)
67+
})
68+
})
69+
.then(res => res.json())
70+
.then(res => pc.setRemoteDescription(res))
71+
.catch(alert)
72+
}
73+
74+
window.doSignaling(false)
75+
</script>
76+
</html>

examples/ice-restart/main.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
"github.com/pion/webrtc/v3"
10+
)
11+
12+
var peerConnection *webrtc.PeerConnection //nolint
13+
14+
func doSignaling(w http.ResponseWriter, r *http.Request) {
15+
var err error
16+
17+
if peerConnection == nil {
18+
if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil {
19+
panic(err)
20+
}
21+
22+
// Set the handler for ICE connection state
23+
// This will notify you when the peer has connected/disconnected
24+
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
25+
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
26+
})
27+
28+
// Send the current time via a DataChannel to the remote peer every 3 seconds
29+
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
30+
d.OnOpen(func() {
31+
for range time.Tick(time.Second * 3) {
32+
if err = d.SendText(time.Now().String()); err != nil {
33+
panic(err)
34+
}
35+
}
36+
})
37+
})
38+
}
39+
40+
var offer webrtc.SessionDescription
41+
if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
42+
panic(err)
43+
}
44+
45+
if err = peerConnection.SetRemoteDescription(offer); err != nil {
46+
panic(err)
47+
}
48+
49+
// Create channel that is blocked until ICE Gathering is complete
50+
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
51+
52+
answer, err := peerConnection.CreateAnswer(nil)
53+
if err != nil {
54+
panic(err)
55+
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
56+
panic(err)
57+
}
58+
59+
// Block until ICE Gathering is complete, disabling trickle ICE
60+
// we do this because we only can exchange one signaling message
61+
// in a production application you should exchange ICE Candidates via OnICECandidate
62+
<-gatherComplete
63+
64+
response, err := json.Marshal(*peerConnection.LocalDescription())
65+
if err != nil {
66+
panic(err)
67+
}
68+
69+
w.Header().Set("Content-Type", "application/json")
70+
if _, err := w.Write(response); err != nil {
71+
panic(err)
72+
}
73+
}
74+
75+
func main() {
76+
http.Handle("/", http.FileServer(http.Dir(".")))
77+
http.HandleFunc("/doSignaling", doSignaling)
78+
79+
fmt.Println("Open http://localhost:8080 to access this demo")
80+
panic(http.ListenAndServe(":8080", nil))
81+
}

gathering_complete_promise.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package webrtc
22

3-
import "context"
3+
import (
4+
"context"
5+
)
46

57
// GatheringCompletePromise is a Pion specific helper function that returns a channel that is closed when gathering is complete.
68
// This function may be helpful in cases where you are unable to trickle your ICE Candidates.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.12
55
require (
66
github.com/pion/datachannel v1.4.17
77
github.com/pion/dtls/v2 v2.0.1
8-
github.com/pion/ice/v2 v2.0.0-rc.1
8+
github.com/pion/ice/v2 v2.0.0-rc.3
99
github.com/pion/logging v0.2.2
1010
github.com/pion/quic v0.1.1
1111
github.com/pion/rtcp v1.2.3

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5
3131
github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM=
3232
github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA=
3333
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
34-
github.com/pion/ice/v2 v2.0.0-rc.1 h1:1/5XKZx6Ioladykw5xp9/fCZG61pcmndTjY9bZhG0Fs=
35-
github.com/pion/ice/v2 v2.0.0-rc.1/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8=
34+
github.com/pion/ice/v2 v2.0.0-rc.3 h1:GvQ6nMGIGz7GltCUC9EU0m9JyQMan2vbifO4i8Y6T6A=
35+
github.com/pion/ice/v2 v2.0.0-rc.3/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8=
3636
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
3737
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
3838
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=

icegatherer.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,22 +136,22 @@ func (g *ICEGatherer) Gather() error {
136136
return err
137137
}
138138

139-
onLocalCandidateHdlr := func(*ICECandidate) {}
140-
if hdlr, ok := g.onLocalCandidateHdlr.Load().(func(candidate *ICECandidate)); ok && hdlr != nil {
141-
onLocalCandidateHdlr = hdlr
142-
}
143-
144-
onGatheringCompleteHdlr := func() {}
145-
if hdlr, ok := g.onGatheringCompleteHdlr.Load().(func()); ok && hdlr != nil {
146-
onGatheringCompleteHdlr = hdlr
147-
}
148-
149139
g.lock.Lock()
150140
agent := g.agent
151141
g.lock.Unlock()
152142

153143
g.setState(ICEGathererStateGathering)
154144
if err := agent.OnCandidate(func(candidate ice.Candidate) {
145+
onLocalCandidateHdlr := func(*ICECandidate) {}
146+
if hdlr, ok := g.onLocalCandidateHdlr.Load().(func(candidate *ICECandidate)); ok && hdlr != nil {
147+
onLocalCandidateHdlr = hdlr
148+
}
149+
150+
onGatheringCompleteHdlr := func() {}
151+
if hdlr, ok := g.onGatheringCompleteHdlr.Load().(func()); ok && hdlr != nil {
152+
onGatheringCompleteHdlr = hdlr
153+
}
154+
155155
if candidate != nil {
156156
c, err := newICECandidateFromICE(candidate)
157157
if err != nil {
@@ -194,7 +194,11 @@ func (g *ICEGatherer) GetLocalParameters() (ICEParameters, error) {
194194
return ICEParameters{}, err
195195
}
196196

197-
frag, pwd := g.agent.GetLocalUserCredentials()
197+
frag, pwd, err := g.agent.GetLocalUserCredentials()
198+
if err != nil {
199+
return ICEParameters{}, err
200+
}
201+
198202
return ICEParameters{
199203
UsernameFragment: frag,
200204
Password: pwd,

icetransport.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,23 @@ func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role *
152152
return nil
153153
}
154154

155+
// restart is not exposed currently because ORTC has users create a whole new ICETransport
156+
// so for now lets keep it private so we don't cause ORTC users to depend on non-standard APIs
157+
func (t *ICETransport) restart() error {
158+
t.lock.Lock()
159+
defer t.lock.Unlock()
160+
161+
agent := t.gatherer.getAgent()
162+
if agent == nil {
163+
return errors.New("ICEAgent does not exist, unable to restart ICETransport")
164+
}
165+
166+
if err := agent.Restart(t.gatherer.api.settingEngine.candidates.UsernameFragment, t.gatherer.api.settingEngine.candidates.Password); err != nil {
167+
return err
168+
}
169+
return t.gatherer.Gather()
170+
}
171+
155172
// Stop irreversibly stops the ICETransport.
156173
func (t *ICETransport) Stop() error {
157174
t.lock.Lock()
@@ -300,3 +317,32 @@ func (t *ICETransport) collectStats(collector *statsReportCollector) {
300317

301318
collector.Collect(stats.ID, stats)
302319
}
320+
321+
func (t *ICETransport) haveRemoteCredentialsChange(newUfrag, newPwd string) bool {
322+
t.lock.Lock()
323+
defer t.lock.Unlock()
324+
325+
agent := t.gatherer.getAgent()
326+
if agent == nil {
327+
return false
328+
}
329+
330+
uFrag, uPwd, err := agent.GetRemoteUserCredentials()
331+
if err != nil {
332+
return false
333+
}
334+
335+
return uFrag != newUfrag || uPwd != newPwd
336+
}
337+
338+
func (t *ICETransport) setRemoteCredentials(newUfrag, newPwd string) error {
339+
t.lock.Lock()
340+
defer t.lock.Unlock()
341+
342+
agent := t.gatherer.getAgent()
343+
if agent == nil {
344+
return errors.New("ICEAgent does not exist, unable to SetRemoteCredentials")
345+
}
346+
347+
return agent.SetRemoteCredentials(newUfrag, newPwd)
348+
}

0 commit comments

Comments
 (0)