Skip to content

scramble45/Yamauba

Repository files navigation

Yamauba

Self-hosted screen sharing over WebRTC with Socket.IO relay fallback.

Why Yamauba

Most screen sharing tools force a tradeoff:

  • pure peer-to-peer design, which is fast but brittle across NAT, firewalls, VPNs, and locked-down networks
  • pure server relay design, which is reliable but heavier, higher latency, and more expensive to run

Yamauba is built around both paths on purpose.

The normal path is direct WebRTC media with ICE negotiation and TURN support. When that works, the host sends media straight to the viewer with low latency and without pushing the full stream through the app server. That is the efficient path and the one you want for real remote use.

The second path is application-mediated relay over Socket.IO. It is not as efficient as WebRTC, but it keeps the product usable when the network will not allow a successful peer connection or when TURN is unavailable. That fallback matters because many real networks do not behave like a simple home lab:

  • symmetric NAT can prevent direct peer connectivity
  • enterprise firewalls often block or interfere with UDP
  • hotel, campus, and guest Wi-Fi networks can be restrictive or unstable
  • reverse proxies and tunnels can expose the app URL without exposing TURN relay ports

That is the practical difference in this project. Yamauba is not just "screen sharing in a browser." It is a self-hosted transport stack that tries the efficient media path first, then degrades to a server-mediated path instead of simply failing.

Technicals

Yamauba has three distinct layers:

  1. Control plane Express serves the UI and Socket.IO handles room state, signaling, chat, and fallback relay events.
  2. Media plane The host captures screen, window, tab, or camera/video-device media with getDisplayMedia() and getUserMedia().
  3. Connectivity plane ICE gathers candidates and attempts direct connectivity with STUN and TURN assistance.

In WebRTC mode, the host and viewer exchange SDP and ICE candidates through Socket.IO. If the peers can connect directly, media flows peer-to-peer. If they cannot connect directly but both can reach TURN, TURN relays the media and still preserves the WebRTC session model. This is how the app gets through many NAT and firewall scenarios: the browsers make outbound connections to STUN/TURN infrastructure, gather reachable candidates, and negotiate the best path they can establish.

If that fails, Yamauba can still keep the session alive with its own relay path. In relay mode, the host renders frames into an application-controlled stream and sends them through the server to the viewer. That does not "punch through" the network in the same way WebRTC plus TURN does. Instead, it works because both clients only need normal outbound connectivity to the app URL, which is often the only thing available behind strict firewalls or via a single HTTPS tunnel.

Overview

Yamauba has two delivery modes:

  • WebRTC: preferred path, low latency, works best when TURN is available.
  • Relay: fallback path, always app-mediated, higher latency, video-only.

Connection flow:

Host -> getDisplayMedia/getUserMedia -> WebRTC -> Viewer
Host -> canvas JPEG relay -> Socket.IO -> Viewer

Deployment Modes

Choose one:

  1. Local only Run on localhost. No public URL required.
  2. Public app URL, relay fallback acceptable Expose the app over HTTPS or Cloudflare Tunnel. Remote users can load the app. WebRTC may fall back to relay if TURN is not reachable.
  3. Reliable remote WebRTC Expose the app over HTTPS and provide TURN, either with self-hosted coturn or Cloudflare TURN.

Rule:

  • One public URL is enough for the app UI and signaling.
  • It is not enough for TURN unless you use a managed TURN provider.

Quick Start

Local Node.js run:

npm install
npm start

Open http://localhost:3000.

Local Docker run:

cp .env.example .env
docker compose up -d

Change PASSWORD before exposing the app to any network.

Required Network Reachability

Remote deployment has two separate network paths:

  1. App path Browser -> HTTPS app URL -> Yamauba UI + Socket.IO signaling
  2. ICE path Browser -> STUN/TURN -> WebRTC connectivity

If only the app path is public, the app still loads, but remote WebRTC may fail and the session may fall back to relay mode.

Docker

The repo already includes a working self-hosted Docker setup:

  • yamauba app container
  • coturn container
  • host networking
  • TURN env wiring

The standard deployment usually does not require changes to Dockerfile.

Important Environment Variables

Variable Purpose
APP_PORT App listen port
PUBLIC_BASE_URL URL shown and copied in the host UI
PASSWORD Login password
SESSION_SECRET Session signing secret
NODE_ENV Set to production in production
TURN_PUBLIC_HOST Public hostname or IP used in ICE URLs
TURN_EXTERNAL_IP Public relay IP coturn advertises when behind NAT or Docker
TURN_USER TURN username
TURN_PASS TURN password

Default Docker Behavior

docker-compose.yml currently does this:

  • runs both services with network_mode: host
  • passes TURN settings into the app
  • starts coturn with:
    • --listening-port=3478
    • --min-port=49152
    • --max-port=49252
    • --relay-ip=0.0.0.0
    • optional --external-ip=$TURN_EXTERNAL_IP

Ports

If you self-host coturn and want remote WebRTC to work, these ports must be reachable from the public internet:

  • 3000/tcp for the Yamauba app, or your reverse proxy port such as 443/tcp
  • 3478/tcp and 3478/udp for STUN/TURN
  • 5349/tcp and 5349/udp for TURNS if you use turns:
  • 49152-49252/udp for TURN relay traffic

If only the app URL is public and the TURN ports are not, users can still open the app, but remote WebRTC will often fail and the session may fall back to relay mode.

Port Protocol Purpose
3000 TCP Yamauba app
3478 TCP + UDP STUN + TURN
5349 TCP + UDP TURNS over TLS
49152-49252 UDP TURN relay media range

For self-hosted coturn, browsers must be able to reach:

  • TURN_PUBLIC_HOST:3478
  • TURN_PUBLIC_HOST:5349 if using turns:
  • TURN_EXTERNAL_IP on the relay UDP range

If coturn is behind NAT, Docker, or a proxy, TURN_EXTERNAL_IP must be the public relay IP, not the internal container or host IP.

If Host Networking Is Not Available

Expose ports manually:

services:
  yamauba:
    ports:
      - "3000:3000"

  coturn:
    ports:
      - "3478:3478/tcp"
      - "3478:3478/udp"
      - "5349:5349/tcp"
      - "5349:5349/udp"
      - "49152-49252:49152-49252/udp"

In that setup, TURN_EXTERNAL_IP is mandatory.

Single Public URL vs TURN

Use this rule:

  • https://share.example.com or a Cloudflare Tunnel URL serves the app UI and signaling.
  • TURN still needs direct browser reachability on STUN/TURN ports unless you use Cloudflare TURN.

If you want one public app URL and reliable WebRTC from anywhere, use one of these:

  • self-hosted coturn exposed publicly
  • Cloudflare TURN credentials

If you use Cloudflare TURN, you do not need to expose your own coturn instance publicly.

Recommended Setups

Local Development

Set:

  • APP_PORT
  • PASSWORD
  • SESSION_SECRET

Leave TURN on localhost.

Public App URL + Self-Hosted TURN

Set:

  • PUBLIC_BASE_URL
  • TURN_PUBLIC_HOST
  • TURN_EXTERNAL_IP
  • TURN_USER
  • TURN_PASS

Public App URL + Cloudflare TURN

Set:

  • PUBLIC_BASE_URL
  • cloudflare.enabled=true
  • cloudflare.tokenId
  • cloudflare.apiToken

Do not expose your own coturn instance publicly in this setup.

HTTPS

getDisplayMedia() requires a secure context. Use HTTPS or localhost.

Plain HTTP will break screen sharing.

Example nginx reverse proxy:

server {
    listen 443 ssl;
    server_name your.domain.com;

    ssl_certificate     /etc/letsencrypt/live/your.domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400;
    }
}

Share Link Behavior

The host UI shows and copies a room URL using this priority:

  1. PUBLIC_BASE_URL
  2. server.publicBaseUrl in config.json
  3. active Cloudflare Tunnel URL
  4. browser origin

This lets the host choose a custom public URL, a tunnel URL, or origin fallback.

Configuration

Create config.json only if needed. All fields are optional.

{
  "server": {
    "port": 3000,
    "password": "changeme",
    "sessionSecret": "a-long-random-string-32-chars-min",
    "publicBaseUrl": "https://share.example.com"
  },
  "webrtc": {
    "enabled": true,
    "timeout": 5000,
    "preferRelay": false,
    "fallbackIceServers": [
      { "urls": "stun:stun.cloudflare.com:3478" },
      { "urls": "stun:stun.l.google.com:19302" }
    ]
  },
  "relay": {
    "enabled": true,
    "fps": 30,
    "quality": 0.85,
    "maxWidth": 1920,
    "maxHeight": 1080
  },
  "socket": {
    "pingTimeout": 60000,
    "pingInterval": 3000,
    "maxHttpBufferSize": 100000000
  },
  "cloudflare": {
    "enabled": false,
    "tokenId": "",
    "apiToken": "",
    "ttl": 86400
  },
  "tunnel": {
    "enabled": false,
    "showQR": true
  }
}

Use environment variables for secrets in production.

TURN

TURN is required for reliable remote WebRTC when direct peer-to-peer connectivity fails.

Without TURN:

  • home networks may still work
  • strict NAT and corporate networks often fail
  • the app may fall back to relay mode

Self-Hosted coturn

Minimal turnserver.conf:

listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
realm=your.domain.com
server-name=your.domain.com
user=yamauba:yourpassword
cert=/etc/letsencrypt/live/your.domain.com/fullchain.pem
pkey=/etc/letsencrypt/live/your.domain.com/privkey.pem
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255

Do not remove the denied-peer-ip rules.

Cloudflare TURN

If you do not want to run your own TURN server:

{
  "cloudflare": {
    "enabled": true,
    "tokenId": "your-cloudflare-turn-token-id",
    "apiToken": "your-cloudflare-api-token",
    "ttl": 86400
  }
}

Public STUN Only

Public STUN is not sufficient for production. It helps only with simple NAT traversal.

Production Checklist

  1. Set a strong PASSWORD.
  2. Set a strong SESSION_SECRET.
  3. Set NODE_ENV=production.
  4. Expose the app over HTTPS.
  5. Make sure WebSocket upgrade works.
  6. Provide TURN, either self-hosted or Cloudflare TURN.
  7. If self-hosting TURN behind NAT or Docker, set TURN_EXTERNAL_IP correctly.
  8. Set PUBLIC_BASE_URL if the host should copy a specific public URL.

Features

  • Screen, window, tab, and camera/video-device sharing
  • Optional microphone audio
  • WebRTC with relay fallback
  • Room URL and code sharing
  • Recording on the viewer side
  • Text chat with per-room nicknames
  • Mobile and desktop UI

Browser Support

Feature Chrome Firefox Safari iOS Safari
Screen sharing (host) Yes Yes Yes, 13+ No
Watch stream Yes Yes Yes Yes
Recording Yes Yes No No
WebRTC Yes Yes Yes Yes

iOS Safari can watch streams via relay mode but cannot host screen sharing.

Requirements

  • Node.js 24+
  • HTTPS for any non-localhost deployment
  • TURN for reliable cross-network WebRTC
  • cloudflared in PATH if using Cloudflare Tunnel

License

MIT

About

Self-hosted screen sharing over WebRTC with Socket.IO relay fallback.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors