YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
PixelStream π₯οΈβ‘οΈπ
A mini "TeamViewer for a browser". You open a web UI, hit Start Browser, and a headless Chromium running inside a Docker container streams its screen back to your browser in real time. You can click, scroll, type, and navigate β and it all happens inside that remote headless browser...........
How it works
ββββββββββββββββ click / scroll / type (WebSocket) βββββββββββββββββ CDP ββββββββββββββββββββ
β Web UI β ββββββββββββββββββββββββββββββββββββΊ β Node backend β ββββββββΊ β Headless Chromium β
β (Next.js) β β (ws + CDP) β β (Puppeteer) β
β <canvas> β ββββββββββββββββββββββββββββββββββββ β β ββββββββ β β
ββββββββββββββββ live JPEG frames (WebSocket) βββββββββββββββββ screencast ββββββββββββββββββββ
- Frontend (
/frontend) β Next.js app. A<canvas>paints the live frames; mouse/keyboard/scroll events are captured, converted to real browser coordinates, and sent over a WebSocket. - Backend (
/backend) β Node.js +ws+ Puppeteer. Launches Chromium, uses the Chrome DevTools Protocol (CDP)Page.startScreencastto get a continuous stream of JPEG frames, and injects input viaInput.dispatchMouseEvent/Input.dispatchKeyEvent. - Docker β both services run in containers via
docker compose. The backend uses the official Puppeteer image (Chromium + system deps preinstalled).
Why CDP screencast (not a screenshot loop)?
Page.startScreencast only emits a frame when the page actually changes, and it's
much smoother and lower-latency than taking page.screenshot() on a timer.
Run it
Option A β Docker (recommended)
docker compose up --build
Then open http://localhost:3000 and click Start Browser.
Option B β Local (no Docker, for fast iteration)
# terminal 1
cd backend && npm install && npm start # ws server on :8080
# terminal 2
cd frontend && npm install && npm run dev # UI on :3000
Deploy (Frontend β Vercel, Backend β Render)
β οΈ The browser page is served over HTTPS (Vercel), so it can only open a secure WebSocket (
wss://). Render gives you anhttps://β¦onrender.comURL, whose WebSocket endpoint iswss://β¦onrender.com. Plainws://will be blocked as mixed content.
1. Backend on Render
- New Web Service β connect this repo.
- Runtime: Docker, Dockerfile path:
backend/Dockerfile, context:backend(or just use the includedrender.yamlBlueprint). - Deploy. Note the URL, e.g.
https://pixelstream-backend.onrender.com. - Render injects
PORTautomatically β the server already reads it.
2. Frontend on Vercel
New Project β import this repo β set Root Directory =
frontend.Add an environment variable:
Key Value NEXT_PUBLIC_WS_URLwss://pixelstream-backend.onrender.comDeploy. Open the Vercel URL β Start Browser.
What works β
- Start a headless Chromium in a Docker container from the web UI
- Live screen streaming via CDP screencast (JPEG frames over WebSocket)
- Mouse: move, left/right click, scroll
- Keyboard: printable characters + common special keys (Enter, Backspace, Tab, arrows, Esc, Deleteβ¦)
- URL bar to navigate the remote browser
- Accurate click mapping (canvas coords β real viewport coords)
Known limitations / where it gets hard β οΈ
- Keyboard coverage β only common special keys are mapped; modifier combos (Ctrl+C, Shift+selection) aren't fully wired yet.
- Single session β one browser per server; no multi-user / multi-tab.
- Streaming β JPEG-over-WebSocket is simple but bandwidth-heavy; WebRTC or VP8/H.264 encoding would be smoother at scale.
- Chromium-in-Docker β needs
--no-sandbox,--disable-dev-shm-usage, andshm_size: 1gbor Chromium crashes. (These are already set.)
Next steps π
- Full keyboard model with modifiers + IME/composition events
- Multiple isolated sessions (one container per user, spawned on demand)
- WebRTC streaming for lower latency / adaptive quality
- Reconnect handling + session timeouts + resource cleanup
Project structure
Pixel/
βββ docker-compose.yml
βββ backend/
β βββ Dockerfile
β βββ package.json
β βββ server.js # ws server + Puppeteer + CDP screencast/input
βββ frontend/
βββ Dockerfile
βββ package.json
βββ next.config.js
βββ app/
βββ layout.js
βββ page.js # UI + canvas + input capture