src/plugin/bridge.js
import dgram from 'dgram'
import { WebSocketServer } from 'ws'
import Plugin from './plugin'
/**
* Status flags
* @private
*/
const STATUS = {
IS_NOT_INITIALIZED: -1,
IS_CONNECTING: 0,
IS_OPEN: 1,
IS_CLOSING: 2,
IS_CLOSED: 3,
}
/**
* Default options
* @private
*/
const defaultOptions = {
udpServer: {
host: 'localhost',
port: 41234,
exclusive: false,
},
udpClient: {
host: 'localhost',
port: 41235,
},
wsServer: {
host: 'localhost',
port: 8080,
},
receiver: 'ws',
}
/**
* Helper method to merge nested objects
* @private
*/
function mergeOptions(base, custom) {
return {
...defaultOptions,
...base,
...custom,
udpServer: { ...defaultOptions.udpServer, ...base.udpServer, ...custom.udpServer },
udpClient: { ...defaultOptions.udpClient, ...base.udpClient, ...custom.udpClient },
wsServer: { ...defaultOptions.wsServer, ...base.wsServer, ...custom.wsServer },
}
}
/**
* OSC plugin for setting up communication between a Websocket
* client and a udp client with a bridge inbetween
*/
export default class BridgePlugin extends Plugin {
/**
* Create an OSC Bridge instance with given options. Defaults to
* localhost:41234 for udp server, localhost:41235 for udp client and
* localhost:8080 for Websocket server
* @param {object} [options] Custom options
* @param {string} [options.udpServer.host='localhost'] Hostname of udp server to bind to
* @param {number} [options.udpServer.port=41234] Port of udp server to bind to
* @param {boolean} [options.udpServer.exclusive=false] Exclusive flag
* @param {string} [options.udpClient.host='localhost'] Hostname of udp client for messaging
* @param {number} [options.udpClient.port=41235] Port of udp client for messaging
* @param {string} [options.wsServer.host='localhost'] Hostname of Websocket server
* @param {number} [options.wsServer.port=8080] Port of Websocket server
* @param {http.Server|https.Server} [options.wsServer.server] Use existing Node.js HTTP/S server
* @param {string} [options.receiver='ws'] Where messages sent via 'send' method will be
* delivered to, 'ws' for Websocket clients, 'udp' for udp client
*
* @example
* const plugin = new OSC.BridgePlugin({ wsServer: { port: 9912 } })
* const osc = new OSC({ plugin: plugin })
*
* @example <caption>Using an existing HTTP server</caption>
* const http = require('http')
* const httpServer = http.createServer();
* const plugin = new OSC.BridgePlugin({ wsServer: { server: httpServer } })
* const osc = new OSC({ plugin: plugin })
*/
constructor(options = {}) {
super()
// `dgram` and `WebSocketServer` get replaced with an undefined value in
// builds targeting browser environments
if (!dgram || !WebSocketServer) {
throw new Error('BridgePlugin can not be used in browser context')
}
/** @type {object} options
* @private
*/
this.options = mergeOptions({}, options)
/**
* @type {object} websocket
* @private
*/
this.websocket = null
/**
* @type {object} socket
* @private
*/
this.socket = dgram.createSocket('udp4')
/**
* @type {number} socketStatus
* @private
*/
this.socketStatus = STATUS.IS_NOT_INITIALIZED
// register udp events
this.socket.on('message', (message) => {
this.send(message, { receiver: 'ws' })
this.notify(message.buffer)
})
this.socket.on('error', (error) => {
this.notify('error', error)
})
/**
* @type {function} notify
* @private
*/
this.notify = () => {}
}
/**
* Internal method to hook into osc library's
* EventHandler notify method
* @param {function} fn Notify callback
* @private
*/
registerNotify(fn) {
this.notify = fn
}
/**
* Returns the current status of the connection
* @return {number} Status ID
*/
status() {
return this.socketStatus
}
/**
* Bind a udp socket to a hostname and port
* @param {object} [customOptions] Custom options
* @param {string} [customOptions.host='localhost'] Hostname of udp server to bind to
* @param {number} [customOptions.port=41234] Port of udp server to bind to
* @param {boolean} [customOptions.exclusive=false] Exclusive flag
*/
open(customOptions = {}) {
const options = mergeOptions(this.options, customOptions)
this.socketStatus = STATUS.IS_CONNECTING
// bind udp server
this.socket.bind({
address: options.udpServer.host,
port: options.udpServer.port,
exclusive: options.udpServer.exclusive,
}, () => {
let wsServerOptions = {}
if (options.wsServer.server) {
wsServerOptions.server = options.wsServer.server
} else {
wsServerOptions = options.wsServer
}
// bind Websocket server
this.websocket = new WebSocketServer(wsServerOptions)
this.websocket.binaryType = 'arraybuffer'
// register Websocket events
this.websocket.on('listening', () => {
this.socketStatus = STATUS.IS_OPEN
this.notify('open')
})
this.websocket.on('error', (error) => {
this.notify('error', error)
})
this.websocket.on('connection', (client) => {
client.on('message', (message, rinfo) => {
this.send(message, { receiver: 'udp' })
this.notify(new Uint8Array(message), rinfo)
})
})
})
}
/**
* Close udp socket and Websocket server
*/
close() {
this.socketStatus = STATUS.IS_CLOSING
// close udp socket
this.socket.close(() => {
// close Websocket
this.websocket.close(() => {
this.socketStatus = STATUS.IS_CLOSED
this.notify('close')
})
})
}
/**
* Send an OSC Packet, Bundle or Message. Use options here for
* custom receiver, otherwise the global options will be taken
* @param {Uint8Array} binary Binary representation of OSC Packet
* @param {object} [customOptions] Custom options
* @param {string} [customOptions.udpClient.host='localhost'] Hostname of udp client for messaging
* @param {number} [customOptions.udpClient.port=41235] Port of udp client for messaging
* @param {string} [customOptions.receiver='ws'] Messages will be delivered to Websocket ('ws')
* clients or udp client ('udp')
*/
send(binary, customOptions = {}) {
const options = mergeOptions(this.options, customOptions)
const { receiver } = options
if (receiver === 'udp') {
// send data to udp client
const data = binary instanceof Buffer ? binary : Buffer.from(binary)
this.socket.send(
data,
0,
data.byteLength,
options.udpClient.port,
options.udpClient.host,
)
} else if (receiver === 'ws') {
// send data to all Websocket clients
this.websocket.clients.forEach((client) => {
client.send(binary, { binary: true })
})
} else {
throw new Error('BridgePlugin can not send message to unknown receiver')
}
}
}