DJ Visualizer Pi: standalone Node+WebSocket server with mobile PWA remote
New separate project (the Electron Mac app stays untouched). Reuses the visual engine (WebGL shaders, effect catalog, Web Audio) verbatim and replaces Electron IPC with a Node HTTP+WebSocket server: - server.js: serves the kiosk output page + mobile control PWA, relays the same ctl/rpt JSON protocol over WebSocket, plus REST for media library/upload, JSON state (pads), waveform peaks and MP4 recording (ffmpeg). - public/js/net.js: window.djv shim over WebSocket+HTTP so output.js runs intact. - public/index.html + control.js: touch-first mobile remote (effects, media/queue, ticker, pads, audio/EQ, fullscreen + recording). - PWA (manifest + service worker + generated icons). - scripts/: Chromium kiosk launcher + install-pi.sh (systemd units for Pi 5). Verified: REST endpoints, WebSocket relay both directions, control PWA loads the 2958-effect catalog and renders. (Headless WebGL output not testable without GPU.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
b94c787231
81 files changed
+23994
No files matched your search
@@ -0,0 +1,54 @@
|
||||
// Minimal dependency-free PNG icon generator (solid bg + glowing disc + bars).
|
||||
const zlib = require('zlib');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function crc32(buf) {
|
||||
let c, crc = 0xffffffff;
|
||||
for (let n = 0; n < buf.length; n++) {
|
||||
c = (crc ^ buf[n]) & 0xff;
|
||||
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
crc = (crc >>> 8) ^ c;
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
function chunk(type, data) {
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
|
||||
const t = Buffer.from(type);
|
||||
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0);
|
||||
return Buffer.concat([len, t, data, crc]);
|
||||
}
|
||||
function png(size, draw) {
|
||||
const rgba = Buffer.alloc(size * size * 4);
|
||||
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
||||
const [r, g, b, a] = draw(x, y, size);
|
||||
const i = (y * size + x) * 4; rgba[i] = r; rgba[i + 1] = g; rgba[i + 2] = b; rgba[i + 3] = a;
|
||||
}
|
||||
const raw = Buffer.alloc(size * (size * 4 + 1));
|
||||
for (let y = 0; y < size; y++) { raw[y * (size * 4 + 1)] = 0; rgba.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4); }
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4); ihdr[8] = 8; ihdr[9] = 6;
|
||||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
|
||||
}
|
||||
function draw(x, y, s) {
|
||||
const cx = s / 2, cy = s / 2, d = Math.hypot(x - cx, y - cy) / (s / 2);
|
||||
// background gradient (dark navy -> black)
|
||||
let r = 11 + (1 - y / s) * 10, g = 11 + (1 - y / s) * 8, b = 20 + (1 - y / s) * 18;
|
||||
// glowing disc
|
||||
const glow = Math.max(0, 1 - d * 1.15);
|
||||
r += glow * 90; g += glow * 130; b += glow * 255;
|
||||
// 3 vertical bars (equalizer)
|
||||
const bw = s * 0.10, gap = s * 0.06, total = bw * 3 + gap * 2, x0 = cx - total / 2;
|
||||
const heights = [0.30, 0.52, 0.40];
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const bx = x0 + k * (bw + gap);
|
||||
const bh = s * heights[k], by = cy + s * 0.18 - bh;
|
||||
if (x >= bx && x <= bx + bw && y >= by && y <= cy + s * 0.18) { r = 240; g = 245; b = 255; }
|
||||
}
|
||||
return [Math.min(255, r) | 0, Math.min(255, g) | 0, Math.min(255, b) | 0, 255];
|
||||
}
|
||||
const outDir = path.join(__dirname, '..', 'public', 'icons');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
for (const size of [192, 512]) fs.writeFileSync(path.join(outDir, 'icon-' + size + '.png'), png(size, draw));
|
||||
console.log('icone generate in', outDir);
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot installer for Raspberry Pi OS (Bookworm). Run from the project folder:
|
||||
# bash scripts/install-pi.sh
|
||||
set -e
|
||||
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
USER_NAME="$(whoami)"
|
||||
NODE_BIN="$(command -v node || echo /usr/bin/node)"
|
||||
|
||||
echo "==> Installo dipendenze di sistema (node, ffmpeg, chromium, cage)…"
|
||||
sudo apt update
|
||||
sudo apt install -y nodejs npm ffmpeg cage curl chromium-browser || \
|
||||
sudo apt install -y nodejs npm ffmpeg cage curl chromium
|
||||
|
||||
echo "==> Installo i pacchetti npm…"
|
||||
cd "$DIR"
|
||||
npm install --omit=dev
|
||||
node scripts/gen-icons.js || true
|
||||
chmod +x scripts/kiosk.sh
|
||||
|
||||
echo "==> Creo i servizi systemd…"
|
||||
sudo tee /etc/systemd/system/djvisualizer-server.service >/dev/null <<UNIT
|
||||
[Unit]
|
||||
Description=DJ Visualizer Pi server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=$DIR
|
||||
ExecStart=$NODE_BIN server.js
|
||||
Restart=always
|
||||
User=$USER_NAME
|
||||
Environment=PORT=8080
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
sudo tee /etc/systemd/system/djvisualizer-kiosk.service >/dev/null <<UNIT
|
||||
[Unit]
|
||||
Description=DJ Visualizer kiosk (Chromium fullscreen on HDMI)
|
||||
After=djvisualizer-server.service systemd-user-sessions.target
|
||||
Wants=djvisualizer-server.service
|
||||
|
||||
[Service]
|
||||
User=$USER_NAME
|
||||
PAMName=login
|
||||
TTYPath=/dev/tty1
|
||||
ExecStartPre=/bin/sh -c 'until curl -sf http://localhost:8080/ >/dev/null; do sleep 1; done'
|
||||
ExecStart=/usr/bin/cage -- $DIR/scripts/kiosk.sh
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now djvisualizer-server.service
|
||||
sudo systemctl enable --now djvisualizer-kiosk.service
|
||||
|
||||
IP="$(hostname -I | awk '{print $1}')"
|
||||
echo
|
||||
echo "==> Fatto!"
|
||||
echo " Output (HDMI): parte da solo in kiosk."
|
||||
echo " Telecomando: http://$IP:8080/ (apri dal telefono, stessa rete)"
|
||||
echo " Media: copia i brani in $DIR/media/ (o carica dall'app)"
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch Chromium fullscreen (kiosk) on the Pi's HDMI, pointed at the output page.
|
||||
URL="${1:-http://localhost:8080/output.html}"
|
||||
BIN="$(command -v chromium-browser || command -v chromium || echo chromium)"
|
||||
exec "$BIN" \
|
||||
--kiosk --noerrordialogs --disable-infobars --no-first-run \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--disable-translate --disable-features=Translate \
|
||||
--check-for-update-interval=31536000 \
|
||||
--overscroll-history-navigation=0 \
|
||||
--ozone-platform=wayland --use-gl=egl \
|
||||
"$URL"
|
||||
Reference in new issue
Block a user