Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chat P2P

Small browser-to-browser WebRTC group chat with audio, video, and text.

The app uses a lightweight signaling server and mesh WebRTC. It is meant for learning, demos, and small-room testing, not large production meetings.

Live demo:

https://chat-p2p.hcharlie.deno.net/

Features

  • Create a room with a 6-character code.
  • Join the same room from up to 5 browsers/devices.
  • Audio and video are sent peer-to-peer with WebRTC.
  • Text chat is sent through the WebSocket signaling server.
  • Google STUN is used by default.
  • Optional TURN support is configured with environment variables.

How It Works

The server does not relay audio or video. It only:

  • Serves the static web app.
  • Keeps in-memory room state.
  • Assigns each browser a peer ID.
  • Relays WebRTC signaling messages between peers.
  • Broadcasts text chat messages to the room.

For media, the browsers use mesh WebRTC. In a 5-person room, each browser creates 4 WebRTC peer connections and sends its camera/microphone stream to each other browser.

Requirements

  • Deno
  • A modern browser
  • cloudflared, only if testing across the public internet with a temporary tunnel

Install Deno:

brew install deno
deno --version

Install Cloudflare Tunnel:

brew install cloudflared
cloudflared --version

Run Locally

Start the app:

deno task start

Open:

http://localhost:8000

To test locally, open that URL in multiple browser windows or tabs.

Test Over The Public Internet

Camera and microphone access require HTTPS outside localhost. A free Cloudflare quick tunnel is the easiest way to test with another laptop or someone outside your network.

Terminal 1:

deno task start

Terminal 2:

cloudflared tunnel --url http://localhost:8000

cloudflared prints a temporary public URL like:

https://example-random-name.trycloudflare.com

Test flow:

  1. Send the https://...trycloudflare.com URL to each user.
  2. User A opens the URL and clicks Create Room.
  3. User A is already inside the room.
  4. User A copies the room code from the call header.
  5. Other users open the same URL, enter the code, and click Join Room.
  6. The room supports up to 5 users total.

Keep both terminal processes running while testing. If either the Deno server or Cloudflare tunnel stops, the public URL stops working.

Room Behavior

The creator does not need to join with the code. Clicking Create Room creates the room and joins it immediately.

The room code is only for other users. If the creator opens another tab and joins with the same code, that tab counts as another participant.

Rooms are stored in memory. Restarting the server clears all rooms.

STUN And TURN

Default free STUN config:

STUN_URLS=stun:stun.l.google.com:19302

STUN helps peers discover public network paths. It is enough for many home and mobile networks.

TURN is needed when direct peer-to-peer connections are blocked by strict NATs or firewalls. TURN relays media through a server, so it usually costs bandwidth.

Optional TURN config:

TURN_URLS=turn:your-turn-host:3478,turns:your-turn-host:5349
TURN_USERNAME=your-username
TURN_CREDENTIAL=your-password
deno task start

TURN credentials are sent to the browser because WebRTC needs them. For production, prefer short-lived TURN credentials instead of a permanent shared password.

Deploy

Deploy this as a single web service that supports HTTP and WebSocket upgrades.

Deno Deploy

This repo includes Deno Deploy config in deno.json:

{
  "deploy": {
    "runtime": {
      "type": "dynamic",
      "entrypoint": "server.ts"
    }
  }
}

The entrypoint must be server.ts. Do not use server.js on Deno Deploy; that file is a Node fallback and uses CommonJS require().

Recommended Deno Deploy app settings:

Framework preset: No Preset
Install command: empty
Build command: empty
Runtime mode: Dynamic
Entrypoint: server.ts
Arguments: empty
Working directory: .

Environment variables are optional for the default Google STUN setup. The app falls back to stun:stun.l.google.com:19302 automatically.

After deploy, verify:

https://chat-p2p.hcharlie.deno.net/config

Expected response:

{"iceServers":[{"urls":["stun:stun.l.google.com:19302"]}]}

Then open the app URL, click Create Room, and confirm the call screen appears with a room code in the header.

Generic Server

Run command:

deno run --allow-net --allow-read --allow-env server.ts

Environment:

PORT=8000
STUN_URLS=stun:stun.l.google.com:19302

The browser automatically uses:

  • / for the app
  • /ws for WebSocket signaling
  • /config for STUN/TURN config

In production, serve the app over HTTPS so browsers allow camera and microphone access.

Future TODOs

Short-Term Improvements

  • Add a real room lobby with display names instead of short peer IDs.
  • Add explicit join/leave state so stale tabs are easier to understand.
  • Show per-peer connection status in each video tile.
  • Add screen sharing.
  • Add mute/camera state indicators for remote users.
  • Add better error messages for blocked camera/microphone permissions.
  • Add basic automated browser tests for create/join/signaling flows.

Reliability Improvements

  • Add TURN for networks where direct peer-to-peer fails.
  • Prefer short-lived TURN credentials instead of static credentials.
  • Add room expiration and cleanup timers.
  • Add reconnect handling for temporary WebSocket disconnects.
  • Add rate limits and input validation around room creation and chat.

Production Direction: SFU / LiveKit

This app currently uses mesh WebRTC. Mesh is simple, but every user sends media to every other user. That becomes expensive quickly.

For production group video or live streaming, move to an SFU architecture:

User A -> SFU -> Users B/C/D/E
User B -> SFU -> Users A/C/D/E

An SFU lets each user upload media once, then the server forwards streams to the right subscribers. This is the normal architecture for larger group calls, live classes, webinars, and production streaming.

Good SFU options:

  • LiveKit
  • mediasoup
  • Janus
  • Jitsi
  • Pion-based custom SFU

LiveKit is a strong next step because it provides the SFU, client SDKs, room management, subscriptions, permissions, recording options, and deployment paths. Moving to LiveKit would replace most of this app's custom WebRTC mesh/signaling code with LiveKit rooms and SDK calls.

Troubleshooting

Room Is Full

Each room supports up to 5 peers. Room is full means the signaling server already has five WebSocket connections registered for that room.

Common causes:

  • The room is already open in another tab, window, or device.
  • A join button was clicked more than once.
  • A laptop refreshed and the old WebSocket has not closed yet.
  • A stale test room is still stored in the running server process.

Clean reset:

  1. Close all app tabs on all devices.
  2. Stop the Deno server with Ctrl+C.
  3. Stop the Cloudflare tunnel with Ctrl+C.
  4. Start the Deno server again.
  5. Start a new Cloudflare tunnel.
  6. Use the new https://...trycloudflare.com URL.

Users Join But Media Never Connects

If users can create/join a room but the badge never becomes Connected, WebRTC likely failed on that network. Add a TURN server and set TURN_URLS, TURN_USERNAME, and TURN_CREDENTIAL.

Mesh Room Limits

Mesh WebRTC is simple, but it does not scale well. With 5 users, each user uploads media to 4 other users. CPU and bandwidth usage grow quickly as the room gets larger.

For larger or production group calls, use an SFU such as LiveKit, mediasoup, Janus, or Jitsi.

About

WebRTC P2P video/voice/text chat

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages