Self-hosted screen sharing over WebRTC with Socket.IO relay fallback.
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.
Yamauba has three distinct layers:
- Control plane Express serves the UI and Socket.IO handles room state, signaling, chat, and fallback relay events.
- Media plane
The host captures screen, window, tab, or camera/video-device media with
getDisplayMedia()andgetUserMedia(). - 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.
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
Choose one:
- Local only
Run on
localhost. No public URL required. - 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.
- 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.
Local Node.js run:
npm install
npm startOpen http://localhost:3000.
Local Docker run:
cp .env.example .env
docker compose up -dChange PASSWORD before exposing the app to any network.
Remote deployment has two separate network paths:
- App path Browser -> HTTPS app URL -> Yamauba UI + Socket.IO signaling
- 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.
The repo already includes a working self-hosted Docker setup:
yamaubaapp containercoturncontainer- host networking
- TURN env wiring
The standard deployment usually does not require changes to Dockerfile.
| 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 |
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
If you self-host coturn and want remote WebRTC to work, these ports must be reachable from the public internet:
3000/tcpfor the Yamauba app, or your reverse proxy port such as443/tcp3478/tcpand3478/udpfor STUN/TURN5349/tcpand5349/udpfor TURNS if you useturns:49152-49252/udpfor 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:3478TURN_PUBLIC_HOST:5349if usingturns:TURN_EXTERNAL_IPon 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.
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.
Use this rule:
https://share.example.comor 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.
Set:
APP_PORTPASSWORDSESSION_SECRET
Leave TURN on localhost.
Set:
PUBLIC_BASE_URLTURN_PUBLIC_HOSTTURN_EXTERNAL_IPTURN_USERTURN_PASS
Set:
PUBLIC_BASE_URLcloudflare.enabled=truecloudflare.tokenIdcloudflare.apiToken
Do not expose your own coturn instance publicly in this setup.
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;
}
}The host UI shows and copies a room URL using this priority:
PUBLIC_BASE_URLserver.publicBaseUrlinconfig.json- active Cloudflare Tunnel URL
- browser origin
This lets the host choose a custom public URL, a tunnel URL, or origin fallback.
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 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
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.255Do not remove the denied-peer-ip rules.
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 is not sufficient for production. It helps only with simple NAT traversal.
- Set a strong
PASSWORD. - Set a strong
SESSION_SECRET. - Set
NODE_ENV=production. - Expose the app over HTTPS.
- Make sure WebSocket upgrade works.
- Provide TURN, either self-hosted or Cloudflare TURN.
- If self-hosting TURN behind NAT or Docker, set
TURN_EXTERNAL_IPcorrectly. - Set
PUBLIC_BASE_URLif the host should copy a specific public URL.
- 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
| 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.
- Node.js 24+
- HTTPS for any non-localhost deployment
- TURN for reliable cross-network WebRTC
cloudflaredin PATH if using Cloudflare Tunnel
MIT