- JavaScript 82.8%
- Vue 14.6%
- CSS 2.1%
- HTML 0.5%
- Server: Express + ws — polls FPP status, extrapolates show clock, broadcasts sync snapshots @1Hz, serves show/tracks APIs + range audio - Mock FPP simulator with admin control endpoints for dev/CI - Vue 3 SPA: join live mid-song, drift correction (hard seek >350ms, rate nudge 0.98-1.02), About + Music List pages - Tests: 29 server (Vitest/supertest), 21 web (Vue Test Utils), 2 Playwright e2e — all green - Docs: README architecture/config/deploy, Requirements, Build plan |
||
|---|---|---|
| e2e | ||
| mock-fpp | ||
| server | ||
| web | ||
| .gitignore | ||
| AGENTS.md | ||
| Build_plan.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| Requirements.md | ||
Get Pixel
Music Sync over the Internet for Pixel Light Shows.
Get Pixel is a mobile-first web app for guests at a pixel light show. Scan a QR code, tap Play Live, and your phone plays the show's music in sync with the lights driven by a Falcon Pi Player (FPP) controller — plus About and Music List pages for show context.
Features
- Join the live show mid-song — audio starts at the controller's current playlist position
- Continuous drift correction — hard seek beyond ±350 ms, gentle playbackRate nudges (0.98–1.02) inside 80–350 ms
- Live metadata — song title/artist, track progress, time until next sequence, up-next title
- Show stats — total show length and song count
- About this Show — creator story, build specs, links (from
server/data/show.json) - Show Music List — ordered tracks with purchase/download links (from
server/data/tracks.json) - Auto-reconnect — WebSocket client reconnects with backoff; server keeps extrapolating through brief FPP dropouts
Architecture
[FPP controller or mock-fpp] --REST /api/fppd/status--> [Get Pixel server]
| polls (~500 ms), derives authoritative clock
[Guest phones] --HTTP--> static Vue SPA + /api/show* |
<-------WebSocket /ws sync snapshots @1Hz ------------+
<-------HTTP range audio from server/media/ ---------+
Sync model
- Server polls FPP
GET /api/fppd/statusevery ~500 ms →{playing, positionSec, songName}. - Between polls the server extrapolates:
position = fppPositionSec + (now - lastGoodPoll). - The WS hub broadcasts
{playing, trackIndex, track, positionSec, showElapsedSec, serverTimeMs, ...}every 1 s (and immediately on play-state/track changes). - Each client estimates its clock offset from WS ping/pong round trips (median of last 5 samples) and computes the target playhead.
- The audio engine corrects drift: > 350 ms → hard seek; 80–350 ms → playbackRate nudge clamped to [0.98, 1.02]; otherwise untouched.
- Tapping Play joins the live show at the current position — no waiting for the next song.
Repo layout
| Path | What it is |
|---|---|
server/ |
Express API, FPP REST client, sync engine, WS hub, /media audio with HTTP range support |
server/data/show.json |
Show title, creator, about text, build specs, links |
server/data/tracks.json |
Ordered tracks: title, artist, file, length, purchase URL |
server/media/ |
Audio files served to guests (01-aurora.wav, …) |
mock-fpp/ |
FPP API simulator with admin control endpoints for dev/testing |
web/ |
Vue 3 + Vite SPA (/ show, /about, /music) |
e2e/ |
Playwright specs driving the full stack |
Quick start
npm install # installs all workspaces
node server/scripts/generate-sample-audio.mjs # demo WAVs into server/media/
npm run dev # mock-fpp :8350 + server :4000 + vite :5173
Open http://localhost:5173 on your phone/desktop. Start the simulated show:
curl -X POST http://localhost:8350/admin/play \
-H 'content-type: application/json' -d '{"positionSec": 0}'
Then tap Play Live — the page joins at the live position.
Production-style single-origin serving:
npm run build # builds web/dist
PORT=4000 WEB_DIST=web/dist npm start # server serves SPA + API + media + ws
Pointing at real FPP hardware
Set FPP_BASE_URL to your controller's HTTP endpoint (FPP serves its REST API on port 80):
FPP_BASE_URL=http://192.168.1.50 PORT=4000 npm run start --workspace @get-pixel/server
The client normalizes common FPP status payloads (numeric or named status,
seconds_played or time_elapsed, current_song/current_sequence). Track titles,
artists, lengths, and purchase links come from your own tracks.json; match is by media
file name (extension-insensitive) falling back to sequence/title.
Configuration (env vars)
| Variable | Default | Purpose |
|---|---|---|
PORT |
4000 |
Get Pixel server port |
HOST |
0.0.0.0 |
Bind address |
FPP_BASE_URL |
http://localhost:8350 |
FPP (or mock) base URL |
FPP_TIMEOUT_MS |
2000 |
FPP request timeout |
FPP_POLL_INTERVAL_MS |
500 |
Status poll cadence |
BROADCAST_INTERVAL_MS |
1000 |
WS snapshot cadence |
DATA_DIR |
server/data |
JSON content directory |
MEDIA_DIR |
server/media |
Audio files directory |
WEB_DIST |
(none) | Serve built SPA from this path |
Mock-FPP admin endpoints (dev/tests only): POST /admin/play {positionSec}, /admin/pause,
/admin/seek {positionSec}, /admin/load {playlist:[{file,lengthSec,…}]}, GET /admin/state.
Editing show content
server/data/show.json— title, creator, tagline,about(string or array of paragraphs),buildSpecs [{label,value}],links [{label,url}].server/data/tracks.json— ordered array of{id, title, artist, file, lengthSec, purchaseUrl}.lengthSecdrives progress bars and show totals; keep it accurate.- Drop matching audio files into
server/media/. Any browser-playable format works; files are served with range requests so seeking is instant.
Testing
npm test # server (29) + web (21) Vitest suites
npm run test:e2e # Playwright: full-stack guest flow incl. mid-show join
E2e automatically builds web/dist, boots mock-fpp + server (SPA served single-origin),
and runs headless Chromium. Requires browsers once via npx playwright install chromium.
Deploying
Any host with Node 20+ works (DigitalOcean, Linode, Hetzner, or on-site mini PC):
npm ci && npm run build- Run
WEB_DIST=web/dist node server/src/index.jsbehind your reverse proxy (nginx/Caddy) with WebSocket upgrade support for/ws. - Set
FPP_BASE_URLto your controller. Guests need network reachability to this host — port-forward over HTTPS if you want access from outside your LAN. - Put a QR code on the yard sign pointing at your URL.
Bandwidth: each guest pulls ~1–2 Mbps while playing. A 200 Mbit uplink comfortably serves 100+ simultaneous phones.
Requirements & plan
Requirements.md— product requirementsBuild_plan.md— epics, stories, acceptance criteria, phased workflowAGENTS.md— agent session log and decisions