Skip to content

Commit

Permalink
Implement ICE Restarts
Browse files Browse the repository at this point in the history
Resolves #1251
  • Loading branch information
Sean-Der committed Jun 30, 2020
1 parent 4009686 commit f294146
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 34 deletions.
28 changes: 28 additions & 0 deletions examples/ice-restart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ice-restart
ice-restart demonstrates Pion WebRTC's ICE Restart abilities.

## Instructions

### Download ice-restart
This example requires you to clone the repo since it is serving static HTML.

```
mkdir -p $GOPATH/src/github.com/pion
cd $GOPATH/src/github.com/pion
git clone https://github.com/pion/webrtc.git
cd webrtc/examples/ice-restart
```

### Run ice-restart
Execute `go run *.go`

### Open the Web UI
Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection
and allow you to do an ICE Restart at anytime.

* `ICE Restart` is the button that causes a new offer to be made wih `iceRestart: true`.
* `ICE Connection States` will contain all the connection states the PeerConnection moves through.
* `ICE Selected Pairs` will print the selected pair every 3 seconds. Note how the uFrag/uPwd/Port change everytime you start the Restart process.
* `Inbound DataChannel Messages` containing the current time sent by the Pion process every 3 seconds.

Congrats, you have used Pion WebRTC! Now start building something cool
76 changes: 76 additions & 0 deletions examples/ice-restart/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<html>
<head>
<title>ice-restart</title>
</head>

<body>
<button onclick="window.doSignaling(true)"> ICE Restart </button><br />


<h3> ICE Connection States </h3>
<div id="iceConnectionStates"></div> <br />

<h3> ICE Selected Pairs </h3>
<div id="iceSelectedPairs"></div> <br />

<h3> Inbound DataChannel Messages </h3>
<div id="inboundDataChannelMessages"></div>
</body>

<script>
let pc = new RTCPeerConnection()
let dc = pc.createDataChannel('data')

dc.onopen = () => {
setInterval(function() {
let el = document.createElement('template')
let selectedPair = pc.sctp.transport.iceTransport.getSelectedCandidatePair()

el.innerHTML = `<div>
<ul>
<li> <i> Local</i> - ${selectedPair.local.candidate}</li>
<li> <i> Remote</i> - ${selectedPair.remote.candidate} </li>
</ul>
</div>`

document.getElementById('iceSelectedPairs').appendChild(el.content.firstChild);
}, 3000);
}

dc.onmessage = event => {
let el = document.createElement('p')
el.appendChild(document.createTextNode(event.data))

document.getElementById('inboundDataChannelMessages').appendChild(el);
}

pc.oniceconnectionstatechange = () => {
let el = document.createElement('p')
el.appendChild(document.createTextNode(pc.iceConnectionState))

document.getElementById('iceConnectionStates').appendChild(el);
}


window.doSignaling = iceRestart => {
pc.createOffer({iceRestart})
.then(offer => {
pc.setLocalDescription(offer)

return fetch(`/doSignaling`, {
method: 'post',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify(offer)
})
})
.then(res => res.json())
.then(res => pc.setRemoteDescription(res))
.catch(alert)
}

window.doSignaling(false)
</script>
</html>
81 changes: 81 additions & 0 deletions examples/ice-restart/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package main

import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/pion/webrtc/v3"
)

var peerConnection *webrtc.PeerConnection //nolint

func doSignaling(w http.ResponseWriter, r *http.Request) {
var err error

if peerConnection == nil {
if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil {
panic(err)
}

// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
})

// Send the current time via a DataChannel to the remote peer every 3 seconds
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
d.OnOpen(func() {
for range time.Tick(time.Second * 3) {
if err = d.SendText(time.Now().String()); err != nil {
panic(err)
}
}
})
})
}

var offer webrtc.SessionDescription
if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
panic(err)
}

if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
}

// Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)

answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
panic(err)
}

// Block until ICE Gathering is complete, disabling trickle ICE
// we do this because we only can exchange one signaling message
// in a production application you should exchange ICE Candidates via OnICECandidate
<-gatherComplete

response, err := json.Marshal(*peerConnection.LocalDescription())
if err != nil {
panic(err)
}

w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(response); err != nil {
panic(err)
}
}

func main() {
http.Handle("/", http.FileServer(http.Dir(".")))
http.HandleFunc("/doSignaling", doSignaling)

fmt.Println("Open http://localhost:8080 to access this demo")
panic(http.ListenAndServe(":8080", nil))
}
4 changes: 3 additions & 1 deletion gathering_complete_promise.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package webrtc

import "context"
import (
"context"
)

// GatheringCompletePromise is a Pion specific helper function that returns a channel that is closed when gathering is complete.
// This function may be helpful in cases where you are unable to trickle your ICE Candidates.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.12
require (
github.com/pion/datachannel v1.4.17
github.com/pion/dtls/v2 v2.0.1
github.com/pion/ice/v2 v2.0.0-rc.1
github.com/pion/ice/v2 v2.0.0-rc.3
github.com/pion/logging v0.2.2
github.com/pion/quic v0.1.1
github.com/pion/rtcp v1.2.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5
github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM=
github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA=
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/ice/v2 v2.0.0-rc.1 h1:1/5XKZx6Ioladykw5xp9/fCZG61pcmndTjY9bZhG0Fs=
github.com/pion/ice/v2 v2.0.0-rc.1/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8=
github.com/pion/ice/v2 v2.0.0-rc.3 h1:GvQ6nMGIGz7GltCUC9EU0m9JyQMan2vbifO4i8Y6T6A=
github.com/pion/ice/v2 v2.0.0-rc.3/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
Expand Down
26 changes: 15 additions & 11 deletions icegatherer.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,22 @@ func (g *ICEGatherer) Gather() error {
return err
}

onLocalCandidateHdlr := func(*ICECandidate) {}
if hdlr, ok := g.onLocalCandidateHdlr.Load().(func(candidate *ICECandidate)); ok && hdlr != nil {
onLocalCandidateHdlr = hdlr
}

onGatheringCompleteHdlr := func() {}
if hdlr, ok := g.onGatheringCompleteHdlr.Load().(func()); ok && hdlr != nil {
onGatheringCompleteHdlr = hdlr
}

g.lock.Lock()
agent := g.agent
g.lock.Unlock()

g.setState(ICEGathererStateGathering)
if err := agent.OnCandidate(func(candidate ice.Candidate) {
onLocalCandidateHdlr := func(*ICECandidate) {}
if hdlr, ok := g.onLocalCandidateHdlr.Load().(func(candidate *ICECandidate)); ok && hdlr != nil {
onLocalCandidateHdlr = hdlr
}

onGatheringCompleteHdlr := func() {}
if hdlr, ok := g.onGatheringCompleteHdlr.Load().(func()); ok && hdlr != nil {
onGatheringCompleteHdlr = hdlr
}

if candidate != nil {
c, err := newICECandidateFromICE(candidate)
if err != nil {
Expand Down Expand Up @@ -194,7 +194,11 @@ func (g *ICEGatherer) GetLocalParameters() (ICEParameters, error) {
return ICEParameters{}, err
}

frag, pwd := g.agent.GetLocalUserCredentials()
frag, pwd, err := g.agent.GetLocalUserCredentials()
if err != nil {
return ICEParameters{}, err
}

return ICEParameters{
UsernameFragment: frag,
Password: pwd,
Expand Down
46 changes: 46 additions & 0 deletions icetransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,23 @@ func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role *
return nil
}

// restart is not exposed currently because ORTC has users create a whole new ICETransport
// so for now lets keep it private so we don't cause ORTC users to depend on non-standard APIs
func (t *ICETransport) restart() error {
t.lock.Lock()
defer t.lock.Unlock()

agent := t.gatherer.getAgent()
if agent == nil {
return errors.New("ICEAgent does not exist, unable to restart ICETransport")
}

if err := agent.Restart(t.gatherer.api.settingEngine.candidates.UsernameFragment, t.gatherer.api.settingEngine.candidates.Password); err != nil {
return err
}
return t.gatherer.Gather()
}

// Stop irreversibly stops the ICETransport.
func (t *ICETransport) Stop() error {
t.lock.Lock()
Expand Down Expand Up @@ -300,3 +317,32 @@ func (t *ICETransport) collectStats(collector *statsReportCollector) {

collector.Collect(stats.ID, stats)
}

func (t *ICETransport) haveRemoteCredentialsChange(newUfrag, newPwd string) bool {
t.lock.Lock()
defer t.lock.Unlock()

agent := t.gatherer.getAgent()
if agent == nil {
return false
}

uFrag, uPwd, err := agent.GetRemoteUserCredentials()
if err != nil {
return false
}

return uFrag != newUfrag || uPwd != newPwd
}

func (t *ICETransport) setRemoteCredentials(newUfrag, newPwd string) error {
t.lock.Lock()
defer t.lock.Unlock()

agent := t.gatherer.getAgent()
if agent == nil {
return errors.New("ICEAgent does not exist, unable to SetRemoteCredentials")
}

return agent.SetRemoteCredentials(newUfrag, newPwd)
}
Loading

0 comments on commit f294146

Please sign in to comment.