From 5963efb17a3146c6a5039eb3600d8afdd5b50590 Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Tue, 10 Dec 2024 15:27:46 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A6=20Open=20Sound=20Control=20-=20UDP?= =?UTF-8?q?=20messaging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 31 +++++++++ package.json | 1 + src/electron/utils/api.ts | 39 ++++++++++- src/frontend/components/actions/api.ts | 4 ++ src/frontend/components/actions/apiOSC.ts | 80 +++++++++++++++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/frontend/components/actions/apiOSC.ts diff --git a/package-lock.json b/package-lock.json index 2042245f..60cdefe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "mp4box": "^0.5.2", "node-machine-id": "^1.1.12", "npm-run-all": "^4.1.5", + "osc-js": "^2.4.1", "pdf2img-electron": "^1.2.3", "pptx2json": "^0.0.10", "protobufjs": "^7.2.3", @@ -6199,6 +6200,36 @@ "node": ">=0.10.0" } }, + "node_modules/osc-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.4.1.tgz", + "integrity": "sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==", + "license": "MIT", + "dependencies": { + "ws": "^8.16.0" + } + }, + "node_modules/osc-js/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "dev": true, diff --git a/package.json b/package.json index 6adc3644..6e011fd1 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,7 @@ "mp4box": "^0.5.2", "node-machine-id": "^1.1.12", "npm-run-all": "^4.1.5", + "osc-js": "^2.4.1", "pdf2img-electron": "^1.2.3", "pptx2json": "^0.0.10", "protobufjs": "^7.2.3", diff --git a/src/electron/utils/api.ts b/src/electron/utils/api.ts index c83552e8..461b746e 100644 --- a/src/electron/utils/api.ts +++ b/src/electron/utils/api.ts @@ -1,6 +1,7 @@ import { ipcMain } from "electron" import express from "express" import http from "http" +import OSC from "osc-js" import { Server } from "socket.io" import { uid } from "uid" import { toApp } from ".." @@ -15,6 +16,7 @@ const DEFAULT_PORTS = { WebSocket: 5505, REST: 5506 } export function startWebSocketAndRest(port: number | undefined) { startRestListener(port ? port + 1 : 0) startWebSocket(port) + startOSC(port) } // WEBSOCKET @@ -24,7 +26,7 @@ export function startWebSocket(PORT: number | undefined) { if (!PORT) PORT = DEFAULT_PORTS.WebSocket server.listen(PORT, () => { - console.log(`WebSocket: Starting server at port ${PORT}.`) + console.log(`WebSocket: Starting server at port ${PORT}`) }) server.once("error", (err: any) => { @@ -93,6 +95,41 @@ export function startRestListener(PORT: number | undefined) { }) } +// Open Sound Control + +function startOSC(PORT: number | undefined) { + if (!PORT) PORT = DEFAULT_PORTS.WebSocket + + // const osc = new OSC({ plugin: new OSC.WebsocketServerPlugin() }) // ws://ip:port + const osc = (servers.OSC = new OSC({ plugin: new OSC.DatagramPlugin() })) // UDP + + osc.on("/freeshow/*", async (msg: any) => { + // const active = msg.args[1] || 0 + let args: any = {} + try { + args = JSON.parse(msg.args[0] || "{}") + } catch (err) { + console.log("OSC: Could not parse JSON!\n", err) + } + + const action = msg.address.replace("/freeshow", "") + const returnData = await receivedData({ action, ...args }, (msg: string) => console.log(`OSC: ${msg}`)) + if (!returnData) return + + var message = new OSC.Message(msg.address, returnData) + osc.send(message) + }) + + osc.on("open", () => { + console.log(`OSC: Listening for data at port ${PORT}`) + }) + osc.on("error", (err: any) => { + console.log(`OSC: Error. ${JSON.stringify(err)}`) + }) + + osc.open({ port: PORT }) +} + // DATA async function receivedData(data: any = {}, log: any) { diff --git a/src/frontend/components/actions/api.ts b/src/frontend/components/actions/api.ts index a6f436d8..31263187 100644 --- a/src/frontend/components/actions/api.ts +++ b/src/frontend/components/actions/api.ts @@ -34,6 +34,7 @@ import { startScripture, toggleLock, } from "./apiHelper" +import { oscToAPI } from "./apiOSC" import { sendRestCommandSync } from "./rest" /// STEPS TO CREATE A CUSTOM API ACTION /// @@ -224,6 +225,9 @@ export const API_ACTIONS = { /// RECEIVER / SENDER /// export async function triggerAction(data: API) { + // Open Sound Control format + if (data.action.startsWith("/")) data = oscToAPI(data) + let id = data.action // API start at 1, code start at 0 diff --git a/src/frontend/components/actions/apiOSC.ts b/src/frontend/components/actions/apiOSC.ts new file mode 100644 index 00000000..62a39994 --- /dev/null +++ b/src/frontend/components/actions/apiOSC.ts @@ -0,0 +1,80 @@ +// Examples: /show//start | /slide/next | /clear/all +const oscActions = { + // project: { + // _id: (id: string) => ({ + // open: () => ({ action: "id_select_project", id }), + // // open: () => ({ action: "index_select_project", index: Number(id) }), + // }), + // }, + slide: { + next: () => ({ action: "next_slide" }), + previous: () => ({ action: "previous_slide" }), + }, + show: { + _id: (id: string) => ({ + // open: () => ({ action: "id_select_show", id }), + start: () => ({ action: "start_show", id }), + // slide: () => ({ + // next: () => ({ action: "next_slide", id }), + // previous: () => ({ action: "previous_slide", id }), + // // _id: (slideId: string) => ({ + // // start: () => ({ action: "id_select_slide", id, slideId }), + // // }), + // }), + }), + }, + clear: { + all: () => ({ action: "clear_all" }), + background: () => ({ action: "clear_background" }), + slide: () => ({ action: "clear_slide" }), + overlays: () => ({ action: "clear_overlays" }), + audio: () => ({ action: "clear_audio" }), + next_timer: () => ({ action: "clear_next_timer" }), + }, + timer: { + _id: (id: string) => ({ + start: () => ({ action: "id_start_timer", id }), + }), + stop: () => ({ action: "stop_timers" }), + }, +} + +// data: { action: string, ... } +export function oscToAPI(data: any) { + try { + data = { ...data, ...parsePath(data.action) } + } catch (err) { + // use path value as api action id + let action = data.action.slice(1) + if (!action.includes("/")) return { ...data, action } + + console.log(err) + return data + } + + console.log("OSC API DATA:", data) + return data +} + +function parsePath(path) { + const parts = path.split("/").filter(Boolean) + console.log("OSC API PATH:", path) + + let currentPath: any = oscActions + + for (let part of parts) { + if (typeof currentPath[part] === "function") { + currentPath = currentPath[part]() + } else if (currentPath[part]) { + currentPath = currentPath[part] + } else if (currentPath._id) { + currentPath = currentPath._id(part) + } else { + throw new Error(`Invalid OSC API path: ${path}`) + } + } + + if (typeof currentPath !== "object") return {} + + return currentPath +}