A video wall system where a single video plays in sync across a grid of N×M connected devices. Each device shows only its assigned crop of the full video frame. When all devices are physically arranged in a grid and placed side-by-side, they together display one large seamless video.
┌─────────────┐ Socket.IO ┌──────────────┐
│ Admin │ ◄────────────────► │ Server │
│ Dashboard │ │ (Node.js) │
└─────────────┘ └───────┬───────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Device │ │ Device │ │ Device │
│ (R1,C1) │ │ (R1,C2) │ │ (R2,C1) │
└─────────┘ └─────────┘ └─────────┘
vidshow/
├── server/
│ ├── index.js # Express + Socket.IO server
│ └── uploads/ # Video upload destination
├── client/
│ ├── index.html # Device viewer (fullscreen video slice)
│ └── client.js # WebSocket + sync + crop logic
├── admin/
│ ├── index.html # Host dashboard
│ └── admin.js # Upload, grid view, playback controls
├── package.json
└── README.md
- Node.js 16+
npm install
npm startThe server starts on http://localhost:3000.
Open http://localhost:3000/admin/ in a browser on the host machine.
- Set grid size — Enter rows and columns, click "Set Grid"
- Upload video — Select a video file and click "Upload" (or paste a URL)
- Wait for devices — Each grid cell turns green when a device connects
- Playback — Click Play. Use seek slider to jump to any position
Open http://localhost:3000/client/ in a browser on each device.
Option A: Manual entry — Enter row number, column number, grid rows, and grid columns, then click Connect.
Option B: URL params (auto-configure) — Use ?row=N&col=M&rows=R&cols=C:
http://localhost:3000/client/?row=1&col=2&rows=2&cols=3
Option C: QR code — The admin dashboard generates QR codes for each grid position. Scan with a phone to auto-configure.
| Drift | Action |
|---|---|
| < 50ms | Ignore |
| 50–150ms | Speed nudge (playbackRate 0.98 or 1.02) |
| > 150ms | Hard seek (snap to correct position) |
- Play — Starts synchronized playback on all connected devices
- Pause — Pauses all devices at the same timestamp
- Seek — Drag slider; all devices jump to the selected time
- Toggle Aspect — Switch between stretch-to-fill (cover) and letterbox (contain) mode
| Event | Direction | Payload |
|---|---|---|
register |
Client → Server | { rows, cols, myRow, myCol } |
registered |
Server → Client | { deviceId, rows, cols, myRow, myCol, videoPath } |
admin_play |
Admin → Server | — |
admin_pause |
Admin → Server | — |
admin_seek |
Admin → Server | { videoTimestamp } |
play |
Server → Client | { masterTime, videoTimestamp } |
pause |
Server → Client | { videoTimestamp } |
seek |
Server → Client | { videoTimestamp } |
heartbeat |
Server → Client | { masterTime, videoTimestamp } |
device_list |
Server → Client | [ { deviceId, row, col, connected, drift } ] |
sync_report |
Client → Server | { drift } |
grid_updated |
Server → Client | { rows, cols } |
- Late join — Device connects after playback started; receives current timestamp immediately
- Reconnect — Socket.IO auto-reconnects; device re-registers and re-syncs
- Mobile browsers — "Tap to start" overlay handles autoplay restrictions
- Partial grid — Playback works even if not all positions are filled; empty cells show black
- Aspect ratio — Toggle between cover (stretch-to-fill) and contain (letterbox with bars)
- Port: Set
PORTenvironment variable (default: 3000) - Max upload size: 500 MB (configurable in
server/index.js) - Supported video formats: mp4, webm, ogg, mov, avi, mkv
MIT