Golang WebRTC. Como usar Pion 🌐Remote Controller
Tags: webdev, webrtc, go, spanish
Published: 2024-07-14 08:47:02

## ¿ Porqué debería elegir Go para crear una aplicación WebRTC 🤷‍♂️?

WebRTC y Go es una combinación poderosa, puedes desplegar pequeños binarios en cualquier sistema operativo soportado por el compilador de Go. Y por el hecho de ser compilado acostumbra ser más rápido que muchos otros lenguajes, así que es ideal si quieres procesar comunicaciones en tiempo real como WebRTC.

(Al menos este es mi punto de vista después de crear un proyecto usando estas 2 tecnologías)

## ¿ Que es Pion WebRTC ?

Pion es una implementación de WebRTC en Go puro (aunque algunás parte más "externas" si que pueden depender de CGo dependiendo del sistema operativo), por ello es muy útil si quieres tiempos de compilación reducidos, binarios más pequeños y mejor soporte multi plataforma que si usase CGo.

## Entendiendo las conexiones extremo a extremo WebRTC

¿ Sabes como funciona WebRTC y todas sus partes ? Ahora te explicaré una versión simplificada de ello limitado al contenido de este tutorial.

### ICE (Interactive Connectivity Establishment)

Es un entorno de trabajo usado por WebRTC, la función principal de este es dar candidatos (posibles rutas o IPs) para que 2 dispositivos o más se puedan conectar incluso si están detrás de un firewall o no están expuestas a internet. Esto se consigue haciendo uso de STUN y TURN.

### STUN

Es un protocolo y un tipo de servidor usado por WebRTC que es adecuado para manejar conexiones que no estén detrás de un NAT restrictivo. Esto es porque algunos NAT depende de como se hallen configurados no permitirán que se resuelvan los ICE candidates.

Es muy fácil empezar a experimentar con ellos ya que existen muchas listas de [servidores STUN públicos](https://github.com/pradt2/always-online-stun/) disponibles.

### TURN

TURN es como STUN pero mejor. La principal diferencia es que puede evadir las restricciones de los NAT que hacen a STUN no funcionar correctamente. Además también existen servidores TURN públicos y algunas compañias los ofrecen gratuitamente.

Ambos TURN y STUN pueden ser auto-alojados (lo que se conoce comúnmente como self-hosting), el proyecto más popular que he encontrado es [coturn](https://github.com/coturn/coturn)

### Canales (Channels)

Son flujos bidireccionales de datos proporcionados por WebRTC que pueden usar conexiones UDP o TCP. A estos te puedes suscribir o escribir en ellos.

Además generalemente existen 2 tipos: Datachannels(datos binarios) y Mediachannels(video/audio).

### SDP

Es un formato que describe la conexión: canales que se van a usar, codecs, encoding, ...

### Señalización

Método escogido para intercambiar los SDPs y los ICEcandidates entre extremos para establecer una conexión. Pueden usar peticiones http, copiar y pegar manuales, websockets, ...

## Ejemplo de código para el extremo de cliente 📘

Ahora vamos a explorar un poco de código, este es un ejemplo que está extraído y simplificado de la base de código del repositorio de Github de "Remote Controller"

 ![RemoteController app showcase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lw2h6hr696cf8nl7pm55.png)

[Remote Controller](https://github.com/PiterWeb/RemoteController) es mi proyecto personal que intenta ser una alternativa abierta a Steam Remote Play (un servicio para jugar juegos cooperativos locales de forma online usando conexiones de extremo a extremo [P2P])

La función principal de este ejemplo será la de conectarnos a un extremo WebRTC que actue como servidor (llamando servidor a aquel que inicia la conexión) y mandando números a través de un Datachannel y escuchando los datos recividos en otro Datachannel.

Primero declararé la variable del Datachannel y una variable de string como una forma genérica de señalización (en el caso real de la aplicación se usa un copia/pega manual basado en las necesidades de la idea de mi producto pero puede ser implementado de muchas otras maneras)

```go
var offerEncodedWithCandidates string //OfferFromServer
var answerResponse := make(chan string) //AnswerFromClient
```
y después vamos a añadir una función que nos será de utilidad para convertir a base64 y comprimir nuestras "señales" (aunque es opcional)

```go
// 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
}

```

Ahora vamos a importar Pion

```go
import (
    ...
    "github.com/pion/webrtc/v3"
)
```

Y ahora haremos la inicialización

```go

// slice de ICECandidates 
candidates := []webrtc.ICECandidateInit{}

// Config struct con los servidores STUN
config := webrtc.Configuration{
		ICEServers: []webrtc.ICEServer{
			{
				URLs: []string{"stun:stun.l.google.com:19305", "stun:stun.l.google.com:19302"},
			},
		},
	}

// Creación de la conexión
peerConnection, err := webrtc.NewAPI().NewPeerConnection(config)
if err != nil {
		panic(err)
}

// Manejo del cerrado de conexión
defer func() {
		if err := peerConnection.Close(); err != nil {
			fmt.Printf("cannot close peerConnection: %v\n", err)
		}
}()

// Registrar los Datachannels y especificar su funcionamiento
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {

     if d.Label() == "numbers" {

	   d.OnOpen(func() {

         // Mandar número 5 por el 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)

      })

   }
	

})

// Escuchar por los 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())

})

// Esto notificará cuando un extremo se ha conectado/desconectado
peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
	fmt.Printf("Peer Connection State has changed: %s\n", s.String())

	if s == webrtc.PeerConnectionStateFailed {

		peerConnection.Close()

	}
})

// Separamos la oferta y los candidatos codificados

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)
		}
	}

	// Crea una respuesta para mandarla al otro extremo
	answer, err := peerConnection.CreateAnswer(nil)
	if err != nil {
		panic(err)
	}

	// Guarda la descripción local, y empieza a escuchar las conexiones UDP
	err = peerConnection.SetLocalDescription(answer)
	if err != nil {
		panic(err)
	}

  // Bucle infinito para bloquear el hilo
  for {}
```

Con este código puedes empezar a implementar tu propio servicio WebRTC. Hay que tener en cuenta que si usar la función opcional que codifica y comprime las "señales" debes implementarlo también en el otro extremo, en el caso que el otro extremo sea JS (ya sea navegador, Node , Deno, ...) deberás usar librerías de terceros. En mi caso he hecho un port simple de Go a WASM para usar desde cualquier plataforma que soporte WebAssembly, puedes encontrarlo [aquí](https://gist.github.com/PiterWeb/47e77e9011fab02af5c8778569c7686b). Solo necesitas compilarlo usando el compilador de Go o con TinyGo o simplemente usar el [WASM del repositorio de Remote Controller]((https://github.com/PiterWeb/RemoteController/blob/main/frontend/static/wasm/signal.wasm)
)

Fuentes de información:

- 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

Esta es una tradución de un artículo existente, el artículo original es el siguiente, 
[Golang WebRTC. How to use Pion 🌐Remote Controller](https://dev.to/piterweb/golang-webrtc-how-to-use-pion-remote-controller-1j00)