DEV Community

PiterDev
PiterDev

Posted on • Updated on

Golang WebRTC. How to use Pion 🌐Remote Controller

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. The

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
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

Now let's import pion

import (
    ...
    "github.com/pion/webrtc/v3"
)
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (4)

Collapse
 
nigel447 profile image
nigel447

time well spent reading this, look fwd to second part +1

Collapse
 
ferradasdiego profile image
ferradasdiego

Waiting the second part 🔥🔥

Collapse
 
piterweb profile image
PiterDev

Tell me if you want a second part explaining the browser side implementation and i will be releasing soon

Collapse
 
santoshmore088 profile image
santosh more

yes please do