William Mulianto

Streaming IP Cameras to the Browser with RTSP, WebRTC, and DDNS

· 6 min read

Most IP cameras speak RTSP. Browsers don’t. Bridging that gap is surprisingly annoying if you don’t know the moving parts. Here’s how I set it up — what RTSP and WebRTC actually are, how RTSPtoWeb connects them, and how DDNS lets you access it all remotely.

What Is RTSP

RTSP (Real-Time Streaming Protocol) is how most IP cameras expose their video feed. You get a URL like:

rtsp://192.168.1.100:554/stream1

You can open this in VLC and it works. But browsers can’t play RTSP natively — there’s no built-in support for it. So if you want to view your camera in a web app or dashboard, you need something in between.

What Is WebRTC

WebRTC (Web Real-Time Communication) is a browser-native protocol designed for real-time video, audio, and data. It’s what powers video calls in Google Meet, Zoom’s web client, and similar apps.

The key difference from regular video streaming (like HLS) is latency. HLS typically has 5–30 seconds of delay. WebRTC is sub-second — you see things almost in real time. For camera feeds, that matters.

WebRTC also handles NAT traversal, meaning it can establish connections between devices behind routers — but this is where it gets tricky.

STUN and TURN Servers

WebRTC needs to figure out how two devices can talk to each other, even when both are behind NATs or firewalls. It does this using STUN and TURN servers.

STUN (Session Traversal Utilities for NAT) helps a device discover its own public IP address and port. When a WebRTC connection starts, the browser asks a STUN server “what’s my public address?” and uses that info to establish a direct peer-to-peer connection. STUN is lightweight — it’s just a lookup. Google runs free STUN servers (stun:stun.l.google.com:19302) that work for most cases.

TURN (Traversal Using Relays around NAT) is the fallback. When a direct connection isn’t possible — strict firewalls, symmetric NAT, corporate networks — TURN relays the media traffic through a server. This means all video data flows through your TURN server, which uses bandwidth and adds latency. But without it, some clients simply can’t connect.

In practice, STUN works for most home networks. TURN is needed for maybe 10–20% of connections, but those are the ones that break without it.

The Port Problem with TURN

If you’re running a TURN server (like coturn) on AWS or any cloud provider, you’ll hit a common gotcha — UDP ports.

TURN uses a listening port (default 3478) for the initial connection, but then allocates media relay ports from a range — typically UDP 49152–65535. If your security group or firewall only opens port 3478, the initial handshake works but the actual video stream fails silently.

On AWS, your security group needs:

  • TCP/UDP 3478 — TURN listening port
  • UDP 49152–65535 — media relay port range

That’s a lot of open UDP ports, and it surprises people the first time. You can narrow the range in your TURN server config, but you still need a decent chunk of ports open for concurrent connections.

# coturn config example
listening-port=3478
min-port=49152
max-port=65535

This is one of those things that works perfectly in local testing and then breaks in production because of firewall rules. If your WebRTC stream connects but shows no video, check your UDP ports first.

RTSPtoWeb — The Bridge

RTSPtoWeb takes an RTSP stream from your camera and converts it into formats the browser can play — WebRTC, MSE (Media Source Extensions), or HLS. It’s written in Go, fully native, no FFmpeg or GStreamer required.

The setup is straightforward. You give it your RTSP URLs, and it exposes a web interface where you can view the streams. You can also embed the streams in your own web app using the API.

Running it in Docker:

rtsptoweb:
  image: ghcr.io/deepch/rtsptoweb:latest
  restart: unless-stopped
  ports:
    - "8083:8083"
  volumes:
    - ./config.json:/config/config.json

A basic config to add a camera:

{
  "server": {
    "http_port": ":8083"
  },
  "streams": {
    "front-door": {
      "name": "Front Door",
      "channels": {
        "0": {
          "url": "rtsp://admin:password@192.168.1.100:554/stream1",
          "on_demand": true
        }
      }
    }
  }
}

Once running, you can view the stream at http://localhost:8083 or embed it in your own page using the WebRTC or MSE endpoints.

How the Protocols Connect

The flow looks like this:

Camera (RTSP) → RTSPtoWeb → Browser (WebRTC)
  1. Your IP camera broadcasts an RTSP stream on your local network
  2. RTSPtoWeb connects to that RTSP stream and decodes it
  3. When a browser requests the feed, RTSPtoWeb re-packages it as WebRTC (or MSE/HLS)
  4. The browser plays it natively with sub-second latency

The on_demand option is important — it means RTSPtoWeb only pulls from the camera when someone is actually watching. Without it, it keeps a constant connection open, which wastes bandwidth and camera resources.

Accessing It Remotely with DDNS

This all works great on your local network. But what if you want to check your cameras from outside — your phone, a different location?

Your home internet connection most likely has a dynamic IP address. Your ISP changes it periodically, sometimes every few hours. So you can’t just bookmark your public IP and expect it to work tomorrow.

This is where DDNS (Dynamic DNS) comes in.

How DDNS Works

  1. You register a hostname (like myhome.ddns.net) with a DDNS provider
  2. A small client runs on your network — either on your router, a Raspberry Pi, or a Docker container
  3. The client periodically checks your current public IP and updates the DNS record
  4. When your ISP changes your IP, the DDNS client updates the record within minutes
  5. Your hostname always points to your current IP

Some routers have DDNS clients built in — just enter your provider credentials in the router settings. Otherwise, you can run a DDNS updater as a container.

Port Forwarding

DDNS gets traffic to your router, but you still need to tell your router where to send it. You’ll need to forward the relevant port (e.g., 8083) to the machine running RTSPtoWeb.

A better approach is to put everything behind a reverse proxy like Caddy with HTTPS, and only forward ports 80 and 443. That way your camera feed is encrypted and you’re not exposing random ports to the internet.

cameras.example.com {
    reverse_proxy localhost:8083
}

The Full Picture

Camera (RTSP, local network)

RTSPtoWeb (converts to WebRTC)

Caddy (reverse proxy, HTTPS)

DDNS (hostname → dynamic IP)

Browser (anywhere)

The whole stack is lightweight. RTSPtoWeb, Caddy, and a DDNS updater can all run on a single machine or even a Raspberry Pi. No cloud subscriptions, no third-party camera apps — just your cameras, your server, your data.

Related