Two phones, one booth. Partners join a private room, see each other live, and press the shutter together - a server-synchronized countdown fires both cameras at the same instant, and the paired shots render into a shared, downloadable photostrip.
- Why it exists: the long-distance photobooth problem
- Features
- Tech Stack
- Repository Layout
- How It Works
- Local Setup
- Deployment
- Validation
- Notes & Roadmap
- License
A photobooth is a two-person machine - and long-distance couples can never stand in front of one together. Video calls feel like calls, not shared moments; the magic of a booth is the synchronized flash and the strip you split afterward. Photobooth removes the distance:
| Problem | Solution | Result |
|---|---|---|
| Couples live apart, so a shared booth is impossible | Each person's device is the booth, joined over the internet | A couple strip made from two different cities |
| Two shutters must fire at the exact same instant | Server-synchronized countdown with per-client clock-offset estimation | Paired shots taken at the same moment |
| WebRTC can fail between restrictive networks | Server-relayed low-fps preview fallback | The live preview still works, always |
Result: the photobooth experience with zero photo-booth hardware, zero
accounts, and zero fees - just two browsers and a small Node.js server.
Everything on the client (camera, video, compression, strip rendering) uses
native web platform APIs; the only runtime dependency is socket.io.
- Private rooms - Up to two people per room. Share a link or room name.
- Live partner preview - WebRTC video with automatic connection retry, plus a server-relayed low-fps preview fallback when peer-to-peer can't connect.
- Synchronized capture - Server-timestamped countdown with per-client clock-offset estimation.
- Dual capture - Front or back camera, switched live without freezing the partner's preview.
- Shared photo tray - Each person sees both sets of shots, up to six each.
- Photostrip builder - Seven templates: classic 35mm-style film strip (default), long 6-frame film strip, Instax instant print, couple strip, 2 x 2, 3 x 2, and a dramatic dark polaroid. Film-stock color grades (Fuji, Instax, clean DSLR), grain, and vignettes are applied per template.
- Screen flash - Optional flash effect that fires in sync with every capture.
- Resilient connections - Auto-reconnect, room-full handling, and graceful cleanup on leave.
| Layer | Technology | Notes |
|---|---|---|
| Server | Node.js (>= 18) + Socket.IO | Room signaling, synchronized countdown, photo relay |
| Live video | WebRTC (RTCPeerConnection) | P2P media with STUN, optional TURN relay |
| Camera | MediaDevices.getUserMedia() |
Native browser API, no camera library |
| Rendering | HTML5 Canvas | Photo compression and photostrip templates |
| Client | Vanilla JavaScript, zero dependencies | Single index.html, no build step |
photobooth/
├── server.js # HTTP + Socket.IO signaling server
├── public/
│ └── index.html # Entire client: UI, WebRTC, capture, strip rendering
├── test/
│ ├── screenshot.js # headless-browser visual check (PNGs per view)
│ └── webrtc-e2e.js # two-headless-browser WebRTC E2E check
├── package.json
└── .gitignore
- Each client joins a room through the Socket.IO server.
- Clients exchange WebRTC offers, answers, and ICE candidates through the server. Video flows directly between peers.
- The shutter asks the server for a capture deadline. Each client estimates the server clock offset and counts down locally.
- Each client compresses its video frame to a JPEG data URL and sends it to the server, which relays it to the partner.
- Photos pair by capture ID and render into the selected strip template on a canvas.
npm install
npm start # http://localhost:3000Open http://localhost:3000 in two browser windows, enter the same room
name, and press the shutter together.
npm run dev # restarts the server on file changes
npm run check # syntax-check server.js- HTTPS is required for camera access anywhere except
localhost. Use a reverse proxy (Caddy, nginx, or a PaaS TLS terminator) in front of the Node server. - TURN servers are required for reliable connections across restrictive NATs and mobile carriers. Configure via environment variables:
| Variable | Purpose |
|---|---|
PORT |
HTTP port (default 3000) |
TURN_URL |
Comma-separated TURN URLs, e.g. turn:turn.example.com:3478 |
TURN_USERNAME |
TURN credential username |
TURN_CREDENTIAL |
TURN credential password |
ALLOWED_ORIGIN |
CORS origin override for the Socket.IO handshake |
Without TURN, connections work on open networks but may fail between restrictive networks; in that case the app automatically falls back to relaying low-fps preview frames through the server, so the live preview still works.
npm run check # syntax-check server.js
node test/webrtc-e2e.js # two headless browsers join a room; reports WebRTC state per side
node test/screenshot.js # headless browser with fake camera; dumps PNGs of each view to /tmp- Room state and photos live in server memory. Up to six compressed photos per person, per room. Restarting the server clears all rooms.
- Roadmap (not yet built): production room state and photos in Redis or an object store with expiry, and a shareable strip URL for each capture.
ISC