153 lines
5.7 KiB
JavaScript
153 lines
5.7 KiB
JavaScript
// =====================================================================
|
|
// web_realart / server.js -- Bridge OSC <- data_feeds bridge.py vers
|
|
// WS pour clients web (real.art.saillant.cc).
|
|
//
|
|
// Aucune dependance a SuperCollider : ce serveur ne fait QUE relayer
|
|
// les /data/<source>/<sub> recus en OSC vers tous les clients WS.
|
|
//
|
|
// Architecture cible :
|
|
//
|
|
// data_feeds/bridge.py --OSC UDP 57124--> server.js --WS--> navigateurs
|
|
// ^
|
|
// |
|
|
// Cloudflared tunnel
|
|
// real.art.saillant.cc
|
|
//
|
|
// Variables d'environnement :
|
|
// HTTP_PORT (defaut 4400)
|
|
// DATA_PORT_IN (defaut 57124, ce que bridge.py envoie ici)
|
|
// FEEDBACK_PORT_IN (defaut 9000, feedback SC engine -> navigateurs)
|
|
// =====================================================================
|
|
import express from "express";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
import { createServer } from "node:http";
|
|
import { WebSocketServer } from "ws";
|
|
import osc from "osc";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const HTTP_PORT = parseInt(process.env.HTTP_PORT ?? "4400", 10);
|
|
const DATA_PORT_IN = parseInt(process.env.DATA_PORT_IN ?? "57124", 10);
|
|
const SC_HOST = process.env.AVLIVE_SC_HOST ?? "127.0.0.1";
|
|
const SC_PORT = parseInt(process.env.AVLIVE_SC_PORT ?? "57121", 10);
|
|
const FEEDBACK_PORT_IN = parseInt(process.env.FEEDBACK_PORT_IN ?? "9000", 10);
|
|
|
|
const app = express();
|
|
app.use(express.static(join(__dirname, "public")));
|
|
|
|
app.get("/api/health", (_req, res) => {
|
|
res.json({
|
|
ok: true,
|
|
clients: wss?.clients.size ?? 0,
|
|
data_port: DATA_PORT_IN,
|
|
last_event: lastEventAt,
|
|
});
|
|
});
|
|
|
|
const httpServer = createServer(app);
|
|
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
|
|
// derniere snapshot de chaque /data/<source>/<sub> -> envoye a chaque
|
|
// nouveau client pour qu'il ne demarre pas a vide.
|
|
const lastByAddress = new Map();
|
|
let lastEventAt = 0;
|
|
|
|
function broadcast(msg) {
|
|
const payload = JSON.stringify(msg);
|
|
for (const client of wss.clients) {
|
|
if (client.readyState === client.OPEN) client.send(payload);
|
|
}
|
|
}
|
|
|
|
wss.on("connection", (ws) => {
|
|
console.log(`[ws] client connecte (total: ${wss.clients.size})`);
|
|
// replay snapshot
|
|
for (const [address, args] of lastByAddress) {
|
|
ws.send(JSON.stringify({ address, args }));
|
|
}
|
|
ws.send(JSON.stringify({ address: "/sync/hello",
|
|
args: [{ data_port: DATA_PORT_IN }] }));
|
|
ws.on("message", (data) => {
|
|
let m; try { m = JSON.parse(data.toString()); } catch (_e) { return; }
|
|
if (!m || typeof m.address !== "string") return;
|
|
const args = (Array.isArray(m.args) ? m.args : []).map((a) =>
|
|
typeof a === "number"
|
|
? (Number.isInteger(a) ? { type: "i", value: a } : { type: "f", value: a })
|
|
: { type: "s", value: String(a) });
|
|
try { scPort.send({ address: m.address, args }, SC_HOST, SC_PORT); } catch (_e) { /* SC down */ }
|
|
});
|
|
ws.on("close", () => console.log(`[ws] client deconnecte (total: ${wss.clients.size})`));
|
|
});
|
|
|
|
const dataPort = new osc.UDPPort({
|
|
localAddress: "0.0.0.0",
|
|
localPort: DATA_PORT_IN,
|
|
metadata: false,
|
|
});
|
|
|
|
dataPort.on("ready", () => {
|
|
console.log(`[data] ecoute :${DATA_PORT_IN} <- bridge.py`);
|
|
});
|
|
|
|
dataPort.on("message", (oscMsg) => {
|
|
lastEventAt = Date.now();
|
|
if (oscMsg.address.startsWith("/data/")) {
|
|
lastByAddress.set(oscMsg.address, oscMsg.args);
|
|
}
|
|
broadcast({ address: oscMsg.address, args: oscMsg.args });
|
|
});
|
|
|
|
dataPort.on("error", (err) => console.error("[data] erreur:", err.message));
|
|
dataPort.open();
|
|
|
|
// Second UDP listener: engine feedback (SC -> browser clients).
|
|
// Relays /armed/*, /sync/beat, /sync/rms, /seq/*/state to all WS clients.
|
|
// Does NOT update lastByAddress snapshot (feedback is transient, not snapshottable).
|
|
const feedbackPort = new osc.UDPPort({
|
|
localAddress: "0.0.0.0",
|
|
localPort: FEEDBACK_PORT_IN,
|
|
metadata: false,
|
|
});
|
|
|
|
feedbackPort.on("ready", () => {
|
|
console.log(`[feedback] ecoute :${FEEDBACK_PORT_IN} <- SC engine`);
|
|
});
|
|
|
|
const FEEDBACK_PREFIXES = ["/armed/", "/sync/", "/seq/", "/scene/", "/matrix/"];
|
|
feedbackPort.on("message", (oscMsg) => {
|
|
if (!FEEDBACK_PREFIXES.some((p) => oscMsg.address.startsWith(p))) return;
|
|
broadcast({ address: oscMsg.address, args: oscMsg.args });
|
|
});
|
|
|
|
feedbackPort.on("error", (err) => console.error("[feedback] erreur:", err.message));
|
|
feedbackPort.open();
|
|
|
|
const scPort = new osc.UDPPort({ localAddress: "127.0.0.1", localPort: 0, metadata: true });
|
|
scPort.on("ready", () => console.log(`[sc] OSC out -> ${SC_HOST}:${SC_PORT}`));
|
|
scPort.open();
|
|
|
|
httpServer.listen(HTTP_PORT, () => {
|
|
console.log(`[server] HTTP :${HTTP_PORT} listening`);
|
|
console.log("");
|
|
console.log("=== AV-Live realart ===");
|
|
console.log(` Landing : http://localhost:${HTTP_PORT}/`);
|
|
console.log(` WebGL : http://localhost:${HTTP_PORT}/webgl/`);
|
|
console.log(` Audio : http://localhost:${HTTP_PORT}/audio/`);
|
|
console.log(` Hydra : http://localhost:${HTTP_PORT}/hydra/`);
|
|
console.log(` WS : ws://localhost:${HTTP_PORT}/ws`);
|
|
console.log(` Health : http://localhost:${HTTP_PORT}/api/health`);
|
|
console.log("");
|
|
});
|
|
|
|
function shutdown() {
|
|
console.log("\n[server] shutdown...");
|
|
wss.close();
|
|
dataPort.close();
|
|
feedbackPort.close();
|
|
scPort.close();
|
|
httpServer.close(() => process.exit(0));
|
|
}
|
|
process.on("SIGINT", shutdown);
|
|
process.on("SIGTERM", shutdown);
|