diff --git a/src/AbstractMessage.js b/src/AbstractMessage.js index 4c938a5..a11242b 100644 --- a/src/AbstractMessage.js +++ b/src/AbstractMessage.js @@ -1,27 +1,30 @@ /* eslint-disable no-restricted-syntax */ // A Protocol message interface -function AbstractMessage() {} +class AbstractMessage { + constructor() { + this.isMessage = true; + this.isValid = true; + this.buffer = Buffer.alloc(0); + } -AbstractMessage.prototype.mixin = function copyFrom(data) { - for (const k in data) { - // eslint-disable-next-line no-prototype-builtins - if (data.hasOwnProperty(k)) { - this[k] = data[k]; + mixin(data) { + for (const k in data) { + // eslint-disable-next-line no-prototype-builtins + if (data.hasOwnProperty(k)) { + this[k] = data[k]; + } + } + return this; } - } - return this; -}; -AbstractMessage.prototype.parseBuffer = function parseBuffer(buffer) { - this.buffer = buffer; - return this; -}; + parseBuffer(buffer) { + this.buffer = buffer; + return this; + } -AbstractMessage.prototype.generateBuffer = function generateBuffer() { - return this; -}; -AbstractMessage.prototype.isMessage = true; -AbstractMessage.prototype.isValid = true; -AbstractMessage.prototype.buffer = Buffer.alloc(0); + generateBuffer() { + return this; + } +} module.exports = AbstractMessage; diff --git a/src/ControlMessage.js b/src/ControlMessage.js index a87a0b3..7410031 100644 --- a/src/ControlMessage.js +++ b/src/ControlMessage.js @@ -1,6 +1,4 @@ /* eslint-disable no-bitwise */ -const util = require('util'); - const AbstractMessage = require('./AbstractMessage'); const byteToCommand = { @@ -30,102 +28,101 @@ const flags = { start: 0xFFFF, }; -function ControlMessage() { - AbstractMessage.apply(this); -} +class ControlMessage extends AbstractMessage { + constructor() { + super() + this.name = ''; + this.isValid = true; + this.start = flags.start; + this.version = 2; + } -util.inherits(ControlMessage, AbstractMessage); + parseBuffer(buffer, ...args) { + super.parseBuffer(...args); + this.start = buffer.readUInt16BE(0); + if (this.start !== flags.start) { + this.isValid = false; + return this; + } + this.command = byteToCommand[buffer.readUInt16BE(2)]; -ControlMessage.prototype.name = ''; -ControlMessage.prototype.isValid = true; -ControlMessage.prototype.start = flags.start; -ControlMessage.prototype.version = 2; + switch (this.command) { + case 'invitation': + case 'invitation_accepted': + case 'invitation_rejected': + case 'end': + this.version = buffer.readUInt32BE(4); + this.token = buffer.readUInt32BE(8); + this.ssrc = buffer.readUInt32BE(12); + this.name = buffer.toString('utf-8', 16); + break; + case 'synchronization': + this.ssrc = buffer.readUInt32BE(4, 8); + this.count = buffer.readUInt8(8); + // eslint-disable-next-line no-bitwise + this.padding = (buffer.readUInt8(9) << 0xF0) + buffer.readUInt16BE(10); + this.timestamp1 = buffer.slice(12, 20); + this.timestamp2 = buffer.slice(20, 28); + this.timestamp3 = buffer.slice(28, 36); + break; + case 'receiver_feedback': + this.ssrc = buffer.readUInt32BE(4, 8); + this.sequenceNumber = buffer.readUInt16BE(8); + break; + default: + break; + } + return this; + } -ControlMessage.prototype.parseBuffer = function parseBuffer(buffer, ...args) { - AbstractMessage.prototype.parseBuffer.apply(this, args); - this.start = buffer.readUInt16BE(0); - if (this.start !== flags.start) { - this.isValid = false; - return this; - } - this.command = byteToCommand[buffer.readUInt16BE(2)]; + generateBuffer() { + let buffer; + const commandByte = commandToByte[this.command]; - switch (this.command) { - case 'invitation': - case 'invitation_accepted': - case 'invitation_rejected': - case 'end': - this.version = buffer.readUInt32BE(4); - this.token = buffer.readUInt32BE(8); - this.ssrc = buffer.readUInt32BE(12); - this.name = buffer.toString('utf-8', 16); - break; - case 'synchronization': - this.ssrc = buffer.readUInt32BE(4, 8); - this.count = buffer.readUInt8(8); - // eslint-disable-next-line no-bitwise - this.padding = (buffer.readUInt8(9) << 0xF0) + buffer.readUInt16BE(10); - this.timestamp1 = buffer.slice(12, 20); - this.timestamp2 = buffer.slice(20, 28); - this.timestamp3 = buffer.slice(28, 36); - break; - case 'receiver_feedback': - this.ssrc = buffer.readUInt32BE(4, 8); - this.sequenceNumber = buffer.readUInt16BE(8); - break; - default: - break; - } - return this; -}; + switch (this.command) { + case 'invitation': + case 'invitation_accepted': + case 'invitation_rejected': + case 'end': + this.name = this.name || ''; + buffer = Buffer.alloc(17 + Buffer.byteLength(this.name, 'utf8')); + buffer.writeUInt16BE(this.start, 0); + buffer.writeUInt16BE(commandByte, 2); + buffer.writeUInt32BE(this.version, 4); + buffer.writeUInt32BE(this.token, 8); + buffer.writeUInt32BE(this.ssrc, 12); + buffer.write(this.name, 16); + if (this.command !== 'end') { + buffer.writeUInt8(0, buffer.length - 1); + } + break; + case 'synchronization': + buffer = Buffer.alloc(36); + buffer.writeUInt16BE(this.start, 0); + buffer.writeUInt16BE(commandByte, 2); + buffer.writeUInt32BE(this.ssrc, 4); + buffer.writeUInt8(this.count, 8); + buffer.writeUInt8(this.padding >>> 0xF0, 9); + buffer.writeUInt16BE(this.padding & 0x00FFFF, 10); -ControlMessage.prototype.generateBuffer = function generateBuffer() { - let buffer; - const commandByte = commandToByte[this.command]; + this.timestamp1.copy(buffer, 12); + this.timestamp2.copy(buffer, 20); + this.timestamp3.copy(buffer, 28); - switch (this.command) { - case 'invitation': - case 'invitation_accepted': - case 'invitation_rejected': - case 'end': - this.name = this.name || ''; - buffer = Buffer.alloc(17 + Buffer.byteLength(this.name, 'utf8')); - buffer.writeUInt16BE(this.start, 0); - buffer.writeUInt16BE(commandByte, 2); - buffer.writeUInt32BE(this.version, 4); - buffer.writeUInt32BE(this.token, 8); - buffer.writeUInt32BE(this.ssrc, 12); - buffer.write(this.name, 16); - if (this.command !== 'end') { - buffer.writeUInt8(0, buffer.length - 1); + break; + case 'receiver_feedback': + buffer = Buffer.alloc(12); + buffer.writeUInt16BE(this.start, 0); + buffer.writeUInt16BE(commandByte, 2); + buffer.writeUInt32BE(this.ssrc, 4); + buffer.writeUInt16BE(this.sequenceNumber, 8); + break; + default: + break; } - break; - case 'synchronization': - buffer = Buffer.alloc(36); - buffer.writeUInt16BE(this.start, 0); - buffer.writeUInt16BE(commandByte, 2); - buffer.writeUInt32BE(this.ssrc, 4); - buffer.writeUInt8(this.count, 8); - buffer.writeUInt8(this.padding >>> 0xF0, 9); - buffer.writeUInt16BE(this.padding & 0x00FFFF, 10); - - this.timestamp1.copy(buffer, 12); - this.timestamp2.copy(buffer, 20); - this.timestamp3.copy(buffer, 28); - - break; - case 'receiver_feedback': - buffer = Buffer.alloc(12); - buffer.writeUInt16BE(this.start, 0); - buffer.writeUInt16BE(commandByte, 2); - buffer.writeUInt32BE(this.ssrc, 4); - buffer.writeUInt16BE(this.sequenceNumber, 8); - break; - default: - break; - } - this.buffer = buffer; - return this; -}; + this.buffer = buffer; + return this; + } +} module.exports = ControlMessage; diff --git a/src/MTC.js b/src/MTC.js index 7892c06..3160402 100644 --- a/src/MTC.js +++ b/src/MTC.js @@ -1,117 +1,116 @@ /* eslint-disable no-mixed-operators */ /* eslint-disable prefer-destructuring */ /* eslint-disable no-bitwise */ -const util = require('util'); const { EventEmitter } = require('events'); -function MTC() { - EventEmitter.apply(this); - this.hours = 0; - this.minutes = 0; - this.seconds = 0; - this.frames = 0; - this.type = 0; - this.songPosition = 0; -} +function pad(number) { + if (number < 10) { + return `0${number}`; + } -util.inherits(MTC, EventEmitter); + return number.toString(); +} -MTC.prototype.setSource = function setSource(sessionOrStream) { - sessionOrStream.on('message', (deltaTime, message) => { - if (message[0] === 0xf1) { - this.applyQuarterTime(message); - } else if ( - message[0] === 0xf0 - && message[1] === 0x7f - && message[3] === 0x01 - && message[4] === 0x01 - ) { - this.applyFullTime(message); - } else if (message[0] === 0xf2) { - this.applySongPosition(message); +class MTC extends EventEmitter { + constructor() { + super() + this.hours = 0; + this.minutes = 0; + this.seconds = 0; + this.frames = 0; + this.type = 0; + this.songPosition = 0; } - }); -}; -MTC.prototype.applySongPosition = function applySongPosition(message) { - const before = this.songPosition; - this.songPosition = message[2]; - this.songPosition <<= 7; - this.songPosition |= message[1]; - if (this.songPosition !== before) { - this.emit('change'); - } -}; + setSource(sessionOrStream) { + sessionOrStream.on('message', (deltaTime, message) => { + if (message[0] === 0xf1) { + this.applyQuarterTime(message); + } else if ( + message[0] === 0xf0 + && message[1] === 0x7f + && message[3] === 0x01 + && message[4] === 0x01 + ) { + this.applyFullTime(message); + } else if (message[0] === 0xf2) { + this.applySongPosition(message); + } + }); + } -MTC.prototype.applyFullTime = function applyFullTime(message) { - const originalString = this.toString(); + applySongPosition(message) { + const before = this.songPosition; + this.songPosition = message[2]; + this.songPosition <<= 7; + this.songPosition |= message[1]; + if (this.songPosition !== before) { + this.emit('change'); + } + } - this.type = (message[5] >> 5) & 0x3; + applyFullTime(message) { + const originalString = this.toString(); - this.hours = message[5] & 0x1f; - this.minutes = message[6]; - this.seconds = message[7]; - this.frames = message[8]; + this.type = (message[5] >> 5) & 0x3; - if (this.toString() !== originalString) { - this.emit('change'); - } -}; + this.hours = message[5] & 0x1f; + this.minutes = message[6]; + this.seconds = message[7]; + this.frames = message[8]; -// Build the MTC timestamp of 8 subsequent quarter time commands -// http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/mtc.htm -MTC.prototype.applyQuarterTime = function applyQuarterTime(message) { - const quarterTime = message[1]; - const type = (quarterTime >> 4) & 0x7; - let nibble = quarterTime & 0x0f; - let operator; + if (this.toString() !== originalString) { + this.emit('change'); + } + } - if (type % 2 === 0) { - // Low nibble - operator = 0xf0; - } else { - // High nibble - nibble <<= 4; - operator = 0x0f; - } + // Build the MTC timestamp of 8 subsequent quarter time commands + // http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/mtc.htm + applyQuarterTime(message) { + const quarterTime = message[1]; + const type = (quarterTime >> 4) & 0x7; + let nibble = quarterTime & 0x0f; + let operator; - switch (type) { - case 0: - case 1: - this.frames = this.frames & operator | nibble; - break; - case 2: - case 3: - this.seconds = this.seconds & operator | nibble; - break; - case 4: - case 5: - this.minutes = this.minutes & operator | nibble; - break; - case 6: - this.hours = this.hours & operator | nibble; - break; - case 7: - this.type = (nibble >> 5) & 0x3; - nibble &= 0x10; - this.hours = this.hours & operator | nibble; - this.emit('change'); - break; - default: - break; - } -}; + if (type % 2 === 0) { + // Low nibble + operator = 0xf0; + } else { + // High nibble + nibble <<= 4; + operator = 0x0f; + } -function pad(number) { - if (number < 10) { - return `0${number}`; - } + switch (type) { + case 0: + case 1: + this.frames = this.frames & operator | nibble; + break; + case 2: + case 3: + this.seconds = this.seconds & operator | nibble; + break; + case 4: + case 5: + this.minutes = this.minutes & operator | nibble; + break; + case 6: + this.hours = this.hours & operator | nibble; + break; + case 7: + this.type = (nibble >> 5) & 0x3; + nibble &= 0x10; + this.hours = this.hours & operator | nibble; + this.emit('change'); + break; + default: + break; + } + } - return number.toString(); + getSMTPEString() { + return `${pad(this.hours)}:${pad(this.minutes)}:${pad(this.seconds)}:${pad(this.frames)}`; + } } -MTC.prototype.getSMTPEString = function getSMTPEString() { - return `${pad(this.hours)}:${pad(this.minutes)}:${pad(this.seconds)}:${pad(this.frames)}`; -}; - module.exports = MTC; diff --git a/src/MidiMessage.js b/src/MidiMessage.js index 2c26b6a..f9369bb 100644 --- a/src/MidiMessage.js +++ b/src/MidiMessage.js @@ -3,7 +3,6 @@ /* eslint-disable consistent-return */ /* eslint-disable no-bitwise */ /* eslint-disable camelcase */ -const util = require('util'); const midiCommon = require('midi-common'); const logger = require('./logger'); @@ -22,321 +21,320 @@ function getDataLength(command) { return type ? type.dataLength || 0 : 0; } -function MidiMessage() { - RTPMessage.apply(this); - this.bigLength = false; - this.hasJournal = false; - this.firstHasDeltaTime = false; - this.p = false; - this.commands = []; - this.isValid = true; - this.payloadType = 0x61; -} - -util.inherits(MidiMessage, RTPMessage); - -MidiMessage.prototype.parseBuffer = function parseBuffer(...args) { - RTPMessage.prototype.parseBuffer.apply(this, args); - - const { payload } = this; - const firstByte = payload.readUInt8(0); - let offset; - let statusByte; - let lastStatusByte = null; - let hasOwnStatusByte; - let data_length; - - this.bigLength = !!(firstByte & flag_bigLength); - this.hasJournal = !!(firstByte & flag_hasJournal); - this.firstHasDeltaTime = !!(firstByte & flag_firstHasDeltaTime); - this.p = !!(firstByte & flag_p); +class MidiMessage extends RTPMessage { + constructor() { + super() + this.bigLength = false; + this.hasJournal = false; + this.firstHasDeltaTime = false; + this.p = false; + this.commands = []; + this.isValid = true; + this.payloadType = 0x61; + } - this.length = (firstByte & flag_maskLengthInFirstByte); + parseBuffer(...args) { + super.parseBuffer(...args); - if (this.bigLength) { - this.length = (this.length << 8) + payload.readUInt8(1); - } + const { payload } = this; + const firstByte = payload.readUInt8(0); + let offset; + let statusByte; + let lastStatusByte = null; + let hasOwnStatusByte; + let data_length; - // Read the command section - const commandStartOffset = this.bigLength ? 2 : 1; - offset = commandStartOffset; + this.bigLength = !!(firstByte & flag_bigLength); + this.hasJournal = !!(firstByte & flag_hasJournal); + this.firstHasDeltaTime = !!(firstByte & flag_firstHasDeltaTime); + this.p = !!(firstByte & flag_p); - while (offset < this.length + commandStartOffset - 1) { - const command = {}; - let deltaTime = 0; + this.length = (firstByte & flag_maskLengthInFirstByte); - // Decode the delta time - if (this.commands.length || this.firstHasDeltaTime) { - for (let k = 0; k < 4; k += 1) { - const currentOctet = payload.readUInt8(offset); + if (this.bigLength) { + this.length = (this.length << 8) + payload.readUInt8(1); + } - deltaTime <<= 7; - deltaTime |= currentOctet & flag_maskDeltaTimeByte; - offset += 1; - if (!(currentOctet & flag_deltaTimeHasNext)) { - break; + // Read the command section + const commandStartOffset = this.bigLength ? 2 : 1; + offset = commandStartOffset; + + while (offset < this.length + commandStartOffset - 1) { + const command = {}; + let deltaTime = 0; + + // Decode the delta time + if (this.commands.length || this.firstHasDeltaTime) { + for (let k = 0; k < 4; k += 1) { + const currentOctet = payload.readUInt8(offset); + + deltaTime <<= 7; + deltaTime |= currentOctet & flag_maskDeltaTimeByte; + offset += 1; + if (!(currentOctet & flag_deltaTimeHasNext)) { + break; + } + } + } + command.deltaTime = deltaTime; + + statusByte = payload.readUInt8(offset); + hasOwnStatusByte = (statusByte & 0x80) === 0x80; + if (hasOwnStatusByte) { + lastStatusByte = statusByte; + offset += 1; + } else if (lastStatusByte) { + statusByte = lastStatusByte; } - } - } - command.deltaTime = deltaTime; - - statusByte = payload.readUInt8(offset); - hasOwnStatusByte = (statusByte & 0x80) === 0x80; - if (hasOwnStatusByte) { - lastStatusByte = statusByte; - offset += 1; - } else if (lastStatusByte) { - statusByte = lastStatusByte; - } - // Parse SysEx - if (statusByte === 0xf0) { - data_length = 0; - while (payload.length > offset + data_length - && !(payload.readUInt8(offset + data_length) & 0x80)) { - data_length += 1; + // Parse SysEx + if (statusByte === 0xf0) { + data_length = 0; + while (payload.length > offset + data_length + && !(payload.readUInt8(offset + data_length) & 0x80)) { + data_length += 1; + } + if (payload.readUInt8(offset + data_length) !== 0xf7) { + data_length -= 1; + } + + data_length += 1; + } else { + data_length = getDataLength(statusByte); + } + command.data = Buffer.alloc(1 + data_length); + command.data[0] = statusByte; + if (payload.length < offset + data_length) { + this.isValid = false; + return; + } + if (data_length) { + payload.copy(command.data, 1, offset, offset + data_length); + offset += data_length; + } + if (!(command.data[0] === 0xf0 && command.data[command.data.length - 1] !== 0xf7)) { + this.commands.push(command); + } else { + return this; + } } - if (payload.readUInt8(offset + data_length) !== 0xf7) { - data_length -= 1; + if (this.hasJournal) { + this.journalOffset = offset; + this.journal = this.parseJournal(); } - - data_length += 1; - } else { - data_length = getDataLength(statusByte); - } - command.data = Buffer.alloc(1 + data_length); - command.data[0] = statusByte; - if (payload.length < offset + data_length) { - this.isValid = false; - return; - } - if (data_length) { - payload.copy(command.data, 1, offset, offset + data_length); - offset += data_length; - } - if (!(command.data[0] === 0xf0 && command.data[command.data.length - 1] !== 0xf7)) { - this.commands.push(command); - } else { return this; } - } - if (this.hasJournal) { - this.journalOffset = offset; - this.journal = this.parseJournal(); - } - return this; -}; - -MidiMessage.prototype.parseJournal = function parseJournal() { - let offset = this.journalOffset; - const { payload } = this; - let presentChapters; - - const journal = {}; - const journalHeader = payload[offset]; - - journal.singlePacketLoss = !!(journalHeader & 0x80); - journal.hasSystemJournal = !!(journalHeader & 0x40); - journal.hasChannelJournal = !!(journalHeader & 0x20); - journal.enhancedEncoding = !!(journalHeader & 0x10); - - journal.checkPointPacketSequenceNumber = payload.readUInt16BE(offset + 1); - journal.channelJournals = []; - - offset += 3; - - if (journal.hasSystemJournal) { - const systemJournal = {}; - journal.systemJournal = {}; - presentChapters = {}; - systemJournal.presentChapters = {}; - presentChapters.S = !!(payload[offset] & 0x80); - presentChapters.D = !!(payload[offset] & 0x40); - presentChapters.V = !!(payload[offset] & 0x20); - presentChapters.Q = !!(payload[offset] & 0x10); - presentChapters.F = !!(payload[offset] & 0x08); - presentChapters.X = !!(payload[offset] & 0x04); - systemJournal.length = ((payload[offset] & 0x3) << 8) | payload[offset + 1]; - offset += systemJournal.length; - } - - if (journal.hasChannelJournal) { - let channel = 0; - let channelJournal; - - journal.totalChannels = (journalHeader & 0x0f) + 1; - while (channel < journal.totalChannels && offset < payload.length) { - channelJournal = {}; - channelJournal.channel = (payload[offset] >> 3) & 0x0f; - channelJournal.s = !!(payload[offset] & 0x80); - channelJournal.h = !!(payload[offset] & 0x01); - channelJournal.length = ((payload[offset] & 0x3) << 8) | payload[offset + 1]; - presentChapters = {}; - channelJournal.presentChapters = {}; - presentChapters.P = !!(payload[offset + 2] & 0x80); - presentChapters.C = !!(payload[offset + 2] & 0x40); - presentChapters.M = !!(payload[offset + 2] & 0x20); - presentChapters.W = !!(payload[offset + 2] & 0x10); - presentChapters.N = !!(payload[offset + 2] & 0x08); - presentChapters.E = !!(payload[offset + 2] & 0x04); - presentChapters.T = !!(payload[offset + 2] & 0x02); - presentChapters.A = !!(payload[offset + 2] & 0x01); - - offset += channelJournal.length; - - journal.channelJournals.push(channelJournal); - - channel += 1; - } - } - return journal; -}; - -MidiMessage.prototype.generateBuffer = function generateBuffer() { - let payloadLength = 1; - let payloadOffset = 0; - let i; - let k; - let command; - let commandData; - let commandDataLength; - let commandDeltaTime; - let commandStatusByte = null; - let expectedDataLength; - let lastStatusByte; - let bitmask; - - this.firstHasDeltaTime = true; - - for (i = 0; i < this.commands.length; i += 1) { - command = this.commands[i]; - command._length = 0; - commandData = command.data; - commandDataLength = commandData.length; - - - if (i === 0 && command.deltaTime === 0) { - this.firstHasDeltaTime = false; - } else { - commandDeltaTime = Math.round(command.deltaTime); - if (commandDeltaTime >= 0x7f7f7f) { - command._length += 1; - } - if (commandDeltaTime >= 0x7f7f) { - command._length += 1; + parseJournal() { + let offset = this.journalOffset; + const { payload } = this; + let presentChapters; + + const journal = {}; + const journalHeader = payload[offset]; + + journal.singlePacketLoss = !!(journalHeader & 0x80); + journal.hasSystemJournal = !!(journalHeader & 0x40); + journal.hasChannelJournal = !!(journalHeader & 0x20); + journal.enhancedEncoding = !!(journalHeader & 0x10); + + journal.checkPointPacketSequenceNumber = payload.readUInt16BE(offset + 1); + journal.channelJournals = []; + + offset += 3; + + if (journal.hasSystemJournal) { + const systemJournal = {}; + journal.systemJournal = {}; + presentChapters = {}; + systemJournal.presentChapters = {}; + presentChapters.S = !!(payload[offset] & 0x80); + presentChapters.D = !!(payload[offset] & 0x40); + presentChapters.V = !!(payload[offset] & 0x20); + presentChapters.Q = !!(payload[offset] & 0x10); + presentChapters.F = !!(payload[offset] & 0x08); + presentChapters.X = !!(payload[offset] & 0x04); + systemJournal.length = ((payload[offset] & 0x3) << 8) | payload[offset + 1]; + offset += systemJournal.length; } - if (commandDeltaTime >= 0x7f) { - command._length += 1; - } - command._length += 1; - } - commandStatusByte = command.data[0]; - if (commandStatusByte === 0xf0) { - expectedDataLength = 0; - while (expectedDataLength + 1 < commandDataLength - && command.data[expectedDataLength] !== 0xf7) { - expectedDataLength += 1; - } - } else { - expectedDataLength = getDataLength(commandStatusByte); - } - - if (expectedDataLength + 1 !== commandDataLength) { - command._length = 0; - } else { - command._length += expectedDataLength; - if (commandStatusByte !== lastStatusByte) { - command._hasOwnStatusByte = true; - lastStatusByte = commandStatusByte; - command._length += 1; - } else { - command._hasOwnStatusByte = false; + if (journal.hasChannelJournal) { + let channel = 0; + let channelJournal; + + journal.totalChannels = (journalHeader & 0x0f) + 1; + while (channel < journal.totalChannels && offset < payload.length) { + channelJournal = {}; + channelJournal.channel = (payload[offset] >> 3) & 0x0f; + channelJournal.s = !!(payload[offset] & 0x80); + channelJournal.h = !!(payload[offset] & 0x01); + channelJournal.length = ((payload[offset] & 0x3) << 8) | payload[offset + 1]; + presentChapters = {}; + channelJournal.presentChapters = {}; + presentChapters.P = !!(payload[offset + 2] & 0x80); + presentChapters.C = !!(payload[offset + 2] & 0x40); + presentChapters.M = !!(payload[offset + 2] & 0x20); + presentChapters.W = !!(payload[offset + 2] & 0x10); + presentChapters.N = !!(payload[offset + 2] & 0x08); + presentChapters.E = !!(payload[offset + 2] & 0x04); + presentChapters.T = !!(payload[offset + 2] & 0x02); + presentChapters.A = !!(payload[offset + 2] & 0x01); + + offset += channelJournal.length; + + journal.channelJournals.push(channelJournal); + + channel += 1; + } } - payloadLength += command._length; + return journal; } - } - const length = payloadLength - 1; - - this.bigLength = length > 15; - - if (this.bigLength) { - payloadLength += 1; - } - - const payload = Buffer.alloc(payloadLength); - bitmask = 0; - bitmask |= this.hasJournal ? flag_hasJournal : 0; - bitmask |= this.firstHasDeltaTime ? flag_firstHasDeltaTime : 0; - bitmask |= this.p ? flag_p : 0; - - if (this.bigLength) { - bitmask |= flag_bigLength; - bitmask |= 0x0f & (length >> 8); - payload[1] = 0xff & (length); - payloadOffset += 1; - } else { - bitmask |= 0x0f & (length); - } - - payload[0] = bitmask; - payloadOffset += 1; - - for (i = 0; i < this.commands.length; i += 1) { - command = this.commands[i]; - - if (command._length > 0) { - if (i > 0 || this.firstHasDeltaTime) { - commandDeltaTime = Math.round(command.deltaTime); - - if (commandDeltaTime >= 0x7f7f7f) { - payloadOffset += 1; - payload.writeUInt8(0x80 | (0x7f & (commandDeltaTime >> 21)), payloadOffset); + generateBuffer() { + let payloadLength = 1; + let payloadOffset = 0; + let i; + let k; + let command; + let commandData; + let commandDataLength; + let commandDeltaTime; + let commandStatusByte = null; + let expectedDataLength; + let lastStatusByte; + let bitmask; + + this.firstHasDeltaTime = true; + + for (i = 0; i < this.commands.length; i += 1) { + command = this.commands[i]; + command._length = 0; + commandData = command.data; + commandDataLength = commandData.length; + + + if (i === 0 && command.deltaTime === 0) { + this.firstHasDeltaTime = false; + } else { + commandDeltaTime = Math.round(command.deltaTime); + + if (commandDeltaTime >= 0x7f7f7f) { + command._length += 1; + } + if (commandDeltaTime >= 0x7f7f) { + command._length += 1; + } + if (commandDeltaTime >= 0x7f) { + command._length += 1; + } + command._length += 1; } - if (commandDeltaTime >= 0x7f7f) { - payloadOffset += 1; - payload.writeUInt8(0x80 | (0x7f & (commandDeltaTime >> 14)), payloadOffset); + commandStatusByte = command.data[0]; + + if (commandStatusByte === 0xf0) { + expectedDataLength = 0; + while (expectedDataLength + 1 < commandDataLength + && command.data[expectedDataLength] !== 0xf7) { + expectedDataLength += 1; + } + } else { + expectedDataLength = getDataLength(commandStatusByte); } - if (commandDeltaTime >= 0x7f) { - payloadOffset += 1; - payload.writeUInt8(0x80 | (0x7f & (commandDeltaTime >> 7)), payloadOffset); + + if (expectedDataLength + 1 !== commandDataLength) { + command._length = 0; + } else { + command._length += expectedDataLength; + if (commandStatusByte !== lastStatusByte) { + command._hasOwnStatusByte = true; + lastStatusByte = commandStatusByte; + command._length += 1; + } else { + command._hasOwnStatusByte = false; + } + payloadLength += command._length; } - payloadOffset += 1; - payload.writeUInt8(0x7f & commandDeltaTime, payloadOffset); + } + const length = payloadLength - 1; + + this.bigLength = length > 15; + + if (this.bigLength) { + payloadLength += 1; } - commandData = command.data; - commandDataLength = commandData.length; + const payload = Buffer.alloc(payloadLength); - k = command._hasOwnStatusByte ? 0 : 1; + bitmask = 0; + bitmask |= this.hasJournal ? flag_hasJournal : 0; + bitmask |= this.firstHasDeltaTime ? flag_firstHasDeltaTime : 0; + bitmask |= this.p ? flag_p : 0; - while (k < commandDataLength) { - // eslint-disable-next-line no-plusplus - payload[payloadOffset] = commandData[k]; // Breaks if ++ before + if (this.bigLength) { + bitmask |= flag_bigLength; + bitmask |= 0x0f & (length >> 8); + payload[1] = 0xff & (length); payloadOffset += 1; - k += 1; + } else { + bitmask |= 0x0f & (length); } - } else { - logger.warn('Ignoring invalid command'); - } - } - this.payload = payload; + payload[0] = bitmask; + payloadOffset += 1; + + for (i = 0; i < this.commands.length; i += 1) { + command = this.commands[i]; + + if (command._length > 0) { + if (i > 0 || this.firstHasDeltaTime) { + commandDeltaTime = Math.round(command.deltaTime); + + if (commandDeltaTime >= 0x7f7f7f) { + payloadOffset += 1; + payload.writeUInt8(0x80 | (0x7f & (commandDeltaTime >> 21)), payloadOffset); + } + if (commandDeltaTime >= 0x7f7f) { + payloadOffset += 1; + payload.writeUInt8(0x80 | (0x7f & (commandDeltaTime >> 14)), payloadOffset); + } + if (commandDeltaTime >= 0x7f) { + payloadOffset += 1; + payload.writeUInt8(0x80 | (0x7f & (commandDeltaTime >> 7)), payloadOffset); + } + payloadOffset += 1; + payload.writeUInt8(0x7f & commandDeltaTime, payloadOffset); + } + + commandData = command.data; + commandDataLength = commandData.length; + + k = command._hasOwnStatusByte ? 0 : 1; + + while (k < commandDataLength) { + // eslint-disable-next-line no-plusplus + payload[payloadOffset] = commandData[k]; // Breaks if ++ before + payloadOffset += 1; + k += 1; + } + } else { + logger.warn('Ignoring invalid command'); + } + } - RTPMessage.prototype.generateBuffer.apply(this); - return this; -}; + this.payload = payload; + super.generateBuffer(); + return this; + } -MidiMessage.prototype.toJSON = function toJSON() { - return { - commands: this.commands.map(command => ({ - deltaTime: command.deltaTime, - data: Array.prototype.slice.apply(command.data), - })), - }; -}; + toJSON() { + return { + commands: this.commands.map(command => ({ + deltaTime: command.deltaTime, + data: Array.prototype.slice.apply(command.data), + })), + }; + } +} module.exports = MidiMessage; diff --git a/src/RTPMessage.js b/src/RTPMessage.js index 2ae2526..48db612 100644 --- a/src/RTPMessage.js +++ b/src/RTPMessage.js @@ -1,122 +1,120 @@ /* eslint-disable no-mixed-operators */ /* eslint-disable no-bitwise */ -const util = require('util'); - const AbstractMessage = require('./AbstractMessage'); /** * This represents a RTP Protocol message. * @constructor */ -function RTPMessage() { - AbstractMessage.apply(this); - this.csrcs = []; +class RTPMessage extends AbstractMessage { + constructor() { + super() + this.csrcs = []; + + this.version = 2; + this.padding = false; + this.hasExtension = false; + this.csrcCount = 0; + this.marker = false; + this.payloadType = 0; + this.sequenceNumber = 0; + this.timestamp = 0; + this.ssrc = 0; + this.payload = Buffer.alloc(0); + } + + /** + * Parses a Buffer into this RTPMessage object + * @param {Buffer} The buffer containing a RTP AbstractMessage + * @returns {Buffer} self + */ + parseBuffer(buffer, ...args) { + let currentOffset; + + super.parseBuffer(...args); + const firstByte = buffer.readUInt8(0); + + this.version = firstByte >>> 6; + this.padding = !!(firstByte >>> 5 & 1); + this.hasExtension = !!((firstByte >>> 4) & 1); + this.csrcCount = firstByte & 0xF; + + const secondByte = buffer.readUInt8(1); + this.marker = (secondByte & 0x80) === 0x80; + this.payloadType = secondByte & 0x7f; + + this.sequenceNumber = buffer.readUInt16BE(2); + this.timestamp = buffer.readUInt32BE(4); + this.ssrc = buffer.readUInt32BE(8); + currentOffset = 12; + for (let i = 0; i < this.csrcCount; i += 2) { + this.csrcs.push(buffer.readUInt32BE(currentOffset)); + } + if (this.hasExtension) { + this.extensionHeaderId = buffer.readUInt16BE(currentOffset); + currentOffset += 2; + this.extensionHeaderLength = buffer.readUInt16BE(currentOffset); + currentOffset += 2; + this.extension = buffer.slice(currentOffset, currentOffset += this.extensionHeaderLength / 32); + } + this.payload = buffer.slice(currentOffset); + + return this; + } + + /** + * Generates the buffer of the message. It is then available as the .buffer property. + * @returns {RTPMessage} self + */ + generateBuffer() { + let bufferLength = 12; + let i; + let length; + + bufferLength += ((this.csrcs.length > 15 ? 15 : this.csrcs.length) * 15); + if (this.hasExtension) { + bufferLength += 4 * (this.extension.length + 1); + } + const payLoadOffset = bufferLength; + if (Buffer.isBuffer(this.payload)) { + bufferLength += this.payload.length; + } + + const buffer = Buffer.alloc(bufferLength); + + let firstByte = 0; + firstByte |= this.version << 6; + firstByte |= this.padding ? 0x20 : 0; + firstByte |= this.hasExtension ? 0x10 : 0; + firstByte |= (this.csrcs.length > 15 ? 15 : this.csrcs.length); + + const secondByte = this.payloadType | (this.marker ? 0x80 : 0); + + buffer.writeUInt8(firstByte, 0); + buffer.writeUInt8(secondByte, 1); + buffer.writeUInt16BE(this.sequenceNumber, 2); + buffer.writeUInt32BE(this.timestamp << 0, 4); + + buffer.writeUInt32BE(this.ssrc, 8); + + for (i = 0; i < this.csrcs && i < 15; i += 1) { + buffer.writeUInt32BE(this.csrcs[i], 12 + (4 * i)); + } + + if (this.hasExtension) { + length = Math.ceil(this.extension.length / 32); + buffer.writeUInt16BE(this.extensionHeaderId, 12 + (4 * i)); + buffer.writeUInt16BE(length, 14 + (4 * i)); + this.extension.copy(buffer, 16 + (4 * i)); + } + + if (Buffer.isBuffer(this.payload)) { + this.payload.copy(buffer, payLoadOffset); + } + + this.buffer = buffer; + return this; + } } -util.inherits(RTPMessage, AbstractMessage); - -RTPMessage.prototype.version = 2; -RTPMessage.prototype.padding = false; -RTPMessage.prototype.hasExtension = false; -RTPMessage.prototype.csrcCount = 0; -RTPMessage.prototype.marker = false; -RTPMessage.prototype.payloadType = 0; -RTPMessage.prototype.sequenceNumber = 0; -RTPMessage.prototype.timestamp = 0; -RTPMessage.prototype.ssrc = 0; -RTPMessage.prototype.payload = Buffer.alloc(0); - -/** -* Parses a Buffer into this RTPMessage object -* @param {Buffer} The buffer containing a RTP AbstractMessage -* @returns {Buffer} self -*/ -RTPMessage.prototype.parseBuffer = function parseBuffer(buffer, ...args) { - let currentOffset; - - AbstractMessage.prototype.parseBuffer.apply(this, args); - const firstByte = buffer.readUInt8(0); - - this.version = firstByte >>> 6; - this.padding = !!(firstByte >>> 5 & 1); - this.hasExtension = !!((firstByte >>> 4) & 1); - this.csrcCount = firstByte & 0xF; - - const secondByte = buffer.readUInt8(1); - this.marker = (secondByte & 0x80) === 0x80; - this.payloadType = secondByte & 0x7f; - - this.sequenceNumber = buffer.readUInt16BE(2); - this.timestamp = buffer.readUInt32BE(4); - this.ssrc = buffer.readUInt32BE(8); - currentOffset = 12; - for (let i = 0; i < this.csrcCount; i += 2) { - this.csrcs.push(buffer.readUInt32BE(currentOffset)); - } - if (this.hasExtension) { - this.extensionHeaderId = buffer.readUInt16BE(currentOffset); - currentOffset += 2; - this.extensionHeaderLength = buffer.readUInt16BE(currentOffset); - currentOffset += 2; - this.extension = buffer.slice(currentOffset, currentOffset += this.extensionHeaderLength / 32); - } - this.payload = buffer.slice(currentOffset); - - return this; -}; - -/** -* Generates the buffer of the message. It is then available as the .buffer property. -* @returns {RTPMessage} self -*/ -RTPMessage.prototype.generateBuffer = function generateBuffer() { - let bufferLength = 12; - let i; - let length; - - bufferLength += ((this.csrcs.length > 15 ? 15 : this.csrcs.length) * 15); - if (this.hasExtension) { - bufferLength += 4 * (this.extension.length + 1); - } - const payLoadOffset = bufferLength; - if (Buffer.isBuffer(this.payload)) { - bufferLength += this.payload.length; - } - - const buffer = Buffer.alloc(bufferLength); - - let firstByte = 0; - firstByte |= this.version << 6; - firstByte |= this.padding ? 0x20 : 0; - firstByte |= this.hasExtension ? 0x10 : 0; - firstByte |= (this.csrcs.length > 15 ? 15 : this.csrcs.length); - - const secondByte = this.payloadType | (this.marker ? 0x80 : 0); - - buffer.writeUInt8(firstByte, 0); - buffer.writeUInt8(secondByte, 1); - buffer.writeUInt16BE(this.sequenceNumber, 2); - buffer.writeUInt32BE(this.timestamp << 0, 4); - - buffer.writeUInt32BE(this.ssrc, 8); - - for (i = 0; i < this.csrcs && i < 15; i += 1) { - buffer.writeUInt32BE(this.csrcs[i], 12 + (4 * i)); - } - - if (this.hasExtension) { - length = Math.ceil(this.extension.length / 32); - buffer.writeUInt16BE(this.extensionHeaderId, 12 + (4 * i)); - buffer.writeUInt16BE(length, 14 + (4 * i)); - this.extension.copy(buffer, 16 + (4 * i)); - } - - if (Buffer.isBuffer(this.payload)) { - this.payload.copy(buffer, payLoadOffset); - } - - this.buffer = buffer; - return this; -}; - module.exports = RTPMessage; diff --git a/src/Session.js b/src/Session.js index e2b120a..79fff97 100644 --- a/src/Session.js +++ b/src/Session.js @@ -1,4 +1,3 @@ -const util = require('util'); const { EventEmitter } = require('events'); const dgram = require('dgram'); @@ -8,308 +7,308 @@ const MdnsService = require('./mdns'); const logger = require('./logger'); const Stream = require('./Stream'); -function Session(port, localName, bonjourName, ssrc, published, ipVersion) { - EventEmitter.apply(this); - // RTP related - this.streams = []; - this.localName = localName; - this.bonjourName = bonjourName; - this.port = port || 5004; - this.ssrc = ssrc || Math.round(Math.random() * (2 ** (8 * 4))); - this.readyState = 0; - this.published = !!published; - // State - this.bundle = true; - this.queue = []; - this.flushQueued = false; - this.lastFlush = 0; - this.lastMessageTime = 0; - // IPV - this.ipVersion = ipVersion === 6 ? 6 : 4; - // Streams - this.streamConnected = this.streamConnected.bind(this); - this.streamDisconnected = this.streamDisconnected.bind(this); - this.deliverMessage = this.deliverMessage.bind(this); - // Socket handling - this.controlChannel = dgram.createSocket({ - type: `udp${this.ipVersion}`, - reuseAddr: true, - }); - this.controlChannel.on('message', this.handleMessage.bind(this)); - this.controlChannel.on('listening', this.listening.bind(this)); - this.controlChannel.on('error', this.emit.bind(this, 'error')); - - this.messageChannel = dgram.createSocket({ - type: `udp${this.ipVersion}`, - reuseAddr: true, - }); - this.messageChannel.on('message', this.handleMessage.bind(this)); - this.messageChannel.on('listening', this.listening.bind(this)); - this.messageChannel.on('error', this.emit.bind(this, 'error')); - // Message delivery Rate - this.rate = 10000; - // Start timing - this.startTime = Date.now() / 1000 * this.rate; - this.startTimeHr = process.hrtime(); -} +class Session extends EventEmitter { + constructor(port, localName, bonjourName, ssrc, published, ipVersion) { + super() + // RTP related + this.streams = []; + this.localName = localName; + this.bonjourName = bonjourName; + this.port = port || 5004; + this.ssrc = ssrc || Math.round(Math.random() * (2 ** (8 * 4))); + this.readyState = 0; + this.published = !!published; + // State + this.bundle = true; + this.queue = []; + this.flushQueued = false; + this.lastFlush = 0; + this.lastMessageTime = 0; + // IPV + this.ipVersion = ipVersion === 6 ? 6 : 4; + // Streams + this.streamConnected = this.streamConnected.bind(this); + this.streamDisconnected = this.streamDisconnected.bind(this); + this.deliverMessage = this.deliverMessage.bind(this); + // Socket handling + this.controlChannel = dgram.createSocket({ + type: `udp${this.ipVersion}`, + reuseAddr: true, + }); + this.controlChannel.on('message', this.handleMessage.bind(this)); + this.controlChannel.on('listening', this.listening.bind(this)); + this.controlChannel.on('error', this.emit.bind(this, 'error')); + + this.messageChannel = dgram.createSocket({ + type: `udp${this.ipVersion}`, + reuseAddr: true, + }); + this.messageChannel.on('message', this.handleMessage.bind(this)); + this.messageChannel.on('listening', this.listening.bind(this)); + this.messageChannel.on('error', this.emit.bind(this, 'error')); + // Message delivery Rate + this.rate = 10000; + // Start timing + this.startTime = Date.now() / 1000 * this.rate; + this.startTimeHr = process.hrtime(); + } + + start() { + if (this.published) { + this.on('ready', () => this.publish()); + } + // Bind channels to session port + if(!this.controlChannel._bindState){ + this.controlChannel.bind(this.port, this.ipVersion == 4 ? '0.0.0.0' : '::'); + this.messageChannel.bind(this.port + 1, this.ipVersion == 4 ? '0.0.0.0' : '::'); + } + + + } -util.inherits(Session, EventEmitter); + end(callback) { + let i = -1; + const onClose = () => { + this.readyState -= 1; + if (this.readyState <= 0) { + callback && callback(); + } + }; + const next = () => { + i += 1; + const stream = this.streams[i]; + if (stream) { + stream.end(next); + } else { + this.unpublish(); + + this.controlChannel.on('close', onClose); + this.messageChannel.on('close', onClose); + + this.controlChannel.close(); + this.messageChannel.close(); + this.published = false; + } + }; + + if (this.readyState === 2) { + next(); + } else { + callback && callback(); + } + } -Session.prototype.start = function start() { - if (this.published) { - this.on('ready', () => this.publish()); - } - // Bind channels to session port - if(!this.controlChannel._bindState){ - this.controlChannel.bind(this.port, this.ipVersion == 4 ? '0.0.0.0' : '::'); - this.messageChannel.bind(this.port + 1, this.ipVersion == 4 ? '0.0.0.0' : '::'); - } + now() { + const hrtime = process.hrtime(this.startTimeHr); + return Math.round( + ((hrtime[0] + hrtime[1] / 1000 / 1000 / 1000)) * this.rate, + ) % 0xffffffff; + } + listening() { + this.readyState += 1; + if (this.readyState === 2) { + this.emit('ready'); + } + } -}; + handleMessage(message, rinfo) { + logger.debug('Incoming Message = ', message); + const appleMidiMessage = new ControlMessage().parseBuffer(message); + let stream; + if (appleMidiMessage.isValid) { + stream = this.streams.filter( + streamItem => (streamItem.ssrc === appleMidiMessage.ssrc) + || (streamItem.token === appleMidiMessage.token), + ).pop(); + this.emit('controlMessage', appleMidiMessage); + + + if (!stream && appleMidiMessage.command === 'invitation') { + stream = new Stream(this); + stream.handleControlMessage(appleMidiMessage, rinfo); + this.addStream(stream); + } else if (stream) { + stream.handleControlMessage(appleMidiMessage, rinfo); + } + } else { + const rtpMidiMessage = new MidiMessage().parseBuffer(message); + if(!rtpMidiMessage){ + return; + } + stream = this.streams.filter( + streamItem => streamItem.ssrc === rtpMidiMessage.ssrc, + ).pop(); + if (stream) { + stream.handleMidiMessage(rtpMidiMessage); + } + this.emit('midi', rtpMidiMessage); + } + } -Session.prototype.end = function end(callback) { - let i = -1; - const onClose = () => { - this.readyState -= 1; - if (this.readyState <= 0) { - callback && callback(); + sendUdpMessage(rinfo, message, callback) { + message.generateBuffer(); + + if (message.isValid) { + try { + ( + rinfo.port % 2 === 0 ? this.controlChannel : this.messageChannel + ).send( + message.buffer, + 0, + message.buffer.length, + rinfo.port, rinfo.address, + () => { + logger.debug('Outgoing Message = ', message.buffer, rinfo.port, rinfo.address); + callback && callback(); + }, + ); + } catch (error) { + logger.error(error); + } + } else { + logger.warn('Ignoring invalid message', message); + } } - }; - const next = () => { - i += 1; - const stream = this.streams[i]; - if (stream) { - stream.end(next); - } else { - this.unpublish(); - - this.controlChannel.on('close', onClose); - this.messageChannel.on('close', onClose); - - this.controlChannel.close(); - this.messageChannel.close(); - this.published = false; + + queueFlush() { + if (this.bundle) { + if (!this.flushQueued) { + this.flushQueued = true; + setImmediate(this.flushQueue.bind(this)); + } + } else { + this.flushQueue(); + } } - }; - - if (this.readyState === 2) { - next(); - } else { - callback && callback(); - } -}; - -Session.prototype.now = function now() { - const hrtime = process.hrtime(this.startTimeHr); - return Math.round( - ((hrtime[0] + hrtime[1] / 1000 / 1000 / 1000)) * this.rate, - ) % 0xffffffff; -}; - -Session.prototype.listening = function listening() { - this.readyState += 1; - if (this.readyState === 2) { - this.emit('ready'); - } -}; - -Session.prototype.handleMessage = function handleMessage(message, rinfo) { - logger.debug('Incoming Message = ', message); - const appleMidiMessage = new ControlMessage().parseBuffer(message); - let stream; - if (appleMidiMessage.isValid) { - stream = this.streams.filter( - streamItem => (streamItem.ssrc === appleMidiMessage.ssrc) - || (streamItem.token === appleMidiMessage.token), - ).pop(); - this.emit('controlMessage', appleMidiMessage); - - - if (!stream && appleMidiMessage.command === 'invitation') { - stream = new Stream(this); - stream.handleControlMessage(appleMidiMessage, rinfo); + + flushQueue() { + const streams = this.getStreams(); + const queue = this.queue.slice(0); + const now = this.now(); + + this.queue.length = 0; + this.flushQueued = false; + + queue.sort((a, b) => (a.comexTime - b.comexTime)); + + let messageTime = queue[0].comexTime; + + if (messageTime > now) { + messageTime = now; + } + + queue.forEach((message) => { + // eslint-disable-next-line no-param-reassign + message.deltaTime = message.comexTime - messageTime; + }); + + const message = { + timestamp: now, + commands: queue, + }; + + for (let i = 0; i < streams.length; i += 1) { + streams[i].sendMessage(message); + } + } + + sendMessage(comexTime, command, ...args) { + let cTime = comexTime; + let cmd; + + if (arguments.length === 1) { + cTime = this.now(); + command = comexTime; // Picks first arg using array destructing + } else { + cTime = comexTime - this.startTime; + } + + if (!Buffer.isBuffer(command)) { + cmd = Buffer.from(command); + } + + this.queue.push({ comexTime: cTime, data: cmd }); + this.queueFlush(); + } + + connect(rinfo) { + const stream = new Stream(this); + const info = { + address: (this.ipVersion === 6 && rinfo.addressV6) ? rinfo.addressV6 : rinfo.address, + port: rinfo.port, + }; + this.addStream(stream); - } else if (stream) { - stream.handleControlMessage(appleMidiMessage, rinfo); + stream.connect(info); } - } else { - const rtpMidiMessage = new MidiMessage().parseBuffer(message); - if(!rtpMidiMessage){ - return; + + streamConnected(event) { + this.emit('streamAdded', { + stream: event.stream, + }); } - stream = this.streams.filter( - streamItem => streamItem.ssrc === rtpMidiMessage.ssrc, - ).pop(); - if (stream) { - stream.handleMidiMessage(rtpMidiMessage); + + streamDisconnected(event) { + this.removeStream(event.stream); + this.emit('streamRemoved', { + stream: event.stream, + }); } - this.emit('midi', rtpMidiMessage); - } -}; - -Session.prototype.sendUdpMessage = function sendMessage(rinfo, message, callback) { - message.generateBuffer(); - - if (message.isValid) { - try { - ( - rinfo.port % 2 === 0 ? this.controlChannel : this.messageChannel - ).send( - message.buffer, - 0, - message.buffer.length, - rinfo.port, rinfo.address, - () => { - logger.debug('Outgoing Message = ', message.buffer, rinfo.port, rinfo.address); - callback && callback(); - }, - ); - } catch (error) { - logger.error(error); + + addStream(stream) { + stream.on('connected', this.streamConnected); + stream.on('disconnected', this.streamDisconnected); + stream.on('message', this.deliverMessage); + this.streams.push(stream); + } + + removeStream(stream) { + stream.removeListener('connected', this.streamConnected); + stream.removeListener('disconnected', this.streamDisconnected); + stream.removeListener('message', this.deliverMessage); + this.streams.splice(this.streams.indexOf(stream)); } - } else { - logger.warn('Ignoring invalid message', message); - } -}; - -Session.prototype.queueFlush = function queueFlush() { - if (this.bundle) { - if (!this.flushQueued) { - this.flushQueued = true; - setImmediate(this.flushQueue.bind(this)); + + deliverMessage(comexTime, message) { + this.lastMessageTime = this.lastMessageTime || comexTime; + const deltaTime = comexTime - this.lastMessageTime; + this.lastMessageTime = comexTime; + this.emit('message', deltaTime / this.rate, message, comexTime + this.startTime); + } + + getStreams() { + return this.streams.filter(item => item.isConnected); } - } else { - this.flushQueue(); - } -}; - -Session.prototype.flushQueue = function flushQueue() { - const streams = this.getStreams(); - const queue = this.queue.slice(0); - const now = this.now(); - - this.queue.length = 0; - this.flushQueued = false; - - queue.sort((a, b) => (a.comexTime - b.comexTime)); - - let messageTime = queue[0].comexTime; - - if (messageTime > now) { - messageTime = now; - } - - queue.forEach((message) => { - // eslint-disable-next-line no-param-reassign - message.deltaTime = message.comexTime - messageTime; - }); - - const message = { - timestamp: now, - commands: queue, - }; - - for (let i = 0; i < streams.length; i += 1) { - streams[i].sendMessage(message); - } -}; - -Session.prototype.sendMessage = function sendMessage(comexTime, command, ...args) { - let cTime = comexTime; - let cmd; - - if (arguments.length === 1) { - cTime = this.now(); - command = comexTime; // Picks first arg using array destructing - } else { - cTime = comexTime - this.startTime; - } - - if (!Buffer.isBuffer(command)) { - cmd = Buffer.from(command); - } - - this.queue.push({ comexTime: cTime, data: cmd }); - this.queueFlush(); -}; - -Session.prototype.connect = function connect(rinfo) { - const stream = new Stream(this); - const info = { - address: (this.ipVersion === 6 && rinfo.addressV6) ? rinfo.addressV6 : rinfo.address, - port: rinfo.port, - }; - - this.addStream(stream); - stream.connect(info); -}; - -Session.prototype.streamConnected = function streamConnected(event) { - this.emit('streamAdded', { - stream: event.stream, - }); -}; - -Session.prototype.streamDisconnected = function streamDisconnected(event) { - this.removeStream(event.stream); - this.emit('streamRemoved', { - stream: event.stream, - }); -}; - -Session.prototype.addStream = function addStream(stream) { - stream.on('connected', this.streamConnected); - stream.on('disconnected', this.streamDisconnected); - stream.on('message', this.deliverMessage); - this.streams.push(stream); -}; - -Session.prototype.removeStream = function removeStream(stream) { - stream.removeListener('connected', this.streamConnected); - stream.removeListener('disconnected', this.streamDisconnected); - stream.removeListener('message', this.deliverMessage); - this.streams.splice(this.streams.indexOf(stream)); -}; - -Session.prototype.deliverMessage = function deliverMessage(comexTime, message) { - this.lastMessageTime = this.lastMessageTime || comexTime; - const deltaTime = comexTime - this.lastMessageTime; - this.lastMessageTime = comexTime; - this.emit('message', deltaTime / this.rate, message, comexTime + this.startTime); -}; - -Session.prototype.getStreams = function getStreams() { - return this.streams.filter(item => item.isConnected); -}; - -Session.prototype.getStream = function getStream(ssrc) { - for (let i = 0; i < this.streams.length; i += 1) { - if (this.streams[i].ssrc === ssrc) { - return this.streams[i]; + + getStream(ssrc) { + for (let i = 0; i < this.streams.length; i += 1) { + if (this.streams[i].ssrc === ssrc) { + return this.streams[i]; + } + } + return null; + } + + publish() { + MdnsService.publish(this); } - } - return null; -}; - -Session.prototype.publish = function publish() { - MdnsService.publish(this); -}; - -Session.prototype.unpublish = function unpublish() { - MdnsService.unpublish(this); -}; - -Session.prototype.toJSON = function toJSON(includeStreams) { - return { - bonjourName: this.bonjourName, - localName: this.localName, - ssrc: this.ssrc, - port: this.port, - published: this.published, - activated: this.readyState >= 2, - streams: includeStreams ? this.getStreams().map(stream => stream.toJSON()) : undefined, - }; -}; + + unpublish() { + MdnsService.unpublish(this); + } + + toJSON(includeStreams) { + return { + bonjourName: this.bonjourName, + localName: this.localName, + ssrc: this.ssrc, + port: this.port, + published: this.published, + activated: this.readyState >= 2, + streams: includeStreams ? this.getStreams().map(stream => stream.toJSON()) : undefined, + }; + } +} module.exports = Session; diff --git a/src/Stream.js b/src/Stream.js index ef0f98b..f3e68ef 100644 --- a/src/Stream.js +++ b/src/Stream.js @@ -1,4 +1,3 @@ -const util = require('util'); const { EventEmitter } = require('events'); const ControlMessage = require('./ControlMessage.js'); @@ -35,330 +34,330 @@ function readUInt64BE(buffer, i = 0) { * * @param {*} session */ -function Stream(session) { - EventEmitter.apply(this); - this.session = session; - this.token = null; - this.ssrc = null; - this.rinfo1 = null; - this.rinfo2 = null; - this.name = ''; - this.lastSentSequenceNr = Math.round(Math.random() * 0xffff); - this.firstReceivedSequenceNumber = -1; - this.lastReceivedSequenceNumber = -1; - this.lostSequenceNumbers = []; - this.latency = null; - this.subscribers = []; - this.isConnected = false; - this.receiverFeedbackTimeout = null; - this.lastMessageTime = 0; - this.timeDifference = null; - this.isInitiator = false; -} +class Stream extends EventEmitter { + constructor(session) { + super() + this.session = session; + this.token = null; + this.ssrc = null; + this.rinfo1 = null; + this.rinfo2 = null; + this.name = ''; + this.lastSentSequenceNr = Math.round(Math.random() * 0xffff); + this.firstReceivedSequenceNumber = -1; + this.lastReceivedSequenceNumber = -1; + this.lostSequenceNumbers = []; + this.latency = null; + this.subscribers = []; + this.isConnected = false; + this.receiverFeedbackTimeout = null; + this.lastMessageTime = 0; + this.timeDifference = null; + this.isInitiator = false; + } -util.inherits(Stream, EventEmitter); + connect(rinfo) { + this.isInitiator = true; + let counter = 0; + this.connectionInterval = setInterval(() => { + if (counter < 40 && this.ssrc === null) { + this.sendInvitation(rinfo); + counter += 1; + } else { + clearInterval(this.connectionInterval); + if (!this.ssrc) { + const { address, port } = rinfo; + logger.warn(`Server at ${address}:${port} did not respond.`); + } + } + }, 1500); + } -Stream.prototype.connect = function connect(rinfo) { - this.isInitiator = true; - let counter = 0; - this.connectionInterval = setInterval(() => { - if (counter < 40 && this.ssrc === null) { - this.sendInvitation(rinfo); - counter += 1; - } else { - clearInterval(this.connectionInterval); - if (!this.ssrc) { - const { address, port } = rinfo; - logger.warn(`Server at ${address}:${port} did not respond.`); + handleControlMessage(message, rinfo) { + const commandName = message.command; + let handlerName = 'handle'; + handlerName += commandName.slice(0, 1).toUpperCase(); + handlerName += commandName.slice(1); + if (this[handlerName]) { + this[handlerName](message, rinfo); } + this.emit('control-message', message); } - }, 1500); -}; - -Stream.prototype.handleControlMessage = function handleControlMessage(message, rinfo) { - const commandName = message.command; - let handlerName = 'handle'; - handlerName += commandName.slice(0, 1).toUpperCase(); - handlerName += commandName.slice(1); - if (this[handlerName]) { - this[handlerName](message, rinfo); - } - this.emit('control-message', message); -}; -Stream.prototype.handleMidiMessage = function handleMidiMessage(message) { - if (this.firstReceivedSequenceNumber !== -1) { - for (let i = this.lastReceivedSequenceNumber + 1; i < message.sequenceNumber; i += 1) { - this.lostSequenceNumbers.push(i); + handleMidiMessage(message) { + if (this.firstReceivedSequenceNumber !== -1) { + for (let i = this.lastReceivedSequenceNumber + 1; i < message.sequenceNumber; i += 1) { + this.lostSequenceNumbers.push(i); + } + } else { + this.firstReceivedSequenceNumber = message.sequenceNumber; + } + + this.lastReceivedSequenceNumber = message.sequenceNumber; + + let messageTime = this.timeDifference - this.latency + message.timestamp; + + message.commands.forEach((command) => { + messageTime += command.deltaTime; + this.emit('message', messageTime, command.data); + }); + + clearTimeout(this.receiverFeedbackTimeout); + this.receiverFeedbackTimeout = setTimeout(this.sendReceiverFeedback.bind(this), 1000); } - } else { - this.firstReceivedSequenceNumber = message.sequenceNumber; - } - this.lastReceivedSequenceNumber = message.sequenceNumber; - - let messageTime = this.timeDifference - this.latency + message.timestamp; - - message.commands.forEach((command) => { - messageTime += command.deltaTime; - this.emit('message', messageTime, command.data); - }); - - clearTimeout(this.receiverFeedbackTimeout); - this.receiverFeedbackTimeout = setTimeout(this.sendReceiverFeedback.bind(this), 1000); -}; - -// eslint-disable-next-line camelcase -Stream.prototype.handleInvitation_accepted = function handleInvitation_accepted(message, rinfo) { - if (this.rinfo1 === null) { - logger.info(`Invitation accepted by ${message.name}`); - this.name = message.name; - this.ssrc = message.ssrc; - this.rinfo1 = rinfo; - this.sendInvitation({ - address: rinfo.address, - port: rinfo.port + 1, - }); - this.isConnected = true; - this.emit('connected', { - stream: this, - }); - } else if (this.rinfo2 === null) { - logger.info(`Data channel to ${this.name} established`); - this.emit('established', { - stream: this, - }); - this.rinfo2 = rinfo; - let count = 0; - this.syncInterval = setInterval(() => { - this.sendSynchronization(); - count += 1; - if (count > 10 || this.timeDifference) { - clearInterval(this.syncInterval); + // eslint-disable-next-line camelcase + handleInvitation_accepted(message, rinfo) { + if (this.rinfo1 === null) { + logger.info(`Invitation accepted by ${message.name}`); + this.name = message.name; + this.ssrc = message.ssrc; + this.rinfo1 = rinfo; + this.sendInvitation({ + address: rinfo.address, + port: rinfo.port + 1, + }); + this.isConnected = true; + this.emit('connected', { + stream: this, + }); + } else if (this.rinfo2 === null) { + logger.info(`Data channel to ${this.name} established`); + this.emit('established', { + stream: this, + }); + this.rinfo2 = rinfo; + let count = 0; this.syncInterval = setInterval(() => { this.sendSynchronization(); - }, 10000); + count += 1; + if (count > 10 || this.timeDifference) { + clearInterval(this.syncInterval); + this.syncInterval = setInterval(() => { + this.sendSynchronization(); + }, 10000); + } + }, 1500); } - }, 1500); - } -}; - -// eslint-disable-next-line camelcase -Stream.prototype.handleInvitation_rejected = function handleInvitation_accepted(message, rinfo) { - clearInterval(this.connectionInterval); - logger.info(`Invititation was rejected by ${rinfo.address}:${rinfo.port} ${message}`); - this.session.removeStream(this); -}; - -Stream.prototype.handleInvitation = function handleInvitation(message, rinfo) { - if (this.rinfo1 === null) { - this.rinfo1 = rinfo; - this.token = message.token; - this.name = message.name; - this.ssrc = message.ssrc; - logger.info(`Got invitation from ${message.name} on channel 1`); - } else if (this.rinfo2 == null) { - this.rinfo2 = rinfo; - logger.info(`Got invitation from ${message.name} on channel 2`); - this.isConnected = true; - this.emit('connected', { - stream: this, - }); - } - this.sendInvitationAccepted(rinfo); -}; - -Stream.prototype.handleSynchronization = function handleSynchronization(message) { - this.sendSynchronization(message); -}; - -Stream.prototype.handleEnd = function handleEndstream() { - logger.info(`${this.name} ended the stream`); - clearInterval(this.syncInterval); - this.isConnected = false; - this.emit('disconnected', { - stream: this, - }); -}; - -// eslint-disable-next-line camelcase -Stream.prototype.handleReceiver_feedback = function handleReceiver_feedback(message) { - logger.info(`Got receiver feedback SRRC ${message.ssrc} is at ${message.sequenceNumber}. Current is ${this.lastSentSequenceNr}`); -}; - -Stream.prototype.sendInvitation = function sendInvitation(rinfo) { - if (!this.token) { - this.token = generateRandomInteger(4); - } - this.session.sendUdpMessage(rinfo, new ControlMessage().mixin({ - command: 'invitation', - token: this.token, - ssrc: this.session.ssrc, - name: this.session.bonjourName, - })); -}; - -Stream.prototype.sendInvitationAccepted = function sendInvitationAccepted(rinfo) { - this.session.sendUdpMessage(rinfo, new ControlMessage().mixin({ - command: 'invitation_accepted', - token: this.token, - ssrc: this.session.ssrc, - name: this.session.bonjourName, - })); -}; - -Stream.prototype.sendEndstream = function sendEndstream(callback) { - this.session.sendUdpMessage(this.rinfo1, new ControlMessage().mixin({ - command: 'end', - token: this.token, - ssrc: this.session.ssrc, - name: this.name, - }), callback); -}; - -Stream.prototype.sendSynchronization = function sendSynchronization(incomingSyncMessage) { - const now = this.session.now(); - const count = incomingSyncMessage ? incomingSyncMessage.count : -1; - const answer = new ControlMessage(); - - answer.command = 'synchronization'; - answer.timestamp1 = count !== -1 ? incomingSyncMessage.timestamp1 : Buffer.alloc(8); - answer.timestamp2 = count !== -1 ? incomingSyncMessage.timestamp2 : Buffer.alloc(8); - answer.timestamp3 = count !== -1 ? incomingSyncMessage.timestamp3 : Buffer.alloc(8); - answer.count = count + 1; - answer.ssrc = this.session.ssrc; - answer.token = this.token; - - switch (count) { - case -1: - writeUInt64BE(answer.timestamp1, now); - if (this.timeDifference) { - writeUInt64BE(answer.timestamp2, now - this.timeDifference); - } else { - writeUInt64BE(answer.timestamp2, 0); + } + + // eslint-disable-next-line camelcase + handleInvitation_rejected(message, rinfo) { + clearInterval(this.connectionInterval); + logger.info(`Invititation was rejected by ${rinfo.address}:${rinfo.port} ${message}`); + this.session.removeStream(this); + } + + handleInvitation(message, rinfo) { + if (this.rinfo1 === null) { + this.rinfo1 = rinfo; + this.token = message.token; + this.name = message.name; + this.ssrc = message.ssrc; + logger.info(`Got invitation from ${message.name} on channel 1`); + } else if (this.rinfo2 == null) { + this.rinfo2 = rinfo; + logger.info(`Got invitation from ${message.name} on channel 2`); + this.isConnected = true; + this.emit('connected', { + stream: this, + }); + } + this.sendInvitationAccepted(rinfo); + } + + handleSynchronization(message) { + this.sendSynchronization(message); + } + + handleEnd() { + logger.info(`${this.name} ended the stream`); + clearInterval(this.syncInterval); + this.isConnected = false; + this.emit('disconnected', { + stream: this, + }); + } + + // eslint-disable-next-line camelcase + handleReceiver_feedback(message) { + logger.info(`Got receiver feedback SRRC ${message.ssrc} is at ${message.sequenceNumber}. Current is ${this.lastSentSequenceNr}`); + } + + sendInvitation(rinfo) { + if (!this.token) { + this.token = generateRandomInteger(4); + } + this.session.sendUdpMessage(rinfo, new ControlMessage().mixin({ + command: 'invitation', + token: this.token, + ssrc: this.session.ssrc, + name: this.session.bonjourName, + })); + } + + sendInvitationAccepted(rinfo) { + this.session.sendUdpMessage(rinfo, new ControlMessage().mixin({ + command: 'invitation_accepted', + token: this.token, + ssrc: this.session.ssrc, + name: this.session.bonjourName, + })); + } + + sendEndstream(callback) { + this.session.sendUdpMessage(this.rinfo1, new ControlMessage().mixin({ + command: 'end', + token: this.token, + ssrc: this.session.ssrc, + name: this.name, + }), callback); + } + + sendSynchronization(incomingSyncMessage) { + const now = this.session.now(); + const count = incomingSyncMessage ? incomingSyncMessage.count : -1; + const answer = new ControlMessage(); + + answer.command = 'synchronization'; + answer.timestamp1 = count !== -1 ? incomingSyncMessage.timestamp1 : Buffer.alloc(8); + answer.timestamp2 = count !== -1 ? incomingSyncMessage.timestamp2 : Buffer.alloc(8); + answer.timestamp3 = count !== -1 ? incomingSyncMessage.timestamp3 : Buffer.alloc(8); + answer.count = count + 1; + answer.ssrc = this.session.ssrc; + answer.token = this.token; + + switch (count) { + case -1: + writeUInt64BE(answer.timestamp1, now); + if (this.timeDifference) { + writeUInt64BE(answer.timestamp2, now - this.timeDifference); + } else { + writeUInt64BE(answer.timestamp2, 0); + } + if (this.latency) { + writeUInt64BE(answer.timestamp3, now + this.latency); + } else { + writeUInt64BE(answer.timestamp3, 0); + } + break; + case 0: + writeUInt64BE(answer.timestamp2, now); + writeUInt64BE(answer.timestamp3, now - this.timeDifference); + break; + case 1: + writeUInt64BE(answer.timestamp3, now); + this.latency = readUInt64BE(incomingSyncMessage.timestamp3) + - readUInt64BE(incomingSyncMessage.timestamp1); + this.timeDifference = Math.round(readUInt64BE(incomingSyncMessage.timestamp3) + - readUInt64BE(incomingSyncMessage.timestamp2)) - this.latency; + break; + case 2: + break; + default: + break; } - if (this.latency) { - writeUInt64BE(answer.timestamp3, now + this.latency); + + // Debug stuff + this.logSynchronization(incomingSyncMessage, answer); + + if (answer.count < 3) { + this.session.sendUdpMessage(this.rinfo2, answer); } else { - writeUInt64BE(answer.timestamp3, 0); + this.sendSynchronization(); } - break; - case 0: - writeUInt64BE(answer.timestamp2, now); - writeUInt64BE(answer.timestamp3, now - this.timeDifference); - break; - case 1: - writeUInt64BE(answer.timestamp3, now); - this.latency = readUInt64BE(incomingSyncMessage.timestamp3) - - readUInt64BE(incomingSyncMessage.timestamp1); - this.timeDifference = Math.round(readUInt64BE(incomingSyncMessage.timestamp3) - - readUInt64BE(incomingSyncMessage.timestamp2)) - this.latency; - break; - case 2: - break; - default: - break; - } + } - // Debug stuff - this.logSynchronization(incomingSyncMessage, answer); + logSynchronization(incomingSyncMessage, answer) { + const count = incomingSyncMessage ? incomingSyncMessage.count : -1; - if (answer.count < 3) { - this.session.sendUdpMessage(this.rinfo2, answer); - } else { - this.sendSynchronization(); - } -}; + if (count === 0 || count === -1) { + logger.debug( + '\n', 'T', 'C', 'Timestamp 1 ', 'Timestamp 2 ', + 'Timestamp 3 ', 'Latency ', ' Time difference ', 'Rate ', + ); + } + if (incomingSyncMessage) { + logger.debug( + 'I', incomingSyncMessage.count, + pad(readUInt64BE(incomingSyncMessage.timestamp1), 20), + pad(readUInt64BE(incomingSyncMessage.timestamp2), 20), + pad(readUInt64BE(incomingSyncMessage.timestamp3), 20), + pad(this.latency, 10), + (this.timeDifference < 0 ? '-' : ' ') + pad(Math.abs(this.timeDifference), 20), + this.session.rate, + ); + } + if (answer.count < 3) { + logger.debug( + 'O', answer.count, + pad(readUInt64BE(answer.timestamp1), 20), + pad(readUInt64BE(answer.timestamp2), 20), + pad(readUInt64BE(answer.timestamp3), 20), + pad(this.latency, 10), + (this.timeDifference < 0 ? '-' : ' ') + pad(Math.abs(this.timeDifference), 20), + this.session.rate, + ); + } + if (this.timeDifference) { + const d = new Date(); + d.setTime(this.timeDifference / 10); + } + } -Stream.prototype.logSynchronization = function logSynchronization(incomingSyncMessage, answer) { - const count = incomingSyncMessage ? incomingSyncMessage.count : -1; + sendReceiverFeedback(callback) { + if (this.lostSequenceNumbers.length) { + logger.warn(`Lost packages: ${this.lostSequenceNumbers}`); + } + this.session.sendUdpMessage(this.rinfo1, new ControlMessage().mixin({ + command: 'receiver_feedback', + ssrc: this.session.ssrc, + sequenceNumber: this.lastReceivedSequenceNumber, + }), callback); + } - if (count === 0 || count === -1) { - logger.debug( - '\n', 'T', 'C', 'Timestamp 1 ', 'Timestamp 2 ', - 'Timestamp 3 ', 'Latency ', ' Time difference ', 'Rate ', - ); - } - if (incomingSyncMessage) { - logger.debug( - 'I', incomingSyncMessage.count, - pad(readUInt64BE(incomingSyncMessage.timestamp1), 20), - pad(readUInt64BE(incomingSyncMessage.timestamp2), 20), - pad(readUInt64BE(incomingSyncMessage.timestamp3), 20), - pad(this.latency, 10), - (this.timeDifference < 0 ? '-' : ' ') + pad(Math.abs(this.timeDifference), 20), - this.session.rate, - ); - } - if (answer.count < 3) { - logger.debug( - 'O', answer.count, - pad(readUInt64BE(answer.timestamp1), 20), - pad(readUInt64BE(answer.timestamp2), 20), - pad(readUInt64BE(answer.timestamp3), 20), - pad(this.latency, 10), - (this.timeDifference < 0 ? '-' : ' ') + pad(Math.abs(this.timeDifference), 20), - this.session.rate, - ); - } - if (this.timeDifference) { - const d = new Date(); - d.setTime(this.timeDifference / 10); - } -}; + sendMessage(message, callback) { + if (this.latency === null || this.timeDifference === null) { + return; + } -Stream.prototype.sendReceiverFeedback = function sendReceiverFeedback(callback) { - if (this.lostSequenceNumbers.length) { - logger.warn(`Lost packages: ${this.lostSequenceNumbers}`); - } - this.session.sendUdpMessage(this.rinfo1, new ControlMessage().mixin({ - command: 'receiver_feedback', - ssrc: this.session.ssrc, - sequenceNumber: this.lastReceivedSequenceNumber, - }), callback); -}; - -Stream.prototype.sendMessage = function sendMessage(message, callback) { - if (this.latency === null || this.timeDifference === null) { - return; - } + this.lastSentSequenceNr = (this.lastSentSequenceNr + 1) % 0x10000; - this.lastSentSequenceNr = (this.lastSentSequenceNr + 1) % 0x10000; + // eslint-disable-next-line no-param-reassign + message = new MidiMessage().mixin(message); + // eslint-disable-next-line no-param-reassign + message.ssrc = this.session.ssrc; - // eslint-disable-next-line no-param-reassign - message = new MidiMessage().mixin(message); - // eslint-disable-next-line no-param-reassign - message.ssrc = this.session.ssrc; + // eslint-disable-next-line no-param-reassign + message.sequenceNumber = this.lastSentSequenceNr; - // eslint-disable-next-line no-param-reassign - message.sequenceNumber = this.lastSentSequenceNr; + this.session.sendUdpMessage(this.rinfo2, message, callback); + } - this.session.sendUdpMessage(this.rinfo2, message, callback); -}; + end(callback) { + clearInterval(this.syncInterval); + clearInterval(this.connectionInterval); + if (this.isConnected) { + this.sendEndstream(() => { + this.emit('disconnected', { + stream: this, + }); + this.isConnected = false; + callback && callback(); + }); + } else { + callback && callback(); + } + } -Stream.prototype.end = function end(callback) { - clearInterval(this.syncInterval); - clearInterval(this.connectionInterval); - if (this.isConnected) { - this.sendEndstream(() => { - this.emit('disconnected', { - stream: this, - }); - this.isConnected = false; - callback && callback(); - }); - } else { - callback && callback(); - } -}; - -Stream.prototype.toJSON = function toJSON() { - return { - address: this.rinfo1.address, - ssrc: this.ssrc, - port: this.rinfo1.port, - name: this.name, - }; -}; + toJSON() { + return { + address: this.rinfo1.address, + ssrc: this.ssrc, + port: this.rinfo1.port, + name: this.name, + }; + } +} module.exports = Stream; diff --git a/src/manager.js b/src/manager.js index d18d7e3..1e36d40 100644 --- a/src/manager.js +++ b/src/manager.js @@ -116,7 +116,7 @@ function changeSession(config) { session.publish(); } this.emit('sessionChanged', { session }); - }; + } if (config.published === false || republish || config.activated === false) { diff --git a/src/mdns/service-mdns.js b/src/mdns/service-mdns.js index 734c707..4c90cac 100644 --- a/src/mdns/service-mdns.js +++ b/src/mdns/service-mdns.js @@ -3,7 +3,6 @@ const logger = require('../logger'); var mdns = null, -util = require('util'), EventEmitter = require('events').EventEmitter, service_id = '_apple-midi', publishedSessions = [], @@ -44,82 +43,83 @@ function sessionDetails(session) { } var details = {}; -function MDnsService() { - browser = bonjourService.find({ type: 'apple-midi', protocol: 'udp' }); - browser.on('up', function (service) { - remoteSessions[service.name] = service; - details[service.name] = sessionDetails(service); - updateRemoteSessions(); - this.emit('remoteSessionUp', details[service.name]); - }.bind(this)); - browser.on('down', function (service) { - var d = details[service.name]; - delete(remoteSessions[service.name]); - delete(details[service.name]); - updateRemoteSessions(); - this.emit('remoteSessionDown', d); - }.bind(this)); -} - -util.inherits(MDnsService, EventEmitter); +var sessions = []; -MDnsService.prototype.start = function () { - remoteSessions = {}; - if (browser) { - browser.start(); - } else { - logger.log('mDNS discovery is not available.') +function updateRemoteSessions() { + sessions.length = 0; + for (var name in details) { + if (details.hasOwnProperty(name)) { + sessions.push(details[name]); + } } }; -MDnsService.prototype.stop = function() { - if (browser) { - browser.stop(); - } -}; +class MDnsService extends EventEmitter { + constructor() { + super() + browser = bonjourService.find({ type: 'apple-midi', protocol: 'udp' }); + browser.on('up', function (service) { + remoteSessions[service.name] = service; + details[service.name] = sessionDetails(service); + updateRemoteSessions(); + this.emit('remoteSessionUp', details[service.name]); + }.bind(this)); + browser.on('down', function (service) { + var d = details[service.name]; + delete(remoteSessions[service.name]); + delete(details[service.name]); + updateRemoteSessions(); + this.emit('remoteSessionDown', d); + }.bind(this)); + } -MDnsService.prototype.publish = function(session) { - if (publishedSessions.indexOf(session) !== -1) { - return; - } - publishedSessions.push(session); - const ad = bonjourService.publish({ name: session.bonjourName, type: 'apple-midi', port: session.port, protocol: 'udp' }) - logger.debug('Added mDNS service', ad) - advertisments.push(ad); - ad.start(); -}; + start() { + remoteSessions = {}; + if (browser) { + browser.start(); + } else { + logger.log('mDNS discovery is not available.') + } + } -MDnsService.prototype.unpublishAll = function (cb = () => {}) { - bonjourService.unpublishAll(cb) -} + stop() { + if (browser) { + browser.stop(); + } + } -MDnsService.prototype.unpublish = function(session) { - var index = publishedSessions.indexOf(session); - if (index === -1) { - return; - } - var ad = advertisments[index]; + publish(session) { + if (publishedSessions.indexOf(session) !== -1) { + return; + } + publishedSessions.push(session); + const ad = bonjourService.publish({ name: session.bonjourName, type: 'apple-midi', port: session.port, protocol: 'udp' }) + logger.debug('Added mDNS service', ad) + advertisments.push(ad); + ad.start(); + } - ad.stop(() => { - publishedSessions.splice(index); - advertisments.splice(index); - }); -}; + unpublishAll(cb = () => {}) { + bonjourService.unpublishAll(cb) + } -var sessions = []; + unpublish(session) { + var index = publishedSessions.indexOf(session); + if (index === -1) { + return; + } + var ad = advertisments[index]; -function updateRemoteSessions() { - sessions.length = 0; - for (var name in details) { - if (details.hasOwnProperty(name)) { - sessions.push(details[name]); + ad.stop(() => { + publishedSessions.splice(index); + advertisments.splice(index); + }); } - } -}; -MDnsService.prototype.getRemoteSessions = function() { - return sessions; -}; + getRemoteSessions() { + return sessions; + } +} process.on('SIGINT', () => { bonjourService.unpublishAll(() => {