WebRTC is hard

TL;DR:

Turn your TP-Link Tapo C100’s RTSP stream into a secure WebRTC stream. Using go2rtc on a Raspberry Pi with Docker, connected via Tailscale and a VPS (with Caddy as a reverse proxy), you can overcome browser streaming challenges.

Website screenshot

Some pigeon laid an egg on my building. I knew I needed to build a simple website to monitor the incubation of my pigeon egg and share it with the world. I thought it would be a straightforward project - well, guess what? It wasn't.

Streaming is not easy. Let me explain.

I have a webcam that I repurposed for this project - a TP-Link Tapo C100. Nothing fancy. The camera is decent enough, and the app works well, but I wanted to stream the video to a website to share it with the world.

I knew the camera could stream with RTSP, which is a standard protocol for streaming (to NVRs), so I started searching for a way to stream it to a website.

The problem

My first problem started when I found out that RTSP doesn't work in browsers, unless I used some outdated plugin or Flash with Internet Explorer, which turned out to be a dead end. Now I needed to transform the RTSP stream into a format suitable for website streaming, so I began searching for a solution. I found a few options, but ended up using go2rtc.

go2rtc screenshot

I intended to simply use the <video> tag to play the stream, so I considered using MP4. However, that approach did not work either, as Apple devices do not support HTTP progressive streams, which was unfortunate.

In my research I explored several options. I started with MSE (Media Source Extensions), a standard for streaming media in browsers, but discovered it wasn't implemented on iPhones. Then I considered HLS (HTTP Live Streaming), developed by Apple, only to find it OS-dependent. Finally, I looked into MJPEG, but it proved inefficient and demanded too much bandwidth.

It's a mess, and it's frustrating that browsers haven't agreed on a universal standard for streaming media, especially considering how massive the streaming industry has become. The following table from go2rtc illustrates the fragmented state of codec and protocol support across different platforms:

DeviceWebRTCMSEHTTPHLS
latencybestmediumbadbad
- Desktop Chrome 107+
- Desktop Edge
- Android Chrome 107+
H264
PCMU, PCMA
OPUS
H264, H265*
AAC, FLAC*
OPUS
H264, H265*
AAC, FLAC*
OPUS, MP3
no
Desktop FirefoxH264
PCMU, PCMA
OPUS
H264
AAC, FLAC*
OPUS
H264
AAC, FLAC*
OPUS
no
- Desktop Safari 14+
- iPad Safari 14+
- iPhone Safari 17.1+
H264, H265*
PCMU, PCMA
OPUS
H264, H265
AAC, FLAC*
no!H264, H265
AAC, FLAC*
iPhone Safari 14+H264, H265*
PCMU, PCMA
OPUS
no!no!H264, H265
AAC, FLAC*
macOSnononoH264, H265
AAC, FLAC*

My final option is WebRTC, a standard for streaming media in browsers. It's supported on all modern devices and is the best option available. But man, I found out that it's complicated to implement for simple tasks like this.

Setting up WebRTC

First, I set up a WebRTC server on a Raspberry Pi. I won't go into all the details, but here are the steps I followed for this project. After installing Docker on the Pi, I proceeded as follows:

I created a project directory and configuration:

mkdir bird-watch
cd bird-watch
vim docker-compose.yml

I added this docker-compose.yml configuration:

services:
  go2rtc:
    image: alexxit/go2rtc
    network_mode: host
    privileged: true
    restart: unless-stopped
    environment:
      - TZ=America/Mexico_City
    volumes:
      - "./config:/config"

Then, I started the container with the following command:

docker compose up -d

Then, I accessed the go2rtc web UI at http://<raspberry-pi-ip>:1984 and configured my stream:

go2rtc web ui screenshot

In the Config tab, I added my camera's stream configuration:

go2rtc config screenshot
streams:
  camera1: tapo://<your-tapo-cloud-password>@<your-tapo-camera-local-ip>

At this point, I could see the camera stream in the web UI:

go2rtc stream screenshot

Making it Public

Perfect! The stream works locally, but I needed to make it accessible from the internet.

My options:

  • Cloudflare Tunnel: Not viable because WebRTC is blocked and requires UDP forwarding
  • ngrok: Works but requires a paid plan
  • VPN + VPS combination: ✅ Selected solution - most flexible and cost-effective

I ended up using a VPN and a VPS, because it's the easiest way to forward the stream from the Raspberry Pi to the internet. Here's the setup I used:

VPN connection: I decided to use Tailscale as it's free and easy to set up.

  • I installed Tailscale on both the Raspberry Pi and a VPS
  • I connected both devices to the Tailscale network
Tailscale screenshot

VPS: I had one running from Hetzner. For less than $6/month, I can have a VPS in the USA that is close to my location.

  • I installed and configured go2rtc
  • I followed the same Docker setup steps as above

I needed to forward the stream from the Raspberry Pi to the VPS. I did this by adding the following to the Config tab:

streams:
    webrtc-go2rtc:  webrtc:ws://<tailscale-rpi-ip>:1984/api/ws?src=camera1
api:
  origin: "*"

To secure this ports I used Caddy running on the host, so I installed it with the official guide and configure the Caddy file with the following commands:

sudo vim /etc/caddy/Caddyfile

And added the following:

# The Caddyfile is an easy way to configure your Caddy web server.
#
# Unless the file starts with a global options block, the first
# uncommented line is always the address of your site.
#
# To use your own domain name (with automatic HTTPS), first make
# sure your domain's A/AAAA DNS records are properly pointed to
# this machine's public IP, then replace ":80" below with your
# domain name.

:80 {
	# Set this path to your site's directory.
	root * /usr/share/caddy

	# Enable the static file server.
	file_server

	# Another common task is to set up a reverse proxy:
	# reverse_proxy localhost:8080

	# Or serve a PHP site through php-fpm:
	# php_fastcgi localhost:9000
}

# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile

cam.toniotgz.com {
    reverse_proxy localhost:1984
}

Firewall: I opened the required ports on my VPS firewall.

  • 80/tcp: HTTP traffic
  • 443/tcp: HTTPS traffic
  • 8555/tcp+udp: WebRTC P2P connections
Hetzner screenshot

With this setup, anyone can now access the stream securely from the internet. The final website employs a simple video player based on the go2rtc WebRTC example; you can find the complete code in my GitHub repository.

Although it's not perfect, it works. It's more complex than expected, yet you can set up streaming in less than a weekend.

Website screenshot

P.S

Unfortunately, the egg won't survive since it needs incubation, and the parents have abandoned it. On the bright side, I now have a website to monitor the unincubated egg. :)