diff --git a/docs/README.md b/docs/README.md index 56ea832..6b7c65d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,21 @@ Welcome to the full documentation for Bridge. Whether you're want to build your own extension or are looking for more information about the internals, this is the place to find it. -**This project is still in early development and more content will be added periodically** +## Bundled plugins +- [Inspector](/plugins/inspector/README.md) +- [Rundown](/plugins/rundown/README.md) +- [State](/plugins/state/README.md) +- Clock +- [Caspar](/plugins/caspar/README.md) +- [OSC](/plugins/osc/README.md) + +## Developing plugins +- [Guide](/docs/plugins/README.md) +- [API reference](/docs/api/README.md) + +## Internals +- [Architecture](/docs/architecture.md) +- [Project structure](/docs/structure.md) ## Terminology ![Methodology](/media/docs/architecture/methodology.png) @@ -18,11 +32,3 @@ Widgets are web views hosted by plugins. They provide a user interface that can ### Plugin Plugins are extensions to Bridge that add specific functionality. They are primarily run in the main process and have access to the full Bridge and Nodejs apis. A plugin can register none, one or multiple widgets that are available in the workspace. They can also react to events and provide their own functionality through commands. - -## Plugins -- [Guide](/docs/plugins/README.md) -- [API reference](/docs/api/README.md) - -## Internals -- [Architecture](/docs/architecture.md) -- [Project structure](/docs/structure.md) \ No newline at end of file diff --git a/plugins/osc/README.md b/plugins/osc/README.md new file mode 100644 index 0000000..d8cc766 --- /dev/null +++ b/plugins/osc/README.md @@ -0,0 +1,37 @@ +# OSC plugin +Bridge's default OSC plugin + +## Description +This plugis allows external services to communicate to Bridge using the Open Sound Control protocol (OSC). + +## Table of contents +- [Description](#description) +- [Reference](#reference) + +## Reference +The following list is a reference of the OSC paths that are available within this plugin. + +### `/api/commands/executeCommand` +Execute a command + +#### Arguments +| Index | Type | Description | +| --- | --- | --- | +| 0 | String | The id of the command to execute | +| 1...n | any | Arguments that will be passed to the command | + +### `/api/items/playItem` +Play an item + +#### Arguments +| Index | Type | Description | +| --- | --- | --- | +| 0 | String | The id of the item to play | + +### `/api/items/stopItem` +Stop an item + +#### Arguments +| Index | Type | Description | +| --- | --- | --- | +| 0 | String | The id of the item to stop | diff --git a/plugins/osc/index.js b/plugins/osc/index.js new file mode 100644 index 0000000..b8b70e8 --- /dev/null +++ b/plugins/osc/index.js @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2024 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +/** + * @type { import('../../api').Api } + */ +const bridge = require('bridge') + +const manifest = require('./package.json') + +const Server = require('./lib/Server') +const UDPTransport = require('./lib/UDPTransport') + +const handlers = require('./lib/handlers') + +const Router = require('obj-router') +const router = new Router(handlers) + +/** + * The default server port, + * + * this will be used as the default + * settings value if no other is provided + * + * @type { Number } + */ +const DEFAULT_SERVER_PORT = 8080 + +exports.activate = async () => { + /** + * A reference to the current server + * @type { Server | undefined } + */ + let server + + /** + * Set up the server and start listen + * on a specific port + * @param { Number } port + */ + function setupServer (port = DEFAULT_SERVER_PORT, address) { + teardownServer() + + const transport = new UDPTransport() + transport.listen(port, address) + + server = new Server(transport) + server.on('message', async osc => { + try { + await router.execute(osc.address, osc) + } catch (e) { + console.log(e) + } + }) + } + + /** + * Tear down the + * current server + */ + function teardownServer () { + if (!server) { + return + } + server.teardown() + server = undefined + } + + /** + * A snapshot of the current + * server configuration used + * for diffing against state + * updates + * @type { String } + */ + let serverConfigSnapshot + + /* + Listen to state changes and compare + the current server configuration + + Only set up the server if the + configuration has changed + */ + bridge.events.on('state.change', newState => { + const serverConfig = newState?.plugins?.[manifest.name]?.settings.server + if (serverConfigSnapshot !== JSON.stringify(serverConfig)) { + serverConfigSnapshot = JSON.stringify(serverConfig) + + if (!serverConfig?.active) { + teardownServer() + } else { + setupServer(serverConfig?.port, serverConfig?.bindToAll ? '0.0.0.0' : '127.0.0.1') + } + } + }) + + /* + Set up the server on + startup if active + */ + const serverConfig = await bridge.state.get(`plugins.${manifest.name}.settings.server`) + serverConfigSnapshot = JSON.stringify(serverConfig) + if (serverConfig?.active) { + setupServer(serverConfig?.port, serverConfig?.bindToAll ? '0.0.0.0' : '127.0.0.1') + } + + /* + Set defaults + if missing + */ + if (!serverConfig?.port) { + bridge.state.apply({ + plugins: { + [manifest.name]: { + settings: { + server: { + port: DEFAULT_SERVER_PORT + } + } + } + } + }) + } +} diff --git a/plugins/osc/lib/Server.js b/plugins/osc/lib/Server.js new file mode 100644 index 0000000..9c78b54 --- /dev/null +++ b/plugins/osc/lib/Server.js @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +const EventEmitter = require('events') + +const osc = require('osc-min') + +class Server extends EventEmitter { + /** + * @private + * @type { import('./Transport') } + */ + _transport + + /** + * @param { import('./Transport') } transport + */ + constructor (transport) { + super() + + this._transport = transport + this._transport.on('message', msg => { + const processed = this._process(msg) + this.emit('message', processed) + }) + } + + /** + * @private + * @param { Buffer } buffer + */ + _process (buffer) { + return osc.fromBuffer(buffer) + } + + teardown () { + this._transport.teardown() + this.removeAllListeners() + } +} + +module.exports = Server diff --git a/plugins/osc/lib/Transport.js b/plugins/osc/lib/Transport.js new file mode 100644 index 0000000..278fa7d --- /dev/null +++ b/plugins/osc/lib/Transport.js @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +const EventEmitter = require('events') + +/** + * A base class for transports + * for the Server class + */ +class Transport extends EventEmitter { + /** + * Tear down this transport + */ + teardown () { + this.removeAllListeners() + } +} + +module.exports = Transport diff --git a/plugins/osc/lib/UDPTransport.js b/plugins/osc/lib/UDPTransport.js new file mode 100644 index 0000000..16f6461 --- /dev/null +++ b/plugins/osc/lib/UDPTransport.js @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +const Transport = require('./Transport') + +const dgram = require('node:dgram') + +/** + * @typedef {{ + * ipAddress: String, + * port: String + * }} UDPTransportOptions + */ +class UDPTransport extends Transport { + /** + * @private + * @type { UDPTransportOptions } + */ + _opts + + /** + * @private + * @type { dgram.Socket } + */ + _socket + + /** + * @param { UDPTransportOptions } opts + */ + constructor (opts = {}) { + super() + this._opts = opts + this._socket = dgram.createSocket('udp4') + + this._socket.on('message', (msg, rinfo) => { + this.emit('message', msg) + }) + } + + teardown () { + super.teardown() + this._socket.close() + this._socket.removeAllListeners() + } + + listen (port, address) { + this._socket.bind(port, address) + } +} + +module.exports = UDPTransport diff --git a/plugins/osc/lib/handlers.js b/plugins/osc/lib/handlers.js new file mode 100644 index 0000000..c91409d --- /dev/null +++ b/plugins/osc/lib/handlers.js @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +const bridge = require('bridge') + +/* +Define any available osc paths +*/ +module.exports = { + '/api': { + '/commands': { + '/executeCommand': message => bridge.commands.executeCommand(...message.args.map(arg => arg.value)) + }, + '/items': { + '/playItem': message => bridge.items.playItem(message?.args?.[0].value), + '/stopItem': message => bridge.items.stopItem(message?.args?.[0].value) + } + } +} diff --git a/plugins/osc/package-lock.json b/plugins/osc/package-lock.json new file mode 100644 index 0000000..e2bbde7 --- /dev/null +++ b/plugins/osc/package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "bridge-plugin-osc", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "binpack": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/binpack/-/binpack-0.1.0.tgz", + "integrity": "sha512-KcSrsGiIKgklTWweVb9XnZPWO1/rGSsK3fwR7VnbDPbLKPlkvSKd/ZrJ1W712r6HzH5u0fa/AZCftATO09x8Aw==" + }, + "obj-router": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/obj-router/-/obj-router-3.0.2.tgz", + "integrity": "sha512-hV8mhp0QooKB6nonX9MldMJTS+oBvNbu+KHM7iVPyVlzWucvo2IWPA0yXiSZ3Fm60lgozn6PMlUmRHA4tIumMg==" + }, + "osc-min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/osc-min/-/osc-min-1.1.2.tgz", + "integrity": "sha512-8DbiO8ME85R75stgNVCZtHxB9MNBBNcyy+isNBXrsFeinXGjwNAauvKVmGlfRas5VJWC/mhzIx7spR2gFvWxvg==", + "requires": { + "binpack": "~0" + } + } + } +} diff --git a/plugins/osc/package.json b/plugins/osc/package.json new file mode 100644 index 0000000..d7e9b44 --- /dev/null +++ b/plugins/osc/package.json @@ -0,0 +1,63 @@ +{ + "name": "bridge-plugin-osc", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "engines": { + "bridge": "^0.0.1" + }, + "keywords": [ + "bridge", + "plugin" + ], + "author": "Axel Boberg (git@axelboberg.se)", + "license": "UNLICENSED", + "contributes": { + "settings": [ + { + "group": "OSC", + "title": "OSC server (UDP)", + "description": "Activate the OSC server", + "inputs": [ + { + "type": "boolean", + "bind": "shared.plugins.bridge-plugin-osc.settings.server.active", + "label": "Active" + } + ] + }, + { + "group": "OSC", + "title": "OSC server port (UDP)", + "inputs": [ + { + "type": "number", + "bind": "shared.plugins.bridge-plugin-osc.settings.server.port", + "label": "Port", + "default": 8080, + "min": 3000, + "max": 65535 + } + ] + }, + { + "group": "OSC", + "title": "OSC server listen on all interfaces", + "inputs": [ + { + "type": "boolean", + "bind": "shared.plugins.bridge-plugin-osc.settings.server.bindToAll", + "label": "Bind to 0.0.0.0" + } + ] + } + ] + }, + "dependencies": { + "obj-router": "^3.0.2", + "osc-min": "^1.1.2" + } +}