# WebRTC Tool

A single-page tool for direct peer-to-peer audio, video, chat, and file
transfer between browsers, using WebRTC for the media/data path. The
client is one self-contained `index.html` with no build step. An optional
signaling server (~600 lines of C, POSIX `poll`, no dependencies) lets
peers connect by sharing a short room code instead of pasting SDP blobs.

## What it does

- **Multi-peer calls.** Microphone, camera, screen share. Add
  participants one at a time; each is an independent pairwise connection,
  and they tile into a conference grid. Standard WebRTC.
- **Chat** over a text data channel, fanned out to every connected peer.
- **File transfer** over a separate data channel with backpressure,
  cancellable mid-upload, sent to the peers you select. The receiver gets
  an accept/deny prompt; on accept the file is streamed straight to a
  chosen location on disk (via the File System Access API, with an
  in-memory download fallback for browsers that lack it, e.g. Firefox)
- **Manual signaling.** Generate an offer, copy/paste it to the other
  peer, paste back their answer. No backend required.
- **Auto signaling.** Optional: both peers enter the same room code and
  the signaling server relays the offer/answer for them. Media itself
  still flows peer-to-peer.
- **Loopback mode.** Run both peers in the same tab for testing.
- **Stats** (RTP, codec, bandwidth) and an in-page console.

## How it works

WebRTC requires each pair of peers to exchange Session Description
Protocol (SDP) blobs (offer/answer) before media can flow. After that
exchange, the connection is peer-to-peer; the signaling channel is no
longer used. A call with more than two participants is simply several of
these pairwise connections, each set up independently.

This tool offers two ways to do that exchange:

1. **Manual.** The initiator generates an offer (a few KB of text),
   sends it to the joiner by any means (email, chat, etc.). The joiner
   pastes it, generates an answer, sends it back, and the initiator
   pastes it. No server involved.
2. **Auto.** Both peers enter the same room code into the signaling
   server's UI. The server stores the offer briefly, hands it to the
   other peer when they ask, and is forgotten as soon as the answer
   has been delivered. Rooms expire after 5 minutes of inactivity. Each
   room carries a single offer/answer pair — one pairwise link — so a
   mesh of more than two peers is built one connection (one room code) at
   a time.

The signaling server understands five endpoints:

```
POST /room/<code>/offer     POST /room/<code>/answer
GET  /room/<code>/offer     GET  /room/<code>/answer
GET  /health
```

Long-polling: a `GET` that arrives before the matching blob exists is
parked for up to 10 s and answered with `204 No Content` on timeout; the
client retries. As soon as the matching `POST` arrives, the parked
request is woken with `200` and the SDP body. After the answer has been
delivered to the initiator, the room is dropped.

## Usage

### Standalone (no backend)

The full feature set works without the backend, using manual signaling.
Each peer just needs to load `index.html`; they don't need to load it
from the same place. Options:

- Send the file to the other peer and have them open it from disk
  (`file://…`).
- Host it on any static web server (a personal site, GitHub Pages,
  Netlify, S3, etc.) and share the URL.
- For local testing on one machine, serve it on loopback:
  `python3 -m http.server 8000` and open `http://localhost:8000/`.

Then both peers pick **Manual** mode in the configure screen, the
initiator generates the offer and sends the blob to the joiner (email,
chat, etc.), and the joiner sends the answer back the same way.

For mic/camera/screen-share to work, the page must be loaded from a
secure context: either `https://…`, `http://localhost`, or `file://`
(some browsers, with limits). On plain `http://` to a remote host,
browsers will refuse to grant media access.

### With the signaling backend

The backend is needed only if you want auto signaling (peers exchange
SDPs via a room code rather than copy-paste). It does **not** see or
proxy media — that's still peer-to-peer.

```
podman compose up -d            # builds the image and starts the server on :8080
# or: docker compose up -d
```

After startup, `./static/index.html` appears on the host (copied out of
the image). Point your own web server at `./static` to serve the page.

Or just build and run the binary directly:

```
cc -O2 -Wall -Wextra -Werror server/signal.c -o signal
./signal 8080
```

### Loopback (testing)

In the configure screen, pick **Loopback** and click Continue. Both
peers run in the same tab over a local RTCPeerConnection pair. Useful
for trying chat/file transfer/codec settings without a second device.

## Self-hosting at a single URL

For a production setup with both the HTML and the signaling server on
the same origin (no CORS, no `Server URL` field to fill in), reverse-
proxy `/room/*` and `/health` to the backend and serve everything else
as static files.

The backend binds to `127.0.0.1:8080` (or whatever you mapped in
`compose.yml`). Configs below assume the static files live at
`/var/www/webrtc` and the backend listens on `127.0.0.1:8080`.

### Caddy

```caddy
example.com {
    encode zstd gzip
    root * /var/www/webrtc

    @signaling path /room/* /health
    reverse_proxy @signaling 127.0.0.1:8080

    file_server
}
```

### nginx

```nginx
server {
    listen 443 ssl http2;
    server_name example.com;

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

    root /var/www/webrtc;
    index index.html;

    # Signaling backend: long-polled, must disable response buffering
    # so the 204/200 reaches the browser as soon as the server writes it.
    location ~ ^/(room/|health$) {
        proxy_pass         http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header   Host $host;
        proxy_buffering    off;
        proxy_read_timeout 30s;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}
```

### lighttpd

```lighttpd
server.modules += ( "mod_proxy" )
server.document-root = "/var/www/webrtc"
index-file.names = ( "index.html" )

$HTTP["url"] =~ "^/(room/|health$)" {
    proxy.server = ( "" =>
        (( "host" => "127.0.0.1", "port" => 8080 ))
    )
    proxy.header = ( "upgrade" => "disable" )
}
```

## Configuration notes

- **HTTPS is required for production.** Browsers gate microphone,
  camera, and screen-share behind a secure context. The page works in
  receive-only mode without media access, but most users will want at
  least one side to publish.
- **Same-origin removes the CORS path entirely.** When the HTML and
  signaling are served from the same scheme+host+port, the browser
  doesn't issue preflights and the `Server URL` field in the configure
  screen can be left at its default (`location.origin`).
- **`http://localhost` is treated as secure** by Chromium and Firefox,
  so local development with mic/cam works without certificates.
- **Server capacity is hard-coded in `server/signal.c`:** 1024 rooms.
  Rooms expire after 5 minutes of inactivity and are also deleted immediately
  once the answer reaches the initiator; a GC sweep runs every 60 s.
- **ICE servers.** The client defaults to a public STUN server and lets you
  add your own STUN/TURN entries in the configure screen. STUN is enough for
  most networks, but two peers behind symmetric NATs won't connect without a
  **TURN** relay — add one if connections stall in "checking".
