Golang WebRTC. How to use Pion 🌐Remote Controller

Piterdev
CodeX
Published in
5 min read1 day ago

This article also has a Spanish version in dev.to if you want to check out

Why should i choose Go for my WebRTC app 🤷‍♂️ ?

WebRTC and Go is a very powerfull combination, you can deploy small binaries on any Go supported OS. And because of being compiled is faster than many other languages, so it is ideal if you want to proccess realtime comms like WebRTC.

(At least it is how i have seen this before creating a project)

What is Pion WebRTC ?

Pion is a WebRTC implementation in pure go so it is very helpfull if you want smaller compile times, smaller binaries and better cross-platform than other options that uses CGo.

Understanding WebRTC Peerconnections

Do you know how WebRTC and all of its parts works? Now i will explain you a simplified version of it limited to the frame of the tutorial.

ICE (Interactive Connectivity Establishment)
This is a framework used by WebRTC, the main function is giving candidates (possible routes or IPs) to successfully connect two devices even if they are behind a firewall or not exposed by a public adress using STUN/TURN

STUN
This is protocol and a type of server used by WebRTC which is perfect for handling connections that are not behind a restrictive NAT. This is because some NAT depending on the configuration will not allow to resolve the ICE candidates.

Is very easy to start playing with them since there are a lot of public STUN servers available.

TURN
TURN is like STUN but better. The main difference is that can bypass the NAT restrictions that make STUN not working.There are also public TURN servers available and some companies offer them for free.

Both TURN and STUN can be self hosted, the most popular project i have found is coturn

Channels
Bidirectional flow of data provided by WebRTC that goes by an UDP/TCP connection and you can subscribe and write to it. Can be datachannels or mediachannels.

SDP
Is a format to describe the connection: channels to be open, codecs, encoding, …

Signalling
Method chosen to send SPDs and ICEcandidates between peers to stablish the connection. Can be http requests, manual copy/paste, websockets, …

Code Example for a Client peer 📘

Now we are going to explore some code so I am going to extract an simplified example version from the codebase of Remote Controller github repository.

RemoteController app showcase

Remote Controller is my personal project that tries to be an open alternative to Steam Remote Play (A service to play local co-op games online using P2P connections)

The main function of our example will be connecting to a WebRTC server peer (calling server the one who initialices the connection) and send some numbers using a datachannel and listen to other datachannel.

At first i will declare a channel variable and a normal string variable to make a generic way of signalling (in the real case of the app is used manual copy/paste based on the idea requirements but can be implemented in different ways)

var offerEncodedWithCandidates string //OfferFromServer
var answerResponse := make(chan string) //AnswerFromClient

and then we are going to add an utilitary function that made our signals base64 and compressed (this is optional)

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package signal contains helpers to exchange the SDP session
// description between examples.
package <package>
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"io"
)
// Allows compressing offer/answer to bypass terminal input limits.
const compress = true
// signalEncode encodes the input in base64
// It can optionally zip the input before encoding
func signalEncode(obj interface{}) string {
b, err := json.Marshal(obj)
if err != nil {
panic(err)
}
if compress {
b = signalZip(b)
}
return base64.StdEncoding.EncodeToString(b)
}
// signalDecode decodes the input from base64
// It can optionally unzip the input after decoding
func signalDecode(in string, obj interface{}) {
b, err := base64.StdEncoding.DecodeString(in)
if err != nil {
panic(err)
}
if compress {
b = signalUnzip(b)
}
err = json.Unmarshal(b, obj)
if err != nil {
panic(err)
}
}
func signalZip(in []byte) []byte {
var b bytes.Buffer
gz := gzip.NewWriter(&b)
_, err := gz.Write(in)
if err != nil {
panic(err)
}
err = gz.Flush()
if err != nil {
panic(err)
}
err = gz.Close()
if err != nil {
panic(err)
}
return b.Bytes()
}
func signalUnzip(in []byte) []byte {
var b bytes.Buffer
_, err := b.Write(in)
if err != nil {
panic(err)
}
r, err := gzip.NewReader(&b)
if err != nil {
panic(err)
}
res, err := io.ReadAll(r)
if err != nil {
panic(err)
}
return res
}

Now let’s import pion

import (

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

And now we will do the initial setup

// ICECandidates slice
candidates := []webrtc.ICECandidateInit{}
// Config struct with the STUN servers
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{{
URLs: []string{"stun:stun.l.google.com:19305", "stun:stun.l.google.com:19302"},
},},
}

// Creation of the peer connection
peerConnection, err := webrtc.NewAPI().NewPeerConnection(config)
if err != nil {
panic(err)
}
// Peerconnection close handling
defer func() {
if err := peerConnection.Close(); err != nil {
fmt.Printf("cannot close peerConnection: %v\n", err)
}
}()
// Register data channel creation handling
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
if d.Label() == "numbers" {
d.OnOpen(func() {
// Send Number 5 by the datachannel "numbers"
err := d.SendText("5")
if err != nil {
panic(err)
}
})
return
}
if d.Label() == "other" {
gamepadChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
// Listening for channel called "other"
fmt.Println(msg.Data)
})
}
})

// Listening for ICECandidates
peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) {
// When no more candidate available
if c == nil {
answerResponse <-signalEncode(*peerConnection.LocalDescription()) + ";" + signalEncode(candidates)
return
}
candidates = append(candidates, (*c).ToJSON())
})

// Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
fmt.Printf("Peer Connection State has changed: %s\n", s.String())
if s == webrtc.PeerConnectionStateFailed {
peerConnection.Close()
}
})

// From the offer and candidates encoded we separate them
offerEncodedWithCandidatesSplited := strings.Split(offerEncodedWithCandidates, ";")
offer := webrtc.SessionDescription{}

signalDecode(offerEncodedWithCandidatesSplited[0], &offer)

var receivedCandidates []webrtc.ICECandidateInit
signalDecode(offerEncodedWithCandidatesSplited[1], &receivedCandidates)

// Then we set our remote description
if err := peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
}
// After setting the remote description we add the candidates
for _, candidate := range receivedCandidates {
if err := peerConnection.AddICECandidate(candidate); err != nil {
panic(err)
}
}
// Create an answer to send to the other process
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
panic(err)
}
// Infinite loop to block the thread
for {}

With this code you can start implementing your own WebRTC service. Note that if you use a WebRTC server from JS (browser/Node/Deno/…) if you want to encode/decode your signals you probably need to implement it using third party libraries. In my case I made a simple port from Go to WASM to use it from the browser or other platforms, you can find it here . You only need to compile it using go tooling or tinygo or just use the wasm from the RemoteController repo

Sources of information:

- https://web.dev/articles/webrtc-basics
- https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
- https://github.com/pion/webrtc/tree/master/examples

--

--