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.

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.

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:
Device | WebRTC | MSE | HTTP | HLS |
---|---|---|---|---|
latency | best | medium | bad | bad |
- 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 Firefox | H264 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* |
macOS | no | no | no | H264, 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:

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

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:

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

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 traffic443/tcp
: HTTPS traffic8555/tcp+udp
: WebRTC P2P connections

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.

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. :)