New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@meyer/hyperdeck-emulator

Package Overview
Dependencies
Maintainers
2
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@meyer/hyperdeck-emulator - npm Package Compare versions

Comparing version 0.0.4-canary.35.72ad3f9 to 0.0.4-canary.37.241f21f

dist/api.d.ts

31

dist/constants.d.ts
export declare const CRLF = "\r\n";
export declare const parametersByCommandName: {
help: never[];
commands: never[];
'device info': never[];
'disk list': string[];
quit: never[];
ping: never[];
preview: string[];
play: string[];
'playrange set': string[];
'playrange clear': never[];
record: string[];
stop: never[];
'clips count': never[];
'clips get': string[];
'clips add': string[];
'clips clear': never[];
'transport info': never[];
'slot info': string[];
'slot select': string[];
notify: string[];
goto: string[];
jog: string[];
shuttle: string[];
remote: string[];
configuration: string[];
uptime: never[];
format: string[];
identify: string[];
watchdog: string[];
};
//# sourceMappingURL=constants.d.ts.map

260

dist/hyperdeck-emulator.cjs.development.js

@@ -245,33 +245,2 @@ 'use strict';

var CRLF = '\r\n';
var parametersByCommandName = {
help: [],
commands: [],
'device info': [],
'disk list': ['slot id'],
quit: [],
ping: [],
preview: ['enable'],
play: ['speed', 'loop', 'single clip'],
'playrange set': ['clip id', 'in', 'out'],
'playrange clear': [],
record: ['name'],
stop: [],
'clips count': [],
'clips get': ['clip id', 'count'],
'clips add': ['name'],
'clips clear': [],
'transport info': [],
'slot info': ['slot id'],
'slot select': ['slot id', 'video format'],
notify: ['remote', 'transport', 'slot', 'configuration', 'dropped frames'],
"goto": ['clip id', 'clip', 'timeline', 'timecode', 'slot id'],
jog: ['timecode'],
shuttle: ['speed'],
remote: ['enable', 'override'],
configuration: ['video input', 'audio input', 'file format'],
uptime: [],
format: ['prepare', 'confirm'],
identify: ['enable'],
watchdog: ['period']
};

@@ -288,2 +257,226 @@ function invariant(condition, message) {

/** Internal container class that holds metadata about each HyperDeck event */
var HyperDeckAPI = function HyperDeckAPI(options) {
var _this = this;
if (options === void 0) {
options = {};
}
this.options = options;
this.addOption = function (key, option) {
var _Object$assign;
var k = Array.isArray(key) ? key[0] : key;
!!_this.options.hasOwnProperty(k) ? invariant(false, 'option already exists for key `%s`', k) : void 0; // NOTE: this mutates the original options object
// shouldn't be a problem since this is only used internally
Object.assign(_this.options, (_Object$assign = {}, _Object$assign[k] = option, _Object$assign));
return _this;
};
/** Get a Set of param names keyed by function name */
this.getParamsByKey = function () {
return Object.entries(_this.options).reduce(function (prev, _ref) {
var key = _ref[0],
value = _ref[1];
prev[key] = new Set(value.arguments ? Object.keys(value.arguments).map(function (key) {
return key.replace(/([a-z])([A-Z]+)/g, '$1 $2').toLowerCase();
}) : []);
return prev;
}, {});
};
};
var api = /*#__PURE__*/new HyperDeckAPI().addOption(['help', '?'], {
description: 'Provides help text on all commands and parameters'
}).addOption('commands', {
description: 'return commands in XML format'
}).addOption('device info', {
description: 'return device information'
}).addOption('disk list', {
description: 'query clip list on active disk',
arguments: {
slotId: 'number'
}
}).addOption('quit', {
description: 'disconnect ethernet control'
}).addOption('ping', {
description: 'check device is responding'
}).addOption('preview', {
description: 'switch to preview or output',
arguments: {
enable: 'boolean'
}
}).addOption('play', {
description: 'play from current timecode',
arguments: {
speed: 'number',
loop: 'boolean',
singleClip: 'boolean'
}
}).addOption('playrange', {
description: 'query playrange setting'
}).addOption('playrange set', {
description: 'set play range to play clip {n} only',
arguments: {
// maybe number?
clipId: 'string',
// description: 'set play range to play between timecode {inT} and timecode {outT}',
"in": 'timecode',
out: 'timecode',
// 'set play range in units of frames between timeline position {in} and position {out} clear/reset play range°setting',
timelineIn: 'number',
timelineOut: 'number'
}
}).addOption('playrange clear', {
description: 'clear/reset play range setting'
}).addOption('play on startup', {
description: 'query unit play on startup state',
// description: 'enable or disable play on startup',
arguments: {
enable: 'boolean',
singleClip: 'boolean'
}
}).addOption('play option', {
description: 'query play options',
arguments: {
stopMode: 'stopmode'
}
}).addOption('record', {
description: 'record from current input',
arguments: {
name: 'string'
}
}).addOption('record spill', {
description: 'spill current recording to next slot',
arguments: {
slotId: 'number'
}
}).addOption('stop', {
description: 'stop playback or recording'
}).addOption('clips count', {
description: 'query number of clips on timeline'
}).addOption('clips get', {
description: 'query all timeline clips',
arguments: {
clipId: 'number',
count: 'number',
version: 'number'
}
}).addOption('clips add', {
description: 'append a clip to timeline',
arguments: {
name: 'string',
clipId: 'string',
"in": 'timecode',
out: 'timecode'
}
}).addOption('clips remove', {
description: 'remove clip {n} from the timeline (invalidates clip ids following clip {n})',
arguments: {
clidId: 'string'
}
}).addOption('clips clear', {
description: 'empty timeline clip list'
}).addOption('transport info', {
description: 'query current activity'
}).addOption('slot info', {
description: 'query active slot',
arguments: {
slotId: 'number'
}
}).addOption('slot select', {
description: 'switch to specified slot',
arguments: {
slotId: 'number',
videoFormat: 'videoformat'
}
}).addOption('slot unblock', {
description: 'unblock active slot',
arguments: {
slotId: 'number'
}
}).addOption('dynamic range', {
description: 'query dynamic range settings',
arguments: {
// TODO(meyer) is this correct?
playbackOverride: 'string'
}
}).addOption('notify', {
description: 'query notification status',
arguments: {
remote: 'boolean',
transport: 'boolean',
slot: 'boolean',
configuration: 'boolean',
droppedFrames: 'boolean',
displayTimecode: 'boolean',
timelinePosition: 'boolean',
playrange: 'boolean',
dynamicRange: 'boolean'
}
}).addOption('goto', {
description: 'go forward or backward within a clip or timeline',
arguments: {
clipId: 'string',
clip: 'goto',
timeline: 'goto',
timecode: 'timecode',
slotId: 'number'
}
}).addOption('jog', {
description: 'jog forward or backward',
arguments: {
timecode: 'timecode'
}
}).addOption('shuttle', {
description: 'shuttle with speed',
arguments: {
speed: 'number'
}
}).addOption('remote', {
description: 'query unit remote control state',
arguments: {
enable: 'boolean',
override: 'boolean'
}
}).addOption('configuration', {
description: 'query configuration settings',
arguments: {
videoInput: 'videoinput',
audioInput: 'audioinput',
fileFormat: 'fileformat',
audioCodec: 'audiocodec',
timecodeInput: 'timecodeinput',
timecodePreset: 'timecode',
audioInputChannels: 'number',
recordTrigger: 'recordtrigger',
recordPrefix: 'string',
appendTimestamp: 'boolean'
}
}).addOption('uptime', {
description: 'return time since last boot'
}).addOption('format', {
description: 'prepare a disk formatting operation to filesystem {format}',
arguments: {
prepare: 'string',
confirm: 'string'
}
}).addOption('identify', {
description: 'identify the device',
arguments: {
enable: 'boolean'
}
}).addOption('watchdog', {
description: 'client connection timeout',
arguments: {
period: 'number'
}
});
var paramsByKey = /*#__PURE__*/api.getParamsByKey();
var MultilineParser = /*#__PURE__*/function () {

@@ -352,4 +545,5 @@ function MultilineParser(logger) {

!msg ? invariant(false, 'Unrecognised command') : void 0;
!paramsByKey.hasOwnProperty(msg) ? invariant(false, 'Invalid command: `%s`', msg) : void 0;
var params = {};
var paramNames = new Set(parametersByCommandName[msg]);
var paramNames = paramsByKey[msg];
var param = bits.shift();

@@ -464,3 +658,3 @@ !param ? invariant(false, 'No named parameters found') : void 0;

var formattedKey = key.replace(/([a-z])([A-Z]+)/, '$1 $2').toLowerCase();
var formattedKey = key.replace(/([a-z])([A-Z]+)/g, '$1 $2').toLowerCase();
return prev + formattedKey + ': ' + valueString + CRLF;

@@ -467,0 +661,0 @@ }, firstLine + ':' + CRLF) + CRLF;

@@ -1,2 +0,2 @@

"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var n,t,r,o,i=require("events"),s=e(require("util")),a=require("net"),u=e(require("pino"));function c(e,n){e.prototype=Object.create(n.prototype),e.prototype.constructor=e,e.__proto__=n}function f(e){return(f=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function l(e,n){return(l=Object.setPrototypeOf||function(e,n){return e.__proto__=n,e})(e,n)}function p(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}function m(e,n,t){return(m=p()?Reflect.construct:function(e,n,t){var r=[null];r.push.apply(r,n);var o=new(Function.bind.apply(e,r));return t&&l(o,t.prototype),o}).apply(null,arguments)}function d(e){var n="function"==typeof Map?new Map:void 0;return(d=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==n){if(n.has(e))return n.get(e);n.set(e,t)}function t(){return m(e,arguments,f(this).constructor)}return t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),l(t,e)})(e)}function h(e,n){(null==n||n>e.length)&&(n=e.length);for(var t=0,r=new Array(n);t<n;t++)r[t]=e[t];return r}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator"))),function(e){e[e.SyntaxError=100]="SyntaxError",e[e.UnsupportedParameter=101]="UnsupportedParameter",e[e.InvalidValue=102]="InvalidValue",e[e.Unsupported=103]="Unsupported",e[e.DiskFull=104]="DiskFull",e[e.NoDisk=105]="NoDisk",e[e.DiskError=106]="DiskError",e[e.TimelineEmpty=107]="TimelineEmpty",e[e.InternalError=108]="InternalError",e[e.OutOfRange=109]="OutOfRange",e[e.NoInput=110]="NoInput",e[e.RemoteControlDisabled=111]="RemoteControlDisabled",e[e.ConnectionRejected=120]="ConnectionRejected",e[e.InvalidState=150]="InvalidState",e[e.InvalidCodec=151]="InvalidCodec",e[e.InvalidFormat=160]="InvalidFormat",e[e.InvalidToken=161]="InvalidToken",e[e.FormatNotPrepared=162]="FormatNotPrepared"}(t||(t={})),function(e){e[e.OK=200]="OK",e[e.SlotInfo=202]="SlotInfo",e[e.DeviceInfo=204]="DeviceInfo",e[e.ClipsInfo=205]="ClipsInfo",e[e.DiskList=206]="DiskList",e[e.TransportInfo=208]="TransportInfo",e[e.Notify=209]="Notify",e[e.Remote=210]="Remote",e[e.Configuration=211]="Configuration",e[e.ClipsCount=214]="ClipsCount",e[e.Uptime=215]="Uptime",e[e.FormatReady=216]="FormatReady"}(r||(r={})),function(e){e[e.ConnectionInfo=500]="ConnectionInfo",e[e.SlotInfo=502]="SlotInfo",e[e.TransportInfo=508]="TransportInfo",e[e.RemoteInfo=510]="RemoteInfo",e[e.ConfigurationInfo=511]="ConfigurationInfo"}(o||(o={}));var v,g,y,I=((n={})[o.ConfigurationInfo]="configuration info",n[o.ConnectionInfo]="connection info",n[o.RemoteInfo]="remote info",n[o.SlotInfo]="slot info",n[o.TransportInfo]="transport info",n[t.ConnectionRejected]="connection rejected",n[t.DiskError]="disk error",n[t.DiskFull]="disk full",n[t.FormatNotPrepared]="format not prepared",n[t.InternalError]="internal error",n[t.InvalidCodec]="invalid codec",n[t.InvalidFormat]="invalid format",n[t.InvalidState]="invalid state",n[t.InvalidToken]="invalid token",n[t.InvalidValue]="invalid value",n[t.NoDisk]="no disk",n[t.NoInput]="no input",n[t.OutOfRange]="out of range",n[t.RemoteControlDisabled]="remote control disabled",n[t.SyntaxError]="syntax error",n[t.TimelineEmpty]="timeline empty",n[t.Unsupported]="unsupported",n[t.UnsupportedParameter]="unsupported parameter",n[r.ClipsCount]="clips count",n[r.ClipsInfo]="clips info",n[r.Configuration]="configuration",n[r.DeviceInfo]="device info",n[r.DiskList]="disk list",n[r.FormatReady]="format ready",n[r.Notify]="notify",n[r.OK]="ok",n[r.Remote]="remote",n[r.SlotInfo]="slot info",n[r.TransportInfo]="transport info",n[r.Uptime]="uptime",n);!function(e){e.QuickTimeUncompressed="QuickTimeUncompressed",e.QuickTimeProResHQ="QuickTimeProResHQ",e.QuickTimeProRes="QuickTimeProRes",e.QuickTimeProResLT="QuickTimeProResLT",e.QuickTimeProResProxy="QuickTimeProResProxy",e.QuickTimeDNxHR220="QuickTimeDNxHR220",e.DNxHR220="DNxHR220"}(v||(v={})),function(e){e.embedded="embedded",e.XLR="XLR",e.RCA="RCA"}(g||(g={})),function(e){e.SDI="SDI",e.HDMI="HDMI",e.component="component"}(y||(y={}));var S={help:[],commands:[],"device info":[],"disk list":["slot id"],quit:[],ping:[],preview:["enable"],play:["speed","loop","single clip"],"playrange set":["clip id","in","out"],"playrange clear":[],record:["name"],stop:[],"clips count":[],"clips get":["clip id","count"],"clips add":["name"],"clips clear":[],"transport info":[],"slot info":["slot id"],"slot select":["slot id","video format"],notify:["remote","transport","slot","configuration","dropped frames"],goto:["clip id","clip","timeline","timecode","slot id"],jog:["timecode"],shuttle:["speed"],remote:["enable","override"],configuration:["video input","audio input","file format"],uptime:[],format:["prepare","confirm"],identify:["enable"],watchdog:["period"]};function R(e,n){if(!e){for(var t=arguments.length,r=new Array(t>2?t-2:0),o=2;o<t;o++)r[o-2]=arguments[o];throw new Error(s.format.apply(s,[n].concat(r)))}}var b=function(){function e(e){this.linesQueue=[],this.logger=e.child({name:"MultilineParser"})}var n=e.prototype;return n.receivedString=function(e){var n=[],t=e.split("\r\n");for(t.length>0&&""===t[t.length-1]&&t.pop(),this.linesQueue=this.linesQueue.concat(t);this.linesQueue.length>0;)if(""!==this.linesQueue[0])if(!this.linesQueue[0].includes(":")||1===this.linesQueue.length&&this.linesQueue[0].includes(":")){var r=this.parseResponse(this.linesQueue.splice(0,1));r&&n.push(r)}else{var o=this.linesQueue.indexOf("");if(-1===o)break;var i=this.linesQueue.splice(0,o+1),s=this.parseResponse(i);s&&n.push(s)}else this.linesQueue.shift();return n},n.parseResponse=function(e){var n=e.map((function(e){return e.trim()}));if(1===n.length&&n[0].includes(":")){var t=n[0].split(": "),r=t.shift();r||R(!1);var o={},i=new Set(S[r]),s=t.shift();s||R(!1);for(var a=0;a<t.length-1;a++){for(var u=t[a].split(" "),c="",f=u.length-1;f>=0&&(c=(u.pop()+" "+c).trim(),!i.has(c));f--);u.length>0||R(!1),o[s]=u.join(" "),s=c}return o[s]=t[t.length-1],{raw:n.join("\r\n"),name:r,parameters:o}}var l=n[0].match(/(.+?)(:|)$/im);if(!l)return this.logger.error({header:n[0]},"failed to parse header"),null;for(var p=l[1],m={},d=1;d<n.length;d++){var h=n[d].match(/^(.*?): (.*)$/im);h?m[h[1]]=h[2]:this.logger.error({line:n[d]},"failed to parse line")}return{raw:n.join("\r\n"),name:p,parameters:m}},e}(),k=function(e){function n(n,t,r){var i;return(i=e.call(this)||this).socket=n,i.logger=t,i.receivedCommand=r,i.lastReceivedMS=-1,i.watchdogTimer=null,i.notifySettings={slot:!1,transport:!1,remote:!1,configuration:!1,"dropped frames":!1},i.parser=new b(t),i.socket.setEncoding("utf-8"),i.socket.on("data",(function(e){i.onMessage(e)})),i.socket.on("error",(function(e){t.info({err:e},"error"),i.socket.destroy(),i.emit("disconnected"),t.info("manually disconnected")})),i.sendResponse(o.ConnectionInfo,{"protocol version":"1.11",model:"NodeJS HyperDeck Server Library"}),i}c(n,e);var i=n.prototype;return i.onMessage=function(e){var n=this;this.logger.info({data:e},"<-- received message from client"),this.lastReceivedMS=Date.now();var i=this.parser.receivedString(e);this.logger.info({cmds:i},"parsed commands");for(var s,a=function(){var e=s.value;if("watchdog"===e.name){n.watchdogTimer&&clearInterval(n.watchdogTimer);var i=e;i.parameters.period&&(n.watchdogTimer=setInterval((function(){Date.now()-n.lastReceivedMS>Number(i.parameters.period)&&(n.socket.destroy(),n.emit("disconnected"),n.watchdogTimer&&clearInterval(n.watchdogTimer))}),1e3*Number(i.parameters.period)))}else if("notify"===e.name){var a=e;if(!(Object.keys(a.parameters).length>0)){for(var u={},c=0,f=Object.keys(n.notifySettings);c<f.length;c++){var l=f[c];u[l]=n.notifySettings[l]?"true":"false"}return n.sendResponse(r.Notify,u,e),"continue"}for(var p=0,m=Object.keys(a.parameters);p<m.length;p++){var d=m[p];void 0!==n.notifySettings[d]&&(n.notifySettings[d]="true"===a.parameters[d])}}n.receivedCommand(e).then((function(i){return"object"==typeof i?n.sendResponse(i.code,"params"in i&&i.params||"message"in i&&i.message||void 0,e):"number"==typeof i&&(t[i]||r[i]||o[i])?n.sendResponse(i,void 0,e):(n.logger.error({cmd:e,codeOrObj:i},"codeOrObj was neither a ResponseCode nor a response object"),void n.sendResponse(t.InternalError,void 0,e))}),(function(){return n.sendResponse(t.Unsupported,void 0,e)}))},u=function(e,n){var t;if("undefined"==typeof Symbol||null==e[Symbol.iterator]){if(Array.isArray(e)||(t=function(e,n){if(e){if("string"==typeof e)return h(e,void 0);var t=Object.prototype.toString.call(e).slice(8,-1);return"Object"===t&&e.constructor&&(t=e.constructor.name),"Map"===t||"Set"===t?Array.from(e):"Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?h(e,void 0):void 0}}(e))){t&&(e=t);var r=0;return function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}return(t=e[Symbol.iterator]()).next.bind(t)}(i);!(s=u()).done;)a()},i.sendResponse=function(e,n,r){var o=function(e,n){if("string"==typeof n)return e+" "+n.replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/:/g,"")+"\r\n";var t=e+" "+I[e];if(!n)return t+"\r\n";var r=Object.entries(n).filter((function(e){return null!=e[1]}));return 0===r.length?t+"\r\n":r.reduce((function(e,n){var t,r=n[0],o=n[1];return"string"==typeof o?t=o:"boolean"==typeof o?t=o?"true":"false":"number"==typeof o?t=o.toString():R(!1),e+r.replace(/([a-z])([A-Z]+)/,"$1 $2").toLowerCase()+": "+t+"\r\n"}),t+":\r\n")+"\r\n"}(e,n);this.logger[t[e]?"error":"info"]({responseText:o,cmd:r},"--\x3e send response to client"),this.socket.write(o)},i.notify=function(e,n){this.logger.info({type:e,params:n},"notify"),"configuration"===e&&this.notifySettings.configuration?this.sendResponse(o.ConfigurationInfo,n):"remote"===e&&this.notifySettings.remote?this.sendResponse(o.RemoteInfo,n):"slot"===e&&this.notifySettings.slot?this.sendResponse(o.SlotInfo,n):"transport"===e&&this.notifySettings.transport?this.sendResponse(o.TransportInfo,n):this.logger.error({type:e,params:n},"unhandled notify type")},n}(i.EventEmitter),C=function(e){for(var n=e.clips.length,t={clipsCount:n},r=0;r<n;r++){var o=e.clips[r];t[(r+1).toString()]=o.name+" "+o.startT+" "+o.duration}return t},P=function(e){function n(){return e.apply(this,arguments)||this}return c(n,e),n}(d(Error)),O=function(){try{throw new P}catch(e){return Promise.reject(e)}};exports.HyperDeckServer=function(){function e(e,n){var o=this,i=this;void 0===n&&(n=u()),this.sockets={},this.onDeviceInfo=O,this.onDiskList=O,this.onPreview=O,this.onPlay=O,this.onPlayrangeSet=O,this.onPlayrangeClear=O,this.onRecord=O,this.onStop=O,this.onClipsCount=O,this.onClipsGet=O,this.onClipsAdd=O,this.onClipsClear=O,this.onTransportInfo=O,this.onSlotInfo=O,this.onSlotSelect=O,this.onGoTo=O,this.onJog=O,this.onShuttle=O,this.onConfiguration=O,this.onUptime=O,this.onFormat=O,this.onIdentify=O,this.onWatchdog=O,this.receivedCommand=function(e){try{return Promise.resolve(new Promise((function(e){return setTimeout((function(){return e()}),200)}))).then((function(){var n=!1;return i.logger.info({cmd:e},"<-- "+e.name),function(t,o){try{var s=function(){function t(t){var o=!1;if(n)return t;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){var t=!1;if(o)return n;function s(n){var o=!1;if(t)return n;function s(n){return o?n:"watchdog"===e.name||"ping"===e.name?r.OK:void R(!1)}var a=function(){if("identify"===e.name)return Promise.resolve(i.onIdentify(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("format"===e.name)return Promise.resolve(i.onFormat(e)).then((function(e){return e?(t=!0,{code:r.FormatReady,params:e}):(t=!0,r.OK)}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("uptime"===e.name)return Promise.resolve(i.onUptime(e)).then((function(e){return o=!0,{code:r.Uptime,params:e}}))}();return a&&a.then?a.then(s):s(a)}if("remote"===e.name)return{code:r.Remote,params:{enabled:!0,override:!1}};var a=function(){if("configuration"===e.name)return Promise.resolve(i.onConfiguration(e)).then((function(e){return e?(t=!0,{code:r.Configuration,params:e}):(t=!0,r.OK)}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("shuttle"===e.name)return Promise.resolve(i.onShuttle(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("jog"===e.name)return Promise.resolve(i.onJog(e)).then((function(){return t=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}if("notify"===e.name)return r.OK;var a=function(){if("go to"===e.name)return Promise.resolve(i.onGoTo(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("slot select"===e.name)return Promise.resolve(i.onSlotSelect(e)).then((function(){return t=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("slot info"===e.name)return Promise.resolve(i.onSlotInfo(e)).then((function(e){return o=!0,{code:r.SlotInfo,params:e}}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("transport info"===e.name)return Promise.resolve(i.onTransportInfo(e)).then((function(e){return t=!0,{code:r.TransportInfo,params:e}}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("clips clear"===e.name)return Promise.resolve(i.onClipsClear(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("clips add"===e.name)return Promise.resolve(i.onClipsAdd(e)).then((function(){return t=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("clips get"===e.name)return Promise.resolve(i.onClipsGet(e).then(C)).then((function(e){return o=!0,{code:r.ClipsInfo,params:e}}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("clips count"===e.name)return Promise.resolve(i.onClipsCount(e)).then((function(e){return t=!0,{code:r.ClipsCount,params:e}}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("stop"===e.name)return Promise.resolve(i.onStop(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("record"===e.name)return Promise.resolve(i.onRecord(e)).then((function(){return t=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("playrange clear"===e.name)return Promise.resolve(i.onPlayrangeClear(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("playrange set"===e.name)return Promise.resolve(i.onPlayrangeSet(e)).then((function(){return t=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("play"===e.name)return Promise.resolve(i.onPlay(e)).then((function(){return o=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("preview"===e.name)return Promise.resolve(i.onPreview(e)).then((function(){return t=!0,r.OK}))}();return a&&a.then?a.then(s):s(a)}var a=function(){if("disk list"===e.name)return Promise.resolve(i.onDiskList(e)).then((function(e){return o=!0,{code:r.DiskList,params:e}}))}();return a&&a.then?a.then(s):s(a)}var o=function(){if("device info"===e.name)return Promise.resolve(i.onDeviceInfo(e)).then((function(e){return n=!0,{code:r.DeviceInfo,params:e}}))}();return o&&o.then?o.then(t):t(o)}()}catch(e){return o(e)}return s&&s.then?s.then(void 0,o):s}(0,(function(n){return n instanceof P?(i.logger.error({cmd:e},"unimplemented"),t.Unsupported):(i.logger.error({cmd:e,err:n.message},"unhandled command name"),t.InternalError)}))}))}catch(e){return Promise.reject(e)}},this.logger=n.child({name:"HyperDeck Emulator"}),this.server=a.createServer((function(e){o.logger.info("connection");var n=Math.random().toString(35).substr(-6),t=o.logger.child({name:"HyperDeck socket "+n});o.sockets[n]=new k(e,t,(function(e){return o.receivedCommand(e)})),o.sockets[n].on("disconnected",(function(){t.info("disconnected"),delete o.sockets[n]}))})),this.server.on("listening",(function(){return o.logger.info("listening")})),this.server.on("close",(function(){return o.logger.info("connection closed")})),this.server.on("error",(function(e){return o.logger.error("server error:",e)})),this.server.maxConnections=1,this.server.listen(9993,e)}var n=e.prototype;return n.close=function(){this.server.unref()},n.notifySlot=function(e){this.notify("slot",e)},n.notifyTransport=function(e){this.notify("transport",e)},n.notify=function(e,n){for(var t=0,r=Object.keys(this.sockets);t<r.length;t++)this.sockets[r[t]].notify(e,n)},e}(),exports.ResponseInterface={__proto__:null},exports.Timecode=function(e,n,t,r){var o=[e,n,t,r].map((function(e){var n=Math.floor(e);return n===e&&e>=0&&e<=99||R(!1),(n+100).toString().slice(-2)})).join(":");this.toString=function(){return o}};
"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var n,t,r,o,i=require("events"),a=e(require("util")),s=require("net"),u=e(require("pino"));function c(e,n){e.prototype=Object.create(n.prototype),e.prototype.constructor=e,e.__proto__=n}function l(e){return(l=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function f(e,n){return(f=Object.setPrototypeOf||function(e,n){return e.__proto__=n,e})(e,n)}function d(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}function p(e,n,t){return(p=d()?Reflect.construct:function(e,n,t){var r=[null];r.push.apply(r,n);var o=new(Function.bind.apply(e,r));return t&&f(o,t.prototype),o}).apply(null,arguments)}function m(e){var n="function"==typeof Map?new Map:void 0;return(m=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==n){if(n.has(e))return n.get(e);n.set(e,t)}function t(){return p(e,arguments,l(this).constructor)}return t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),f(t,e)})(e)}function h(e,n){(null==n||n>e.length)&&(n=e.length);for(var t=0,r=new Array(n);t<n;t++)r[t]=e[t];return r}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator"))),function(e){e[e.SyntaxError=100]="SyntaxError",e[e.UnsupportedParameter=101]="UnsupportedParameter",e[e.InvalidValue=102]="InvalidValue",e[e.Unsupported=103]="Unsupported",e[e.DiskFull=104]="DiskFull",e[e.NoDisk=105]="NoDisk",e[e.DiskError=106]="DiskError",e[e.TimelineEmpty=107]="TimelineEmpty",e[e.InternalError=108]="InternalError",e[e.OutOfRange=109]="OutOfRange",e[e.NoInput=110]="NoInput",e[e.RemoteControlDisabled=111]="RemoteControlDisabled",e[e.ConnectionRejected=120]="ConnectionRejected",e[e.InvalidState=150]="InvalidState",e[e.InvalidCodec=151]="InvalidCodec",e[e.InvalidFormat=160]="InvalidFormat",e[e.InvalidToken=161]="InvalidToken",e[e.FormatNotPrepared=162]="FormatNotPrepared"}(t||(t={})),function(e){e[e.OK=200]="OK",e[e.SlotInfo=202]="SlotInfo",e[e.DeviceInfo=204]="DeviceInfo",e[e.ClipsInfo=205]="ClipsInfo",e[e.DiskList=206]="DiskList",e[e.TransportInfo=208]="TransportInfo",e[e.Notify=209]="Notify",e[e.Remote=210]="Remote",e[e.Configuration=211]="Configuration",e[e.ClipsCount=214]="ClipsCount",e[e.Uptime=215]="Uptime",e[e.FormatReady=216]="FormatReady"}(r||(r={})),function(e){e[e.ConnectionInfo=500]="ConnectionInfo",e[e.SlotInfo=502]="SlotInfo",e[e.TransportInfo=508]="TransportInfo",e[e.RemoteInfo=510]="RemoteInfo",e[e.ConfigurationInfo=511]="ConfigurationInfo"}(o||(o={}));var v,g,y,b=((n={})[o.ConfigurationInfo]="configuration info",n[o.ConnectionInfo]="connection info",n[o.RemoteInfo]="remote info",n[o.SlotInfo]="slot info",n[o.TransportInfo]="transport info",n[t.ConnectionRejected]="connection rejected",n[t.DiskError]="disk error",n[t.DiskFull]="disk full",n[t.FormatNotPrepared]="format not prepared",n[t.InternalError]="internal error",n[t.InvalidCodec]="invalid codec",n[t.InvalidFormat]="invalid format",n[t.InvalidState]="invalid state",n[t.InvalidToken]="invalid token",n[t.InvalidValue]="invalid value",n[t.NoDisk]="no disk",n[t.NoInput]="no input",n[t.OutOfRange]="out of range",n[t.RemoteControlDisabled]="remote control disabled",n[t.SyntaxError]="syntax error",n[t.TimelineEmpty]="timeline empty",n[t.Unsupported]="unsupported",n[t.UnsupportedParameter]="unsupported parameter",n[r.ClipsCount]="clips count",n[r.ClipsInfo]="clips info",n[r.Configuration]="configuration",n[r.DeviceInfo]="device info",n[r.DiskList]="disk list",n[r.FormatReady]="format ready",n[r.Notify]="notify",n[r.OK]="ok",n[r.Remote]="remote",n[r.SlotInfo]="slot info",n[r.TransportInfo]="transport info",n[r.Uptime]="uptime",n);function I(e,n){if(!e){for(var t=arguments.length,r=new Array(t>2?t-2:0),o=2;o<t;o++)r[o-2]=arguments[o];throw new Error(a.format.apply(a,[n].concat(r)))}}!function(e){e.QuickTimeUncompressed="QuickTimeUncompressed",e.QuickTimeProResHQ="QuickTimeProResHQ",e.QuickTimeProRes="QuickTimeProRes",e.QuickTimeProResLT="QuickTimeProResLT",e.QuickTimeProResProxy="QuickTimeProResProxy",e.QuickTimeDNxHR220="QuickTimeDNxHR220",e.DNxHR220="DNxHR220"}(v||(v={})),function(e){e.embedded="embedded",e.XLR="XLR",e.RCA="RCA"}(g||(g={})),function(e){e.SDI="SDI",e.HDMI="HDMI",e.component="component"}(y||(y={}));var O=(new function(e){var n=this;void 0===e&&(e={}),this.options=e,this.addOption=function(e,t){var r,o=Array.isArray(e)?e[0]:e;return n.options.hasOwnProperty(o)&&I(!1),Object.assign(n.options,((r={})[o]=t,r)),n},this.getParamsByKey=function(){return Object.entries(n.options).reduce((function(e,n){var t=n[1];return e[n[0]]=new Set(t.arguments?Object.keys(t.arguments).map((function(e){return e.replace(/([a-z])([A-Z]+)/g,"$1 $2").toLowerCase()})):[]),e}),{})}}).addOption(["help","?"],{description:"Provides help text on all commands and parameters"}).addOption("commands",{description:"return commands in XML format"}).addOption("device info",{description:"return device information"}).addOption("disk list",{description:"query clip list on active disk",arguments:{slotId:"number"}}).addOption("quit",{description:"disconnect ethernet control"}).addOption("ping",{description:"check device is responding"}).addOption("preview",{description:"switch to preview or output",arguments:{enable:"boolean"}}).addOption("play",{description:"play from current timecode",arguments:{speed:"number",loop:"boolean",singleClip:"boolean"}}).addOption("playrange",{description:"query playrange setting"}).addOption("playrange set",{description:"set play range to play clip {n} only",arguments:{clipId:"string",in:"timecode",out:"timecode",timelineIn:"number",timelineOut:"number"}}).addOption("playrange clear",{description:"clear/reset play range setting"}).addOption("play on startup",{description:"query unit play on startup state",arguments:{enable:"boolean",singleClip:"boolean"}}).addOption("play option",{description:"query play options",arguments:{stopMode:"stopmode"}}).addOption("record",{description:"record from current input",arguments:{name:"string"}}).addOption("record spill",{description:"spill current recording to next slot",arguments:{slotId:"number"}}).addOption("stop",{description:"stop playback or recording"}).addOption("clips count",{description:"query number of clips on timeline"}).addOption("clips get",{description:"query all timeline clips",arguments:{clipId:"number",count:"number",version:"number"}}).addOption("clips add",{description:"append a clip to timeline",arguments:{name:"string",clipId:"string",in:"timecode",out:"timecode"}}).addOption("clips remove",{description:"remove clip {n} from the timeline (invalidates clip ids following clip {n})",arguments:{clidId:"string"}}).addOption("clips clear",{description:"empty timeline clip list"}).addOption("transport info",{description:"query current activity"}).addOption("slot info",{description:"query active slot",arguments:{slotId:"number"}}).addOption("slot select",{description:"switch to specified slot",arguments:{slotId:"number",videoFormat:"videoformat"}}).addOption("slot unblock",{description:"unblock active slot",arguments:{slotId:"number"}}).addOption("dynamic range",{description:"query dynamic range settings",arguments:{playbackOverride:"string"}}).addOption("notify",{description:"query notification status",arguments:{remote:"boolean",transport:"boolean",slot:"boolean",configuration:"boolean",droppedFrames:"boolean",displayTimecode:"boolean",timelinePosition:"boolean",playrange:"boolean",dynamicRange:"boolean"}}).addOption("goto",{description:"go forward or backward within a clip or timeline",arguments:{clipId:"string",clip:"goto",timeline:"goto",timecode:"timecode",slotId:"number"}}).addOption("jog",{description:"jog forward or backward",arguments:{timecode:"timecode"}}).addOption("shuttle",{description:"shuttle with speed",arguments:{speed:"number"}}).addOption("remote",{description:"query unit remote control state",arguments:{enable:"boolean",override:"boolean"}}).addOption("configuration",{description:"query configuration settings",arguments:{videoInput:"videoinput",audioInput:"audioinput",fileFormat:"fileformat",audioCodec:"audiocodec",timecodeInput:"timecodeinput",timecodePreset:"timecode",audioInputChannels:"number",recordTrigger:"recordtrigger",recordPrefix:"string",appendTimestamp:"boolean"}}).addOption("uptime",{description:"return time since last boot"}).addOption("format",{description:"prepare a disk formatting operation to filesystem {format}",arguments:{prepare:"string",confirm:"string"}}).addOption("identify",{description:"identify the device",arguments:{enable:"boolean"}}).addOption("watchdog",{description:"client connection timeout",arguments:{period:"number"}}).getParamsByKey(),S=function(){function e(e){this.linesQueue=[],this.logger=e.child({name:"MultilineParser"})}var n=e.prototype;return n.receivedString=function(e){var n=[],t=e.split("\r\n");for(t.length>0&&""===t[t.length-1]&&t.pop(),this.linesQueue=this.linesQueue.concat(t);this.linesQueue.length>0;)if(""!==this.linesQueue[0])if(!this.linesQueue[0].includes(":")||1===this.linesQueue.length&&this.linesQueue[0].includes(":")){var r=this.parseResponse(this.linesQueue.splice(0,1));r&&n.push(r)}else{var o=this.linesQueue.indexOf("");if(-1===o)break;var i=this.linesQueue.splice(0,o+1),a=this.parseResponse(i);a&&n.push(a)}else this.linesQueue.shift();return n},n.parseResponse=function(e){var n=e.map((function(e){return e.trim()}));if(1===n.length&&n[0].includes(":")){var t=n[0].split(": "),r=t.shift();r||I(!1),O.hasOwnProperty(r)||I(!1);var o={},i=O[r],a=t.shift();a||I(!1);for(var s=0;s<t.length-1;s++){for(var u=t[s].split(" "),c="",l=u.length-1;l>=0&&(c=(u.pop()+" "+c).trim(),!i.has(c));l--);u.length>0||I(!1),o[a]=u.join(" "),a=c}return o[a]=t[t.length-1],{raw:n.join("\r\n"),name:r,parameters:o}}var f=n[0].match(/(.+?)(:|)$/im);if(!f)return this.logger.error({header:n[0]},"failed to parse header"),null;for(var d=f[1],p={},m=1;m<n.length;m++){var h=n[m].match(/^(.*?): (.*)$/im);h?p[h[1]]=h[2]:this.logger.error({line:n[m]},"failed to parse line")}return{raw:n.join("\r\n"),name:d,parameters:p}},e}(),k=function(e){function n(n,t,r){var i;return(i=e.call(this)||this).socket=n,i.logger=t,i.receivedCommand=r,i.lastReceivedMS=-1,i.watchdogTimer=null,i.notifySettings={slot:!1,transport:!1,remote:!1,configuration:!1,"dropped frames":!1},i.parser=new S(t),i.socket.setEncoding("utf-8"),i.socket.on("data",(function(e){i.onMessage(e)})),i.socket.on("error",(function(e){t.info({err:e},"error"),i.socket.destroy(),i.emit("disconnected"),t.info("manually disconnected")})),i.sendResponse(o.ConnectionInfo,{"protocol version":"1.11",model:"NodeJS HyperDeck Server Library"}),i}c(n,e);var i=n.prototype;return i.onMessage=function(e){var n=this;this.logger.info({data:e},"<-- received message from client"),this.lastReceivedMS=Date.now();var i=this.parser.receivedString(e);this.logger.info({cmds:i},"parsed commands");for(var a,s=function(){var e=a.value;if("watchdog"===e.name){n.watchdogTimer&&clearInterval(n.watchdogTimer);var i=e;i.parameters.period&&(n.watchdogTimer=setInterval((function(){Date.now()-n.lastReceivedMS>Number(i.parameters.period)&&(n.socket.destroy(),n.emit("disconnected"),n.watchdogTimer&&clearInterval(n.watchdogTimer))}),1e3*Number(i.parameters.period)))}else if("notify"===e.name){var s=e;if(!(Object.keys(s.parameters).length>0)){for(var u={},c=0,l=Object.keys(n.notifySettings);c<l.length;c++){var f=l[c];u[f]=n.notifySettings[f]?"true":"false"}return n.sendResponse(r.Notify,u,e),"continue"}for(var d=0,p=Object.keys(s.parameters);d<p.length;d++){var m=p[d];void 0!==n.notifySettings[m]&&(n.notifySettings[m]="true"===s.parameters[m])}}n.receivedCommand(e).then((function(i){return"object"==typeof i?n.sendResponse(i.code,"params"in i&&i.params||"message"in i&&i.message||void 0,e):"number"==typeof i&&(t[i]||r[i]||o[i])?n.sendResponse(i,void 0,e):(n.logger.error({cmd:e,codeOrObj:i},"codeOrObj was neither a ResponseCode nor a response object"),void n.sendResponse(t.InternalError,void 0,e))}),(function(){return n.sendResponse(t.Unsupported,void 0,e)}))},u=function(e,n){var t;if("undefined"==typeof Symbol||null==e[Symbol.iterator]){if(Array.isArray(e)||(t=function(e,n){if(e){if("string"==typeof e)return h(e,void 0);var t=Object.prototype.toString.call(e).slice(8,-1);return"Object"===t&&e.constructor&&(t=e.constructor.name),"Map"===t||"Set"===t?Array.from(e):"Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?h(e,void 0):void 0}}(e))){t&&(e=t);var r=0;return function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}return(t=e[Symbol.iterator]()).next.bind(t)}(i);!(a=u()).done;)s()},i.sendResponse=function(e,n,r){var o=function(e,n){if("string"==typeof n)return e+" "+n.replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/:/g,"")+"\r\n";var t=e+" "+b[e];if(!n)return t+"\r\n";var r=Object.entries(n).filter((function(e){return null!=e[1]}));return 0===r.length?t+"\r\n":r.reduce((function(e,n){var t,r=n[0],o=n[1];return"string"==typeof o?t=o:"boolean"==typeof o?t=o?"true":"false":"number"==typeof o?t=o.toString():I(!1),e+r.replace(/([a-z])([A-Z]+)/g,"$1 $2").toLowerCase()+": "+t+"\r\n"}),t+":\r\n")+"\r\n"}(e,n);this.logger[t[e]?"error":"info"]({responseText:o,cmd:r},"--\x3e send response to client"),this.socket.write(o)},i.notify=function(e,n){this.logger.info({type:e,params:n},"notify"),"configuration"===e&&this.notifySettings.configuration?this.sendResponse(o.ConfigurationInfo,n):"remote"===e&&this.notifySettings.remote?this.sendResponse(o.RemoteInfo,n):"slot"===e&&this.notifySettings.slot?this.sendResponse(o.SlotInfo,n):"transport"===e&&this.notifySettings.transport?this.sendResponse(o.TransportInfo,n):this.logger.error({type:e,params:n},"unhandled notify type")},n}(i.EventEmitter),C=function(e){for(var n=e.clips.length,t={clipsCount:n},r=0;r<n;r++){var o=e.clips[r];t[(r+1).toString()]=o.name+" "+o.startT+" "+o.duration}return t},P=function(e){function n(){return e.apply(this,arguments)||this}return c(n,e),n}(m(Error)),R=function(){try{throw new P}catch(e){return Promise.reject(e)}};exports.HyperDeckServer=function(){function e(e,n){var o=this,i=this;void 0===n&&(n=u()),this.sockets={},this.onDeviceInfo=R,this.onDiskList=R,this.onPreview=R,this.onPlay=R,this.onPlayrangeSet=R,this.onPlayrangeClear=R,this.onRecord=R,this.onStop=R,this.onClipsCount=R,this.onClipsGet=R,this.onClipsAdd=R,this.onClipsClear=R,this.onTransportInfo=R,this.onSlotInfo=R,this.onSlotSelect=R,this.onGoTo=R,this.onJog=R,this.onShuttle=R,this.onConfiguration=R,this.onUptime=R,this.onFormat=R,this.onIdentify=R,this.onWatchdog=R,this.receivedCommand=function(e){try{return Promise.resolve(new Promise((function(e){return setTimeout((function(){return e()}),200)}))).then((function(){var n=!1;return i.logger.info({cmd:e},"<-- "+e.name),function(t,o){try{var a=function(){function t(t){var o=!1;if(n)return t;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){var t=!1;if(o)return n;function a(n){var o=!1;if(t)return n;function a(n){return o?n:"watchdog"===e.name||"ping"===e.name?r.OK:void I(!1)}var s=function(){if("identify"===e.name)return Promise.resolve(i.onIdentify(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("format"===e.name)return Promise.resolve(i.onFormat(e)).then((function(e){return e?(t=!0,{code:r.FormatReady,params:e}):(t=!0,r.OK)}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("uptime"===e.name)return Promise.resolve(i.onUptime(e)).then((function(e){return o=!0,{code:r.Uptime,params:e}}))}();return s&&s.then?s.then(a):a(s)}if("remote"===e.name)return{code:r.Remote,params:{enabled:!0,override:!1}};var s=function(){if("configuration"===e.name)return Promise.resolve(i.onConfiguration(e)).then((function(e){return e?(t=!0,{code:r.Configuration,params:e}):(t=!0,r.OK)}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("shuttle"===e.name)return Promise.resolve(i.onShuttle(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("jog"===e.name)return Promise.resolve(i.onJog(e)).then((function(){return t=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}if("notify"===e.name)return r.OK;var s=function(){if("go to"===e.name)return Promise.resolve(i.onGoTo(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("slot select"===e.name)return Promise.resolve(i.onSlotSelect(e)).then((function(){return t=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("slot info"===e.name)return Promise.resolve(i.onSlotInfo(e)).then((function(e){return o=!0,{code:r.SlotInfo,params:e}}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("transport info"===e.name)return Promise.resolve(i.onTransportInfo(e)).then((function(e){return t=!0,{code:r.TransportInfo,params:e}}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("clips clear"===e.name)return Promise.resolve(i.onClipsClear(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("clips add"===e.name)return Promise.resolve(i.onClipsAdd(e)).then((function(){return t=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("clips get"===e.name)return Promise.resolve(i.onClipsGet(e).then(C)).then((function(e){return o=!0,{code:r.ClipsInfo,params:e}}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("clips count"===e.name)return Promise.resolve(i.onClipsCount(e)).then((function(e){return t=!0,{code:r.ClipsCount,params:e}}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("stop"===e.name)return Promise.resolve(i.onStop(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("record"===e.name)return Promise.resolve(i.onRecord(e)).then((function(){return t=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("playrange clear"===e.name)return Promise.resolve(i.onPlayrangeClear(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("playrange set"===e.name)return Promise.resolve(i.onPlayrangeSet(e)).then((function(){return t=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("play"===e.name)return Promise.resolve(i.onPlay(e)).then((function(){return o=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("preview"===e.name)return Promise.resolve(i.onPreview(e)).then((function(){return t=!0,r.OK}))}();return s&&s.then?s.then(a):a(s)}var s=function(){if("disk list"===e.name)return Promise.resolve(i.onDiskList(e)).then((function(e){return o=!0,{code:r.DiskList,params:e}}))}();return s&&s.then?s.then(a):a(s)}var o=function(){if("device info"===e.name)return Promise.resolve(i.onDeviceInfo(e)).then((function(e){return n=!0,{code:r.DeviceInfo,params:e}}))}();return o&&o.then?o.then(t):t(o)}()}catch(e){return o(e)}return a&&a.then?a.then(void 0,o):a}(0,(function(n){return n instanceof P?(i.logger.error({cmd:e},"unimplemented"),t.Unsupported):(i.logger.error({cmd:e,err:n.message},"unhandled command name"),t.InternalError)}))}))}catch(e){return Promise.reject(e)}},this.logger=n.child({name:"HyperDeck Emulator"}),this.server=s.createServer((function(e){o.logger.info("connection");var n=Math.random().toString(35).substr(-6),t=o.logger.child({name:"HyperDeck socket "+n});o.sockets[n]=new k(e,t,(function(e){return o.receivedCommand(e)})),o.sockets[n].on("disconnected",(function(){t.info("disconnected"),delete o.sockets[n]}))})),this.server.on("listening",(function(){return o.logger.info("listening")})),this.server.on("close",(function(){return o.logger.info("connection closed")})),this.server.on("error",(function(e){return o.logger.error("server error:",e)})),this.server.maxConnections=1,this.server.listen(9993,e)}var n=e.prototype;return n.close=function(){this.server.unref()},n.notifySlot=function(e){this.notify("slot",e)},n.notifyTransport=function(e){this.notify("transport",e)},n.notify=function(e,n){for(var t=0,r=Object.keys(this.sockets);t<r.length;t++)this.sockets[r[t]].notify(e,n)},e}(),exports.ResponseInterface={__proto__:null},exports.Timecode=function(e,n,t,r){var o=[e,n,t,r].map((function(e){var n=Math.floor(e);return n===e&&e>=0&&e<=99||I(!1),(n+100).toString().slice(-2)})).join(":");this.toString=function(){return o}};
//# sourceMappingURL=hyperdeck-emulator.cjs.production.min.js.map

@@ -239,33 +239,2 @@ import { EventEmitter } from 'events';

var CRLF = '\r\n';
var parametersByCommandName = {
help: [],
commands: [],
'device info': [],
'disk list': ['slot id'],
quit: [],
ping: [],
preview: ['enable'],
play: ['speed', 'loop', 'single clip'],
'playrange set': ['clip id', 'in', 'out'],
'playrange clear': [],
record: ['name'],
stop: [],
'clips count': [],
'clips get': ['clip id', 'count'],
'clips add': ['name'],
'clips clear': [],
'transport info': [],
'slot info': ['slot id'],
'slot select': ['slot id', 'video format'],
notify: ['remote', 'transport', 'slot', 'configuration', 'dropped frames'],
"goto": ['clip id', 'clip', 'timeline', 'timecode', 'slot id'],
jog: ['timecode'],
shuttle: ['speed'],
remote: ['enable', 'override'],
configuration: ['video input', 'audio input', 'file format'],
uptime: [],
format: ['prepare', 'confirm'],
identify: ['enable'],
watchdog: ['period']
};

@@ -282,2 +251,226 @@ function invariant(condition, message) {

/** Internal container class that holds metadata about each HyperDeck event */
var HyperDeckAPI = function HyperDeckAPI(options) {
var _this = this;
if (options === void 0) {
options = {};
}
this.options = options;
this.addOption = function (key, option) {
var _Object$assign;
var k = Array.isArray(key) ? key[0] : key;
!!_this.options.hasOwnProperty(k) ? process.env.NODE_ENV !== "production" ? invariant(false, 'option already exists for key `%s`', k) : invariant(false) : void 0; // NOTE: this mutates the original options object
// shouldn't be a problem since this is only used internally
Object.assign(_this.options, (_Object$assign = {}, _Object$assign[k] = option, _Object$assign));
return _this;
};
/** Get a Set of param names keyed by function name */
this.getParamsByKey = function () {
return Object.entries(_this.options).reduce(function (prev, _ref) {
var key = _ref[0],
value = _ref[1];
prev[key] = new Set(value.arguments ? Object.keys(value.arguments).map(function (key) {
return key.replace(/([a-z])([A-Z]+)/g, '$1 $2').toLowerCase();
}) : []);
return prev;
}, {});
};
};
var api = /*#__PURE__*/new HyperDeckAPI().addOption(['help', '?'], {
description: 'Provides help text on all commands and parameters'
}).addOption('commands', {
description: 'return commands in XML format'
}).addOption('device info', {
description: 'return device information'
}).addOption('disk list', {
description: 'query clip list on active disk',
arguments: {
slotId: 'number'
}
}).addOption('quit', {
description: 'disconnect ethernet control'
}).addOption('ping', {
description: 'check device is responding'
}).addOption('preview', {
description: 'switch to preview or output',
arguments: {
enable: 'boolean'
}
}).addOption('play', {
description: 'play from current timecode',
arguments: {
speed: 'number',
loop: 'boolean',
singleClip: 'boolean'
}
}).addOption('playrange', {
description: 'query playrange setting'
}).addOption('playrange set', {
description: 'set play range to play clip {n} only',
arguments: {
// maybe number?
clipId: 'string',
// description: 'set play range to play between timecode {inT} and timecode {outT}',
"in": 'timecode',
out: 'timecode',
// 'set play range in units of frames between timeline position {in} and position {out} clear/reset play range°setting',
timelineIn: 'number',
timelineOut: 'number'
}
}).addOption('playrange clear', {
description: 'clear/reset play range setting'
}).addOption('play on startup', {
description: 'query unit play on startup state',
// description: 'enable or disable play on startup',
arguments: {
enable: 'boolean',
singleClip: 'boolean'
}
}).addOption('play option', {
description: 'query play options',
arguments: {
stopMode: 'stopmode'
}
}).addOption('record', {
description: 'record from current input',
arguments: {
name: 'string'
}
}).addOption('record spill', {
description: 'spill current recording to next slot',
arguments: {
slotId: 'number'
}
}).addOption('stop', {
description: 'stop playback or recording'
}).addOption('clips count', {
description: 'query number of clips on timeline'
}).addOption('clips get', {
description: 'query all timeline clips',
arguments: {
clipId: 'number',
count: 'number',
version: 'number'
}
}).addOption('clips add', {
description: 'append a clip to timeline',
arguments: {
name: 'string',
clipId: 'string',
"in": 'timecode',
out: 'timecode'
}
}).addOption('clips remove', {
description: 'remove clip {n} from the timeline (invalidates clip ids following clip {n})',
arguments: {
clidId: 'string'
}
}).addOption('clips clear', {
description: 'empty timeline clip list'
}).addOption('transport info', {
description: 'query current activity'
}).addOption('slot info', {
description: 'query active slot',
arguments: {
slotId: 'number'
}
}).addOption('slot select', {
description: 'switch to specified slot',
arguments: {
slotId: 'number',
videoFormat: 'videoformat'
}
}).addOption('slot unblock', {
description: 'unblock active slot',
arguments: {
slotId: 'number'
}
}).addOption('dynamic range', {
description: 'query dynamic range settings',
arguments: {
// TODO(meyer) is this correct?
playbackOverride: 'string'
}
}).addOption('notify', {
description: 'query notification status',
arguments: {
remote: 'boolean',
transport: 'boolean',
slot: 'boolean',
configuration: 'boolean',
droppedFrames: 'boolean',
displayTimecode: 'boolean',
timelinePosition: 'boolean',
playrange: 'boolean',
dynamicRange: 'boolean'
}
}).addOption('goto', {
description: 'go forward or backward within a clip or timeline',
arguments: {
clipId: 'string',
clip: 'goto',
timeline: 'goto',
timecode: 'timecode',
slotId: 'number'
}
}).addOption('jog', {
description: 'jog forward or backward',
arguments: {
timecode: 'timecode'
}
}).addOption('shuttle', {
description: 'shuttle with speed',
arguments: {
speed: 'number'
}
}).addOption('remote', {
description: 'query unit remote control state',
arguments: {
enable: 'boolean',
override: 'boolean'
}
}).addOption('configuration', {
description: 'query configuration settings',
arguments: {
videoInput: 'videoinput',
audioInput: 'audioinput',
fileFormat: 'fileformat',
audioCodec: 'audiocodec',
timecodeInput: 'timecodeinput',
timecodePreset: 'timecode',
audioInputChannels: 'number',
recordTrigger: 'recordtrigger',
recordPrefix: 'string',
appendTimestamp: 'boolean'
}
}).addOption('uptime', {
description: 'return time since last boot'
}).addOption('format', {
description: 'prepare a disk formatting operation to filesystem {format}',
arguments: {
prepare: 'string',
confirm: 'string'
}
}).addOption('identify', {
description: 'identify the device',
arguments: {
enable: 'boolean'
}
}).addOption('watchdog', {
description: 'client connection timeout',
arguments: {
period: 'number'
}
});
var paramsByKey = /*#__PURE__*/api.getParamsByKey();
var MultilineParser = /*#__PURE__*/function () {

@@ -346,4 +539,5 @@ function MultilineParser(logger) {

!msg ? process.env.NODE_ENV !== "production" ? invariant(false, 'Unrecognised command') : invariant(false) : void 0;
!paramsByKey.hasOwnProperty(msg) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Invalid command: `%s`', msg) : invariant(false) : void 0;
var params = {};
var paramNames = new Set(parametersByCommandName[msg]);
var paramNames = paramsByKey[msg];
var param = bits.shift();

@@ -458,3 +652,3 @@ !param ? process.env.NODE_ENV !== "production" ? invariant(false, 'No named parameters found') : invariant(false) : void 0;

var formattedKey = key.replace(/([a-z])([A-Z]+)/, '$1 $2').toLowerCase();
var formattedKey = key.replace(/([a-z])([A-Z]+)/g, '$1 $2').toLowerCase();
return prev + formattedKey + ': ' + valueString + CRLF;

@@ -461,0 +655,0 @@ }, firstLine + ':' + CRLF) + CRLF;

{
"name": "@meyer/hyperdeck-emulator",
"version": "0.0.4-canary.35.72ad3f9",
"version": "0.0.4-canary.37.241f21f",
"description": "Typescript Node.js library for emulating a Blackmagic Hyperdeck",

@@ -46,8 +46,6 @@ "main": "dist/index.js",

"prettier": {
"trailingComma": "none",
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"useTabs": true,
"endOfLine": "lf",
"semi": false
"endOfLine": "lf"
},

@@ -59,3 +57,3 @@ "scripts": {

"lint:fix": "eslint --fix",
"test": "tsdx build && tsdx test",
"test": "tsdx build && yarn jest",
"prepack": "rimraf dist && tsdx build",

@@ -74,40 +72,15 @@ "test:coverage": "yarn test -- --coverage",

},
"scripts-info": {
"start": "Run tsdx in watch mode",
"info": "Display information about the scripts",
"build": "Build the library",
"lint": "Lint the project",
"unit": "Build the library and run unit tests",
"test": "Lint, build, and test the library",
"test:integration": "Integration tests. Work in progress",
"watch": "Watch source files, rebuild library on changes, rerun relevant tests",
"cov": "Run tests, generate the HTML coverage report, and open it in a browser",
"cov-open": "Open current test coverage",
"send-coverage": "send coverage to codecov",
"docs": "Generate HTML API documentation and open it in a browser",
"docs:test": "Running the docs generation for testing.",
"docs:html": "Generate HTML documentation",
"docs:json": "Generate API documentation in typedoc JSON format",
"docs:publish": "Generate HTML API documentation and push it to GitHub Pages",
"changelog": "Bump package.json version, update CHANGELOG.md, tag a release",
"release": "Clean, build, test, publish docs, and prepare release (a one-step publish process). Updates versions and creates git commits.",
"reset": "Delete all untracked files and reset the repo to the last commit",
"ci": "Test script for running by the CI (CircleCI)",
"validate:dependencies": "Scan dependencies for vulnerabilities and check licenses",
"license-validate": "Validate licenses for dependencies."
},
"devDependencies": {
"@types/jest": "^26.0.0",
"@types/long": "^4.0.1",
"@types/jest": "^26.0.3",
"@types/npm-packlist": "^1.1.1",
"@types/pino": "^6.3.0",
"@typescript-eslint/eslint-plugin": "^3.3.0",
"@typescript-eslint/parser": "^3.3.0",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"codecov": "^3.7.0",
"eslint": "^7",
"eslint": "^7.3.1",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.3",
"husky": "^4.2.5",
"jest": "^26.0.1",
"jest-cli": "^26.0.1",
"jest": "^26.1.0",
"jest-cli": "^26.1.0",
"lint-staged": "^10.1.3",

@@ -121,9 +94,8 @@ "node-license-validator": "^1.3.0",

"semver": "^7.3.2",
"sinon": "^9.0.2",
"standard-version": "^8.0.0",
"ts-jest": "^26.1.0",
"ts-jest": "^26.1.1",
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
"typescript": "^3.8.3"
"typescript": "^3.9.5"
}
}

@@ -30,19 +30,19 @@ # HyperDeck Emulator ![CI](https://github.com/meyer/hyperdeck-server-connection/workflows/CI/badge.svg) ![Canary](https://github.com/meyer/hyperdeck-server-connection/workflows/Canary/badge.svg)

```javascript
const { HyperdeckServer } = require('../dist/server')
const myHyperdeck = new Hyperdeck()
const { HyperdeckServer } = require('../dist/server');
const myHyperdeck = new Hyperdeck();
const s = new HyperdeckServer()
const s = new HyperdeckServer();
s.onPlay = async (cmd) => {
console.log('playing', cmd)
status.status = 'play'
s.notifyTransport({
...status,
speed: '100',
'slot id': '1',
'clip id': '1',
'single clip': 'true',
'video format': '1080i50',
loop: false
})
}
console.log('playing', cmd);
status.status = 'play';
s.notifyTransport({
...status,
speed: '100',
'slot id': '1',
'clip id': '1',
'single clip': 'true',
'video format': '1080i50',
loop: false,
});
};
```

@@ -49,0 +49,0 @@

@@ -1,53 +0,53 @@

import type { Socket } from 'net'
import { EventEmitter } from 'events'
import { getTestLogger } from './utils'
import { invariant } from '../invariant'
import type { Socket } from 'net';
import { EventEmitter } from 'events';
import { getTestLogger } from './utils';
import { invariant } from '../invariant';
const noop = (): any => {
return
}
return;
};
class MockSocket extends EventEmitter implements Pick<Socket, 'destroy' | 'setEncoding' | 'write'> {
destroy = noop
setEncoding = noop
write = jest.fn()
destroy = noop;
setEncoding = noop;
write = jest.fn();
}
jest.mock('net', () => ({
createServer: (connectionListener?: (socket: MockSocket) => void) => {
const mockSocket = new MockSocket()
invariant(connectionListener, 'Missing connectionListener')
connectionListener(mockSocket)
return {
listen: noop,
on: noop,
unref: noop
}
}
}))
createServer: (connectionListener?: (socket: MockSocket) => void) => {
const mockSocket = new MockSocket();
invariant(connectionListener, 'Missing connectionListener');
connectionListener(mockSocket);
return {
listen: noop,
on: noop,
unref: noop,
};
},
}));
describe('HyperdeckServer', () => {
beforeEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
jest.clearAllMocks();
});
it('sends output back to the ATEM', async () => {
expect.assertions(3)
it('sends output back to the ATEM', async () => {
expect.assertions(3);
const logger = getTestLogger()
const logger = getTestLogger();
const server = await import('../HyperDeckServer')
const hyperdeck = new server.HyperDeckServer('0.0.0.0', logger.logger)
const socketEntries = Object.entries(hyperdeck['sockets'])
expect(socketEntries.length).toBe(1)
const hyperdeckSocket = socketEntries[0][1]
const socket = hyperdeckSocket['socket']
const server = await import('../HyperDeckServer');
const hyperdeck = new server.HyperDeckServer('0.0.0.0', logger.logger);
const socketEntries = Object.entries(hyperdeck['sockets']);
expect(socketEntries.length).toBe(1);
const hyperdeckSocket = socketEntries[0][1];
const socket = hyperdeckSocket['socket'];
socket.emit('data', 'banana')
socket.emit('data', 'banana');
hyperdeck.close()
hyperdeck.close();
await new Promise((resolve) => setTimeout(() => resolve(), 500))
await new Promise((resolve) => setTimeout(() => resolve(), 500));
expect((socket.write as jest.Mock).mock.calls).toMatchInlineSnapshot(`
expect((socket.write as jest.Mock).mock.calls).toMatchInlineSnapshot(`
Array [

@@ -66,5 +66,5 @@ Array [

]
`)
`);
expect(logger.getLoggedOutput()).toMatchInlineSnapshot(`
expect(logger.getLoggedOutput()).toMatchInlineSnapshot(`
Array [

@@ -132,4 +132,4 @@ Object {

]
`)
})
})
`);
});
});

@@ -1,17 +0,17 @@

import { messageForCode } from '../messageForCode'
import { ErrorCode, AsynchronousCode, SynchronousCode } from '../types'
import { messageForCode } from '../messageForCode';
import { ErrorCode, AsynchronousCode, SynchronousCode } from '../types';
describe('messageForCode', () => {
it('works', () => {
expect(messageForCode(ErrorCode.ConnectionRejected)).toMatchInlineSnapshot(`
it('works', () => {
expect(messageForCode(ErrorCode.ConnectionRejected)).toMatchInlineSnapshot(`
"120 connection rejected
"
`)
`);
expect(
messageForCode(AsynchronousCode.ConnectionInfo, {
param1: 'wow',
param2: 'ok'
})
).toMatchInlineSnapshot(`
expect(
messageForCode(AsynchronousCode.ConnectionInfo, {
param1: 'wow',
param2: 'ok',
})
).toMatchInlineSnapshot(`
"500 connection info:

@@ -22,21 +22,21 @@ param1: wow

"
`)
`);
expect(messageForCode(SynchronousCode.OK, 'okie dokie')).toMatchInlineSnapshot(`
expect(messageForCode(SynchronousCode.OK, 'okie dokie')).toMatchInlineSnapshot(`
"200 okie dokie
"
`)
})
`);
});
it('filters out null and undefined values', () => {
expect(messageForCode(SynchronousCode.OK, { param1: null, param2: undefined }))
.toMatchInlineSnapshot(`
it('filters out null and undefined values', () => {
expect(messageForCode(SynchronousCode.OK, { param1: null, param2: undefined }))
.toMatchInlineSnapshot(`
"200 ok
"
`)
})
`);
});
it('stringifies primitives', () => {
expect(messageForCode(SynchronousCode.OK, { param1: 1234, param2: true, param3: false }))
.toMatchInlineSnapshot(`
it('stringifies primitives', () => {
expect(messageForCode(SynchronousCode.OK, { param1: 1234, param2: true, param3: false }))
.toMatchInlineSnapshot(`
"200 ok:

@@ -48,10 +48,10 @@ param1: 1234

"
`)
})
`);
});
it('throws an error if a non-primitive param type is encountered', () => {
expect(() =>
messageForCode(SynchronousCode.OK, { param1: { hmmm: true } })
).toThrowErrorMatchingInlineSnapshot(`"Unhandled value type: \`object\`"`)
})
})
it('throws an error if a non-primitive param type is encountered', () => {
expect(() =>
messageForCode(SynchronousCode.OK, { param1: { hmmm: true } })
).toThrowErrorMatchingInlineSnapshot(`"Unhandled value type: \`object\`"`);
});
});

@@ -1,95 +0,95 @@

import packlist = require('npm-packlist')
import path = require('path')
import packlist = require('npm-packlist');
import path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
const legacySort = (a: string, b: string) =>
a === 'package.json'
? -1
: b === 'package.json'
? 1
: /^node_modules/.test(a) && !/^node_modules/.test(b)
? 1
: /^node_modules/.test(b) && !/^node_modules/.test(a)
? -1
: path.dirname(a) === '.' && path.dirname(b) !== '.'
? -1
: path.dirname(b) === '.' && path.dirname(a) !== '.'
? 1
: a.localeCompare(b)
a === 'package.json'
? -1
: b === 'package.json'
? 1
: /^node_modules/.test(a) && !/^node_modules/.test(b)
? 1
: /^node_modules/.test(b) && !/^node_modules/.test(a)
? -1
: path.dirname(a) === '.' && path.dirname(b) !== '.'
? -1
: path.dirname(b) === '.' && path.dirname(a) !== '.'
? 1
: a.localeCompare(b);
describe('npm publish', () => {
it('only publishes the intended files', async () => {
const publishedFiles = await packlist({ path: PROJECT_ROOT }).then(
(fileList) =>
'\n' +
fileList
.sort(legacySort)
.map((f) => `- ${f}`)
.join('\n') +
'\n'
)
it('only publishes the intended files', async () => {
const publishedFiles = await packlist({ path: PROJECT_ROOT }).then(
(fileList) =>
'\n' +
fileList
.sort(legacySort)
.map((f) => `- ${f}`)
.join('\n') +
'\n'
);
expect(publishedFiles).toMatchInlineSnapshot(`
"
- package.json
- CHANGELOG.md
- LICENSE
- README.md
- dist/__tests__/utils.d.ts
- dist/__tests__/utils.d.ts.map
- dist/constants.d.ts
- dist/constants.d.ts.map
- dist/formatClipsGetResponse.d.ts
- dist/formatClipsGetResponse.d.ts.map
- dist/getEventHandler.d.ts
- dist/getEventHandler.d.ts.map
- dist/hyperdeck-emulator.cjs.development.js
- dist/hyperdeck-emulator.cjs.development.js.map
- dist/hyperdeck-emulator.cjs.production.min.js
- dist/hyperdeck-emulator.cjs.production.min.js.map
- dist/hyperdeck-emulator.esm.js
- dist/hyperdeck-emulator.esm.js.map
- dist/HyperDeckServer.d.ts
- dist/HyperDeckServer.d.ts.map
- dist/HyperDeckSocket.d.ts
- dist/HyperDeckSocket.d.ts.map
- dist/index.d.ts
- dist/index.d.ts.map
- dist/index.js
- dist/invariant.d.ts
- dist/invariant.d.ts.map
- dist/messageForCode.d.ts
- dist/messageForCode.d.ts.map
- dist/MultilineParser.d.ts
- dist/MultilineParser.d.ts.map
- dist/Timecode.d.ts
- dist/Timecode.d.ts.map
- dist/types.d.ts
- dist/types.d.ts.map
- dist/types/DeserializedCommands.d.ts
- dist/types/DeserializedCommands.d.ts.map
- dist/types/ResponseInterface.d.ts
- dist/types/ResponseInterface.d.ts.map
- src/__tests__/HyperDeckServer.spec.ts
- src/__tests__/messageForCode.spec.ts
- src/__tests__/meta.spec.ts
- src/__tests__/MultilineParser.spec.ts
- src/__tests__/utils.ts
- src/constants.ts
- src/formatClipsGetResponse.ts
- src/getEventHandler.ts
- src/HyperDeckServer.ts
- src/HyperDeckSocket.ts
- src/index.ts
- src/invariant.ts
- src/messageForCode.ts
- src/MultilineParser.ts
- src/Timecode.ts
- src/types.ts
- src/types/DeserializedCommands.ts
- src/types/ResponseInterface.ts
"
`)
})
})
expect(publishedFiles).toMatchInlineSnapshot(`
"
- package.json
- CHANGELOG.md
- LICENSE
- README.md
- dist/__tests__/utils.d.ts
- dist/__tests__/utils.d.ts.map
- dist/api.d.ts
- dist/api.d.ts.map
- dist/constants.d.ts
- dist/constants.d.ts.map
- dist/formatClipsGetResponse.d.ts
- dist/formatClipsGetResponse.d.ts.map
- dist/hyperdeck-emulator.cjs.development.js
- dist/hyperdeck-emulator.cjs.development.js.map
- dist/hyperdeck-emulator.cjs.production.min.js
- dist/hyperdeck-emulator.cjs.production.min.js.map
- dist/hyperdeck-emulator.esm.js
- dist/hyperdeck-emulator.esm.js.map
- dist/HyperDeckServer.d.ts
- dist/HyperDeckServer.d.ts.map
- dist/HyperDeckSocket.d.ts
- dist/HyperDeckSocket.d.ts.map
- dist/index.d.ts
- dist/index.d.ts.map
- dist/index.js
- dist/invariant.d.ts
- dist/invariant.d.ts.map
- dist/messageForCode.d.ts
- dist/messageForCode.d.ts.map
- dist/MultilineParser.d.ts
- dist/MultilineParser.d.ts.map
- dist/Timecode.d.ts
- dist/Timecode.d.ts.map
- dist/types.d.ts
- dist/types.d.ts.map
- dist/types/DeserializedCommands.d.ts
- dist/types/DeserializedCommands.d.ts.map
- dist/types/ResponseInterface.d.ts
- dist/types/ResponseInterface.d.ts.map
- src/__tests__/HyperDeckServer.spec.ts
- src/__tests__/messageForCode.spec.ts
- src/__tests__/meta.spec.ts
- src/__tests__/MultilineParser.spec.ts
- src/__tests__/utils.ts
- src/api.ts
- src/constants.ts
- src/formatClipsGetResponse.ts
- src/HyperDeckServer.ts
- src/HyperDeckSocket.ts
- src/index.ts
- src/invariant.ts
- src/messageForCode.ts
- src/MultilineParser.ts
- src/Timecode.ts
- src/types.ts
- src/types/DeserializedCommands.ts
- src/types/ResponseInterface.ts
"
`);
});
});

@@ -1,16 +0,16 @@

import { MultilineParser } from '../MultilineParser'
import { getTestLogger } from './utils'
import { CRLF } from '../constants'
import { MultilineParser } from '../MultilineParser';
import { getTestLogger } from './utils';
import { CRLF } from '../constants';
const getParser = () => {
const { logger, getLoggedOutput } = getTestLogger()
const parser = new MultilineParser(logger)
const parse = (message: string) => parser.receivedString(message)
return { parse, getLoggedOutput }
}
const { logger, getLoggedOutput } = getTestLogger();
const parser = new MultilineParser(logger);
const parse = (message: string) => parser.receivedString(message);
return { parse, getLoggedOutput };
};
describe('MultilineParser', () => {
it('works with single commands', () => {
const parser = getParser()
expect(parser.parse('play')).toMatchInlineSnapshot(`
it('works with single commands', () => {
const parser = getParser();
expect(parser.parse('play')).toMatchInlineSnapshot(`
Array [

@@ -23,9 +23,9 @@ Object {

]
`)
expect(parser.getLoggedOutput()).toEqual([])
})
`);
expect(parser.getLoggedOutput()).toEqual([]);
});
it('works with multiple commands', () => {
const parser = getParser()
expect(parser.parse('play' + CRLF + 'stop' + CRLF + 'play')).toMatchInlineSnapshot(`
it('works with multiple commands', () => {
const parser = getParser();
expect(parser.parse('play' + CRLF + 'stop' + CRLF + 'play')).toMatchInlineSnapshot(`
Array [

@@ -48,9 +48,9 @@ Object {

]
`)
expect(parser.getLoggedOutput()).toEqual([])
})
`);
expect(parser.getLoggedOutput()).toEqual([]);
});
it('does not validate commands with that do not have params', () => {
const parser = getParser()
expect(parser.parse('banana')).toMatchInlineSnapshot(`
it('does not validate commands with that do not have params', () => {
const parser = getParser();
expect(parser.parse('banana')).toMatchInlineSnapshot(`
Array [

@@ -63,20 +63,28 @@ Object {

]
`)
expect(parser.getLoggedOutput()).toEqual([])
})
`);
expect(parser.getLoggedOutput()).toEqual([]);
});
it('validates commands with params', () => {
const parseMe = () =>
getParser().parse(
'notifyyyy: transporttttt: true slottttttttt: true remoteeeeee: true configurationnnn: false'
)
expect(parseMe).toThrowErrorMatchingInlineSnapshot(
`"Command malformed / paramName not recognised: \`true slottttttttt\`"`
)
})
it('throws an error when it receives an invalid command', () => {
expect(() =>
getParser().parse(
'notifyyyy: transporttttt: true slottttttttt: true remoteeeeee: true configurationnnn: false'
)
).toThrowErrorMatchingInlineSnapshot(`"Invalid command: \`notifyyyy\`"`);
});
it('parses valid commands with options', () => {
const parser = getParser()
expect(parser.parse('notify: transport: true slot: true remote: true configuration: false'))
.toMatchInlineSnapshot(`
it('throws an error when it receives a valid command with invalid params', () => {
expect(() =>
getParser().parse(
'notify: transporttttt: true slottttttttt: true remoteeeeee: true configurationnnn: false'
)
).toThrowErrorMatchingInlineSnapshot(
`"Command malformed / paramName not recognised: \`true slottttttttt\`"`
);
});
it('parses valid commands with options', () => {
const parser = getParser();
expect(parser.parse('notify: transport: true slot: true remote: true configuration: false'))
.toMatchInlineSnapshot(`
Array [

@@ -94,5 +102,5 @@ Object {

]
`)
`);
expect(parser.parse('configuration: video input: SDI audio input: XLR')).toMatchInlineSnapshot(`
expect(parser.parse('configuration: video input: SDI audio input: XLR')).toMatchInlineSnapshot(`
Array [

@@ -108,5 +116,5 @@ Object {

]
`)
`);
expect(parser.parse('slot select: slot id: 2 video format: NTSC')).toMatchInlineSnapshot(`
expect(parser.parse('slot select: slot id: 2 video format: NTSC')).toMatchInlineSnapshot(`
Array [

@@ -122,5 +130,5 @@ Object {

]
`)
`);
expect(parser.parse('preview: enable: true')).toMatchInlineSnapshot(`
expect(parser.parse('preview: enable: true')).toMatchInlineSnapshot(`
Array [

@@ -135,5 +143,5 @@ Object {

]
`)
`);
expect(parser.parse('play on startup: single clip: true')).toMatchInlineSnapshot(`
expect(parser.parse('play on startup: single clip: true')).toMatchInlineSnapshot(`
Array [

@@ -148,5 +156,5 @@ Object {

]
`)
`);
expect(parser.parse('clips get: clip id: example clip id')).toMatchInlineSnapshot(`
expect(parser.parse('clips get: clip id: example clip id')).toMatchInlineSnapshot(`
Array [

@@ -161,5 +169,5 @@ Object {

]
`)
`);
expect(parser.parse('playrange set: clip id: 12345')).toMatchInlineSnapshot(`
expect(parser.parse('playrange set: clip id: 12345')).toMatchInlineSnapshot(`
Array [

@@ -174,5 +182,5 @@ Object {

]
`)
`);
expect(parser.parse('shuttle: speed: -1600')).toMatchInlineSnapshot(`
expect(parser.parse('shuttle: speed: -1600')).toMatchInlineSnapshot(`
Array [

@@ -187,6 +195,6 @@ Object {

]
`)
`);
expect(parser.getLoggedOutput()).toEqual([])
})
})
expect(parser.getLoggedOutput()).toEqual([]);
});
});

@@ -1,30 +0,30 @@

import pino from 'pino'
import pino from 'pino';
export const getTestLogger = () => {
const loggedOutput: any[] = []
const loggedOutput: any[] = [];
const logger = pino({
level: 'trace',
name: 'pino-jest',
prettyPrint: true,
prettifier: () => ({
// all the stuff we want to omit
pid,
source,
time,
hostname,
name,
// the remainder
...args
}: Record<string, any>) => {
loggedOutput.push(args)
}
})
const logger = pino({
level: 'trace',
name: 'pino-jest',
prettyPrint: true,
prettifier: () => ({
// all the stuff we want to omit
pid,
source,
time,
hostname,
name,
// the remainder
...args
}: Record<string, any>) => {
loggedOutput.push(args);
},
});
const getLoggedOutput = () => loggedOutput
const getLoggedOutput = () => loggedOutput;
return {
logger,
getLoggedOutput
}
}
return {
logger,
getLoggedOutput,
};
};

@@ -1,33 +0,1 @@

export const CRLF = '\r\n'
export const parametersByCommandName = {
help: [],
commands: [],
'device info': [],
'disk list': ['slot id'],
quit: [],
ping: [],
preview: ['enable'],
play: ['speed', 'loop', 'single clip'],
'playrange set': ['clip id', 'in', 'out'],
'playrange clear': [],
record: ['name'],
stop: [],
'clips count': [],
'clips get': ['clip id', 'count'],
'clips add': ['name'],
'clips clear': [],
'transport info': [],
'slot info': ['slot id'],
'slot select': ['slot id', 'video format'],
notify: ['remote', 'transport', 'slot', 'configuration', 'dropped frames'],
goto: ['clip id', 'clip', 'timeline', 'timecode', 'slot id'],
jog: ['timecode'],
shuttle: ['speed'],
remote: ['enable', 'override'],
configuration: ['video input', 'audio input', 'file format'],
uptime: [],
format: ['prepare', 'confirm'],
identify: ['enable'],
watchdog: ['period']
}
export const CRLF = '\r\n';

@@ -1,19 +0,19 @@

import * as ResponseInterface from './types/ResponseInterface'
import * as ResponseInterface from './types/ResponseInterface';
export const formatClipsGetResponse = (
res: ResponseInterface.ClipsGet
res: ResponseInterface.ClipsGet
): Record<string, string | number> => {
const clipsCount = res.clips.length
const clipsCount = res.clips.length;
const response: Record<string, string | number> = {
clipsCount
}
const response: Record<string, string | number> = {
clipsCount,
};
for (let idx = 0; idx < clipsCount; idx++) {
const clip = res.clips[idx]
const clipKey = (idx + 1).toString()
response[clipKey] = `${clip.name} ${clip.startT} ${clip.duration}`
}
for (let idx = 0; idx < clipsCount; idx++) {
const clip = res.clips[idx];
const clipKey = (idx + 1).toString();
response[clipKey] = `${clip.name} ${clip.startT} ${clip.duration}`;
}
return response
}
return response;
};

@@ -1,12 +0,12 @@

import { HyperDeckSocket } from './HyperDeckSocket'
import type { ReceivedCommandCallback } from './HyperDeckSocket'
import { DeserializedCommand, SynchronousCode, ErrorCode, NotifyType } from './types'
import * as ResponseInterface from './types/ResponseInterface'
import * as DeserializedCommands from './types/DeserializedCommands'
import { formatClipsGetResponse } from './formatClipsGetResponse'
import { createServer, Server } from 'net'
import pino from 'pino'
import { invariant } from './invariant'
import { HyperDeckSocket } from './HyperDeckSocket';
import type { ReceivedCommandCallback } from './HyperDeckSocket';
import { DeserializedCommand, SynchronousCode, ErrorCode, NotifyType } from './types';
import * as ResponseInterface from './types/ResponseInterface';
import * as DeserializedCommands from './types/DeserializedCommands';
import { formatClipsGetResponse } from './formatClipsGetResponse';
import { createServer, Server } from 'net';
import pino from 'pino';
import { invariant } from './invariant';
type Handler<C extends DeserializedCommand, R extends any> = (command: C) => Promise<R>
type Handler<C extends DeserializedCommand, R extends any> = (command: C) => Promise<R>;

@@ -16,239 +16,239 @@ class UnimplementedError extends Error {}

const noop = async () => {
throw new UnimplementedError()
}
throw new UnimplementedError();
};
export class HyperDeckServer {
private logger: pino.Logger
private sockets: { [id: string]: HyperDeckSocket } = {}
private server: Server
private logger: pino.Logger;
private sockets: { [id: string]: HyperDeckSocket } = {};
private server: Server;
onDeviceInfo: Handler<DeserializedCommand, ResponseInterface.DeviceInfo> = noop
onDiskList: Handler<DeserializedCommand, ResponseInterface.DiskList> = noop
onPreview: Handler<DeserializedCommands.PreviewCommand, void> = noop
onPlay: Handler<DeserializedCommands.PlayCommand, void> = noop
onPlayrangeSet: Handler<DeserializedCommands.PlayrangeSetCommand, void> = noop
onPlayrangeClear: Handler<DeserializedCommand, void> = noop
onRecord: Handler<DeserializedCommands.RecordCommand, void> = noop
onStop: Handler<DeserializedCommand, void> = noop
onClipsCount: Handler<DeserializedCommand, ResponseInterface.ClipsCount> = noop
onClipsGet: Handler<DeserializedCommands.ClipsGetCommand, ResponseInterface.ClipsGet> = noop
onClipsAdd: Handler<DeserializedCommands.ClipsAddCommand, void> = noop
onClipsClear: Handler<DeserializedCommand, void> = noop
onTransportInfo: Handler<DeserializedCommand, ResponseInterface.TransportInfo> = noop
onSlotInfo: Handler<DeserializedCommands.SlotInfoCommand, ResponseInterface.SlotInfo> = noop
onSlotSelect: Handler<DeserializedCommands.SlotSelectCommand, void> = noop
onGoTo: Handler<DeserializedCommands.GoToCommand, void> = noop
onJog: Handler<DeserializedCommands.JogCommand, void> = noop
onShuttle: Handler<DeserializedCommands.ShuttleCommand, void> = noop
onConfiguration: Handler<
DeserializedCommands.ConfigurationCommand,
ResponseInterface.Configuration
> = noop
onUptime: Handler<DeserializedCommand, ResponseInterface.Uptime> = noop
onFormat: Handler<DeserializedCommands.FormatCommand, ResponseInterface.Format> = noop
onIdentify: Handler<DeserializedCommands.IdentifyCommand, void> = noop
onWatchdog: Handler<DeserializedCommands.WatchdogCommand, void> = noop
onDeviceInfo: Handler<DeserializedCommand, ResponseInterface.DeviceInfo> = noop;
onDiskList: Handler<DeserializedCommand, ResponseInterface.DiskList> = noop;
onPreview: Handler<DeserializedCommands.PreviewCommand, void> = noop;
onPlay: Handler<DeserializedCommands.PlayCommand, void> = noop;
onPlayrangeSet: Handler<DeserializedCommands.PlayrangeSetCommand, void> = noop;
onPlayrangeClear: Handler<DeserializedCommand, void> = noop;
onRecord: Handler<DeserializedCommands.RecordCommand, void> = noop;
onStop: Handler<DeserializedCommand, void> = noop;
onClipsCount: Handler<DeserializedCommand, ResponseInterface.ClipsCount> = noop;
onClipsGet: Handler<DeserializedCommands.ClipsGetCommand, ResponseInterface.ClipsGet> = noop;
onClipsAdd: Handler<DeserializedCommands.ClipsAddCommand, void> = noop;
onClipsClear: Handler<DeserializedCommand, void> = noop;
onTransportInfo: Handler<DeserializedCommand, ResponseInterface.TransportInfo> = noop;
onSlotInfo: Handler<DeserializedCommands.SlotInfoCommand, ResponseInterface.SlotInfo> = noop;
onSlotSelect: Handler<DeserializedCommands.SlotSelectCommand, void> = noop;
onGoTo: Handler<DeserializedCommands.GoToCommand, void> = noop;
onJog: Handler<DeserializedCommands.JogCommand, void> = noop;
onShuttle: Handler<DeserializedCommands.ShuttleCommand, void> = noop;
onConfiguration: Handler<
DeserializedCommands.ConfigurationCommand,
ResponseInterface.Configuration
> = noop;
onUptime: Handler<DeserializedCommand, ResponseInterface.Uptime> = noop;
onFormat: Handler<DeserializedCommands.FormatCommand, ResponseInterface.Format> = noop;
onIdentify: Handler<DeserializedCommands.IdentifyCommand, void> = noop;
onWatchdog: Handler<DeserializedCommands.WatchdogCommand, void> = noop;
constructor(ip?: string, logger = pino()) {
this.logger = logger.child({ name: 'HyperDeck Emulator' })
constructor(ip?: string, logger = pino()) {
this.logger = logger.child({ name: 'HyperDeck Emulator' });
this.server = createServer((socket) => {
this.logger.info('connection')
const socketId = Math.random().toString(35).substr(-6)
this.server = createServer((socket) => {
this.logger.info('connection');
const socketId = Math.random().toString(35).substr(-6);
const socketLogger = this.logger.child({ name: 'HyperDeck socket ' + socketId })
const socketLogger = this.logger.child({ name: 'HyperDeck socket ' + socketId });
this.sockets[socketId] = new HyperDeckSocket(socket, socketLogger, (cmd) =>
this.receivedCommand(cmd)
)
this.sockets[socketId] = new HyperDeckSocket(socket, socketLogger, (cmd) =>
this.receivedCommand(cmd)
);
this.sockets[socketId].on('disconnected', () => {
socketLogger.info('disconnected')
delete this.sockets[socketId]
})
})
this.sockets[socketId].on('disconnected', () => {
socketLogger.info('disconnected');
delete this.sockets[socketId];
});
});
this.server.on('listening', () => this.logger.info('listening'))
this.server.on('close', () => this.logger.info('connection closed'))
this.server.on('error', (err) => this.logger.error('server error:', err))
this.server.maxConnections = 1
this.server.listen(9993, ip)
}
this.server.on('listening', () => this.logger.info('listening'));
this.server.on('close', () => this.logger.info('connection closed'));
this.server.on('error', (err) => this.logger.error('server error:', err));
this.server.maxConnections = 1;
this.server.listen(9993, ip);
}
close(): void {
this.server.unref()
}
close(): void {
this.server.unref();
}
notifySlot(params: Record<string, string>): void {
this.notify('slot', params)
}
notifySlot(params: Record<string, string>): void {
this.notify('slot', params);
}
notifyTransport(params: Record<string, string>): void {
this.notify('transport', params)
}
notifyTransport(params: Record<string, string>): void {
this.notify('transport', params);
}
private notify(type: NotifyType, params: Record<string, string>): void {
for (const id of Object.keys(this.sockets)) {
this.sockets[id].notify(type, params)
}
}
private notify(type: NotifyType, params: Record<string, string>): void {
for (const id of Object.keys(this.sockets)) {
this.sockets[id].notify(type, params);
}
}
private receivedCommand: ReceivedCommandCallback = async (cmd) => {
// TODO(meyer) more sophisticated debouncing
await new Promise((resolve) => setTimeout(() => resolve(), 200))
private receivedCommand: ReceivedCommandCallback = async (cmd) => {
// TODO(meyer) more sophisticated debouncing
await new Promise((resolve) => setTimeout(() => resolve(), 200));
this.logger.info({ cmd }, '<-- ' + cmd.name)
try {
if (cmd.name === 'device info') {
const res = await this.onDeviceInfo(cmd)
return { code: SynchronousCode.DeviceInfo, params: res }
}
this.logger.info({ cmd }, '<-- ' + cmd.name);
try {
if (cmd.name === 'device info') {
const res = await this.onDeviceInfo(cmd);
return { code: SynchronousCode.DeviceInfo, params: res };
}
if (cmd.name === 'disk list') {
const res = await this.onDiskList(cmd)
return { code: SynchronousCode.DiskList, params: res }
}
if (cmd.name === 'disk list') {
const res = await this.onDiskList(cmd);
return { code: SynchronousCode.DiskList, params: res };
}
if (cmd.name === 'preview') {
await this.onPreview(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'preview') {
await this.onPreview(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'play') {
await this.onPlay(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'play') {
await this.onPlay(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'playrange set') {
await this.onPlayrangeSet(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'playrange set') {
await this.onPlayrangeSet(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'playrange clear') {
await this.onPlayrangeClear(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'playrange clear') {
await this.onPlayrangeClear(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'record') {
await this.onRecord(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'record') {
await this.onRecord(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'stop') {
await this.onStop(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'stop') {
await this.onStop(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'clips count') {
const res = await this.onClipsCount(cmd)
return { code: SynchronousCode.ClipsCount, params: res }
}
if (cmd.name === 'clips count') {
const res = await this.onClipsCount(cmd);
return { code: SynchronousCode.ClipsCount, params: res };
}
if (cmd.name === 'clips get') {
const res = await this.onClipsGet(cmd).then(formatClipsGetResponse)
return { code: SynchronousCode.ClipsInfo, params: res }
}
if (cmd.name === 'clips get') {
const res = await this.onClipsGet(cmd).then(formatClipsGetResponse);
return { code: SynchronousCode.ClipsInfo, params: res };
}
if (cmd.name === 'clips add') {
await this.onClipsAdd(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'clips add') {
await this.onClipsAdd(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'clips clear') {
await this.onClipsClear(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'clips clear') {
await this.onClipsClear(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'transport info') {
const res = await this.onTransportInfo(cmd)
return { code: SynchronousCode.TransportInfo, params: res }
}
if (cmd.name === 'transport info') {
const res = await this.onTransportInfo(cmd);
return { code: SynchronousCode.TransportInfo, params: res };
}
if (cmd.name === 'slot info') {
const res = await this.onSlotInfo(cmd)
return { code: SynchronousCode.SlotInfo, params: res }
}
if (cmd.name === 'slot info') {
const res = await this.onSlotInfo(cmd);
return { code: SynchronousCode.SlotInfo, params: res };
}
if (cmd.name === 'slot select') {
await this.onSlotSelect(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'slot select') {
await this.onSlotSelect(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'notify') {
// implemented in socket.ts
return SynchronousCode.OK
}
if (cmd.name === 'notify') {
// implemented in socket.ts
return SynchronousCode.OK;
}
if (cmd.name === 'go to') {
await this.onGoTo(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'go to') {
await this.onGoTo(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'jog') {
await this.onJog(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'jog') {
await this.onJog(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'shuttle') {
await this.onShuttle(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'shuttle') {
await this.onShuttle(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'remote') {
return {
code: SynchronousCode.Remote,
params: {
enabled: true,
override: false
}
}
}
if (cmd.name === 'remote') {
return {
code: SynchronousCode.Remote,
params: {
enabled: true,
override: false,
},
};
}
if (cmd.name === 'configuration') {
const res = await this.onConfiguration(cmd)
if (res) {
return { code: SynchronousCode.Configuration, params: res }
}
return SynchronousCode.OK
}
if (cmd.name === 'configuration') {
const res = await this.onConfiguration(cmd);
if (res) {
return { code: SynchronousCode.Configuration, params: res };
}
return SynchronousCode.OK;
}
if (cmd.name === 'uptime') {
const res = await this.onUptime(cmd)
return { code: SynchronousCode.Uptime, params: res }
}
if (cmd.name === 'uptime') {
const res = await this.onUptime(cmd);
return { code: SynchronousCode.Uptime, params: res };
}
if (cmd.name === 'format') {
const res = await this.onFormat(cmd)
if (res) {
return { code: SynchronousCode.FormatReady, params: res }
}
return SynchronousCode.OK
}
if (cmd.name === 'format') {
const res = await this.onFormat(cmd);
if (res) {
return { code: SynchronousCode.FormatReady, params: res };
}
return SynchronousCode.OK;
}
if (cmd.name === 'identify') {
await this.onIdentify(cmd)
return SynchronousCode.OK
}
if (cmd.name === 'identify') {
await this.onIdentify(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'watchdog') {
// implemented in socket.ts
return SynchronousCode.OK
}
if (cmd.name === 'watchdog') {
// implemented in socket.ts
return SynchronousCode.OK;
}
if (cmd.name === 'ping') {
// implemented in socket.ts
return SynchronousCode.OK
}
if (cmd.name === 'ping') {
// implemented in socket.ts
return SynchronousCode.OK;
}
invariant(false, 'Unhandled command name: `%s`', cmd.name)
} catch (err) {
if (err instanceof UnimplementedError) {
this.logger.error({ cmd }, 'unimplemented')
return ErrorCode.Unsupported
}
invariant(false, 'Unhandled command name: `%s`', cmd.name);
} catch (err) {
if (err instanceof UnimplementedError) {
this.logger.error({ cmd }, 'unimplemented');
return ErrorCode.Unsupported;
}
this.logger.error({ cmd, err: err.message }, 'unhandled command name')
return ErrorCode.InternalError
}
}
this.logger.error({ cmd, err: err.message }, 'unhandled command name');
return ErrorCode.InternalError;
}
};
}

@@ -1,178 +0,178 @@

import type { Socket } from 'net'
import { EventEmitter } from 'events'
import type { Socket } from 'net';
import { EventEmitter } from 'events';
import {
AsynchronousCode,
DeserializedCommand,
ErrorCode,
NotifyType,
SynchronousCode,
ResponseCode
} from './types'
import * as DeserializedCommands from './types/DeserializedCommands'
import { MultilineParser } from './MultilineParser'
import type { Logger } from 'pino'
import { messageForCode } from './messageForCode'
AsynchronousCode,
DeserializedCommand,
ErrorCode,
NotifyType,
SynchronousCode,
ResponseCode,
} from './types';
import * as DeserializedCommands from './types/DeserializedCommands';
import { MultilineParser } from './MultilineParser';
import type { Logger } from 'pino';
import { messageForCode } from './messageForCode';
interface ResponseWithMessage {
code: ErrorCode
message: string
code: ErrorCode;
message: string;
}
interface ResponseWithParams {
code: ResponseCode
params?: Record<string, any>
code: ResponseCode;
params?: Record<string, any>;
}
export type ReceivedCommandCallback = (
cmd: DeserializedCommand
) => Promise<ResponseCode | ResponseWithParams | ResponseWithMessage>
cmd: DeserializedCommand
) => Promise<ResponseCode | ResponseWithParams | ResponseWithMessage>;
export class HyperDeckSocket extends EventEmitter {
constructor(
private socket: Socket,
private logger: Logger,
private receivedCommand: ReceivedCommandCallback
) {
super()
constructor(
private socket: Socket,
private logger: Logger,
private receivedCommand: ReceivedCommandCallback
) {
super();
this.parser = new MultilineParser(logger)
this.parser = new MultilineParser(logger);
this.socket.setEncoding('utf-8')
this.socket.setEncoding('utf-8');
this.socket.on('data', (data: string) => {
this.onMessage(data)
})
this.socket.on('data', (data: string) => {
this.onMessage(data);
});
this.socket.on('error', (err) => {
logger.info({ err }, 'error')
this.socket.destroy()
this.emit('disconnected')
logger.info('manually disconnected')
})
this.socket.on('error', (err) => {
logger.info({ err }, 'error');
this.socket.destroy();
this.emit('disconnected');
logger.info('manually disconnected');
});
this.sendResponse(AsynchronousCode.ConnectionInfo, {
'protocol version': '1.11',
model: 'NodeJS HyperDeck Server Library'
})
}
this.sendResponse(AsynchronousCode.ConnectionInfo, {
'protocol version': '1.11',
model: 'NodeJS HyperDeck Server Library',
});
}
private parser: MultilineParser
private lastReceivedMS = -1
private watchdogTimer: NodeJS.Timer | null = null
private parser: MultilineParser;
private lastReceivedMS = -1;
private watchdogTimer: NodeJS.Timer | null = null;
private notifySettings = {
slot: false,
transport: false,
remote: false,
configuration: false,
'dropped frames': false
}
private notifySettings = {
slot: false,
transport: false,
remote: false,
configuration: false,
'dropped frames': false,
};
private onMessage(data: string): void {
this.logger.info({ data }, '<-- received message from client')
private onMessage(data: string): void {
this.logger.info({ data }, '<-- received message from client');
this.lastReceivedMS = Date.now()
this.lastReceivedMS = Date.now();
const cmds = this.parser.receivedString(data)
this.logger.info({ cmds }, 'parsed commands')
const cmds = this.parser.receivedString(data);
this.logger.info({ cmds }, 'parsed commands');
for (const cmd of cmds) {
// special cases
if (cmd.name === 'watchdog') {
if (this.watchdogTimer) clearInterval(this.watchdogTimer)
for (const cmd of cmds) {
// special cases
if (cmd.name === 'watchdog') {
if (this.watchdogTimer) clearInterval(this.watchdogTimer);
const watchdogCmd = cmd as DeserializedCommands.WatchdogCommand
if (watchdogCmd.parameters.period) {
this.watchdogTimer = setInterval(() => {
if (Date.now() - this.lastReceivedMS > Number(watchdogCmd.parameters.period)) {
this.socket.destroy()
this.emit('disconnected')
if (this.watchdogTimer) {
clearInterval(this.watchdogTimer)
}
}
}, Number(watchdogCmd.parameters.period) * 1000)
}
} else if (cmd.name === 'notify') {
const notifyCmd = cmd as DeserializedCommands.NotifyCommand
const watchdogCmd = cmd as DeserializedCommands.WatchdogCommand;
if (watchdogCmd.parameters.period) {
this.watchdogTimer = setInterval(() => {
if (Date.now() - this.lastReceivedMS > Number(watchdogCmd.parameters.period)) {
this.socket.destroy();
this.emit('disconnected');
if (this.watchdogTimer) {
clearInterval(this.watchdogTimer);
}
}
}, Number(watchdogCmd.parameters.period) * 1000);
}
} else if (cmd.name === 'notify') {
const notifyCmd = cmd as DeserializedCommands.NotifyCommand;
if (Object.keys(notifyCmd.parameters).length > 0) {
for (const param of Object.keys(notifyCmd.parameters) as Array<
keyof typeof notifyCmd.parameters
>) {
if (this.notifySettings[param] !== undefined) {
this.notifySettings[param] = notifyCmd.parameters[param] === 'true'
}
}
} else {
const settings: Record<string, string> = {}
for (const key of Object.keys(this.notifySettings) as Array<
keyof HyperDeckSocket['notifySettings']
>) {
settings[key] = this.notifySettings[key] ? 'true' : 'false'
}
this.sendResponse(SynchronousCode.Notify, settings, cmd)
if (Object.keys(notifyCmd.parameters).length > 0) {
for (const param of Object.keys(notifyCmd.parameters) as Array<
keyof typeof notifyCmd.parameters
>) {
if (this.notifySettings[param] !== undefined) {
this.notifySettings[param] = notifyCmd.parameters[param] === 'true';
}
}
} else {
const settings: Record<string, string> = {};
for (const key of Object.keys(this.notifySettings) as Array<
keyof HyperDeckSocket['notifySettings']
>) {
settings[key] = this.notifySettings[key] ? 'true' : 'false';
}
this.sendResponse(SynchronousCode.Notify, settings, cmd);
continue
}
}
continue;
}
}
this.receivedCommand(cmd).then(
(codeOrObj) => {
if (typeof codeOrObj === 'object') {
const code = codeOrObj.code
const paramsOrMessage =
('params' in codeOrObj && codeOrObj.params) ||
('message' in codeOrObj && codeOrObj.message) ||
undefined
return this.sendResponse(code, paramsOrMessage, cmd)
}
this.receivedCommand(cmd).then(
(codeOrObj) => {
if (typeof codeOrObj === 'object') {
const code = codeOrObj.code;
const paramsOrMessage =
('params' in codeOrObj && codeOrObj.params) ||
('message' in codeOrObj && codeOrObj.message) ||
undefined;
return this.sendResponse(code, paramsOrMessage, cmd);
}
const code = codeOrObj
const code = codeOrObj;
if (
typeof code === 'number' &&
(ErrorCode[code] || SynchronousCode[code] || AsynchronousCode[code])
) {
return this.sendResponse(code, undefined, cmd)
}
if (
typeof code === 'number' &&
(ErrorCode[code] || SynchronousCode[code] || AsynchronousCode[code])
) {
return this.sendResponse(code, undefined, cmd);
}
this.logger.error(
{ cmd, codeOrObj },
'codeOrObj was neither a ResponseCode nor a response object'
)
this.sendResponse(ErrorCode.InternalError, undefined, cmd)
},
// not implemented by client code:
() => this.sendResponse(ErrorCode.Unsupported, undefined, cmd)
)
}
}
this.logger.error(
{ cmd, codeOrObj },
'codeOrObj was neither a ResponseCode nor a response object'
);
this.sendResponse(ErrorCode.InternalError, undefined, cmd);
},
// not implemented by client code:
() => this.sendResponse(ErrorCode.Unsupported, undefined, cmd)
);
}
}
sendResponse(
code: ResponseCode,
paramsOrMessage?: Record<string, unknown> | string,
cmd?: DeserializedCommand
): void {
const responseText = messageForCode(code, paramsOrMessage)
const method = ErrorCode[code] ? 'error' : 'info'
this.logger[method]({ responseText, cmd }, '--> send response to client')
this.socket.write(responseText)
}
sendResponse(
code: ResponseCode,
paramsOrMessage?: Record<string, unknown> | string,
cmd?: DeserializedCommand
): void {
const responseText = messageForCode(code, paramsOrMessage);
const method = ErrorCode[code] ? 'error' : 'info';
this.logger[method]({ responseText, cmd }, '--> send response to client');
this.socket.write(responseText);
}
notify(type: NotifyType, params: Record<string, string>): void {
this.logger.info({ type, params }, 'notify')
notify(type: NotifyType, params: Record<string, string>): void {
this.logger.info({ type, params }, 'notify');
if (type === 'configuration' && this.notifySettings.configuration) {
this.sendResponse(AsynchronousCode.ConfigurationInfo, params)
} else if (type === 'remote' && this.notifySettings.remote) {
this.sendResponse(AsynchronousCode.RemoteInfo, params)
} else if (type === 'slot' && this.notifySettings.slot) {
this.sendResponse(AsynchronousCode.SlotInfo, params)
} else if (type === 'transport' && this.notifySettings.transport) {
this.sendResponse(AsynchronousCode.TransportInfo, params)
} else {
this.logger.error({ type, params }, 'unhandled notify type')
}
}
if (type === 'configuration' && this.notifySettings.configuration) {
this.sendResponse(AsynchronousCode.ConfigurationInfo, params);
} else if (type === 'remote' && this.notifySettings.remote) {
this.sendResponse(AsynchronousCode.RemoteInfo, params);
} else if (type === 'slot' && this.notifySettings.slot) {
this.sendResponse(AsynchronousCode.SlotInfo, params);
} else if (type === 'transport' && this.notifySettings.transport) {
this.sendResponse(AsynchronousCode.TransportInfo, params);
} else {
this.logger.error({ type, params }, 'unhandled notify type');
}
}
}

@@ -1,3 +0,3 @@

export * from './HyperDeckServer'
export * from './Timecode'
export * as ResponseInterface from './types/ResponseInterface'
export * from './HyperDeckServer';
export * from './Timecode';
export * as ResponseInterface from './types/ResponseInterface';

@@ -1,7 +0,7 @@

import util from 'util'
import util from 'util';
export function invariant(condition: any, message: string, ...args: any[]): asserts condition {
if (!condition) {
throw new Error(util.format(message, ...args))
}
if (!condition) {
throw new Error(util.format(message, ...args));
}
}

@@ -1,55 +0,55 @@

import { CRLF } from './constants'
import { ResponseCode, responseNamesByCode } from './types'
import { invariant } from './invariant'
import { CRLF } from './constants';
import { ResponseCode, responseNamesByCode } from './types';
import { invariant } from './invariant';
// escape CR/LF and remove colons
const sanitiseMessage = (input: string): string => {
return input.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/:/g, '')
}
return input.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/:/g, '');
};
/** For a given code, generate the response message that will be sent to the ATEM */
export const messageForCode = (
code: ResponseCode,
params?: Record<string, unknown> | string
code: ResponseCode,
params?: Record<string, unknown> | string
): string => {
if (typeof params === 'string') {
return code + ' ' + sanitiseMessage(params) + CRLF
}
if (typeof params === 'string') {
return code + ' ' + sanitiseMessage(params) + CRLF;
}
const firstLine = `${code} ${responseNamesByCode[code]}`
const firstLine = `${code} ${responseNamesByCode[code]}`;
// bail if no params
if (!params) {
return firstLine + CRLF
}
// bail if no params
if (!params) {
return firstLine + CRLF;
}
// filter out params with null/undefined values
const paramEntries = Object.entries(params).filter(([, value]) => value != null)
// filter out params with null/undefined values
const paramEntries = Object.entries(params).filter(([, value]) => value != null);
// bail if no params after filtering
if (paramEntries.length === 0) {
return firstLine + CRLF
}
// bail if no params after filtering
if (paramEntries.length === 0) {
return firstLine + CRLF;
}
// turn the params object into a key/value
return (
paramEntries.reduce<string>((prev, [key, value]) => {
let valueString: string
// turn the params object into a key/value
return (
paramEntries.reduce<string>((prev, [key, value]) => {
let valueString: string;
if (typeof value === 'string') {
valueString = value
} else if (typeof value === 'boolean') {
valueString = value ? 'true' : 'false'
} else if (typeof value === 'number') {
valueString = value.toString()
} else {
invariant(false, 'Unhandled value type: `%s`', typeof value)
}
if (typeof value === 'string') {
valueString = value;
} else if (typeof value === 'boolean') {
valueString = value ? 'true' : 'false';
} else if (typeof value === 'number') {
valueString = value.toString();
} else {
invariant(false, 'Unhandled value type: `%s`', typeof value);
}
// convert camelCase keys to space-separated words
const formattedKey = key.replace(/([a-z])([A-Z]+)/, '$1 $2').toLowerCase()
// convert camelCase keys to space-separated words
const formattedKey = key.replace(/([a-z])([A-Z]+)/g, '$1 $2').toLowerCase();
return prev + formattedKey + ': ' + valueString + CRLF
}, firstLine + ':' + CRLF) + CRLF
)
}
return prev + formattedKey + ': ' + valueString + CRLF;
}, firstLine + ':' + CRLF) + CRLF
);
};

@@ -1,128 +0,130 @@

import type { DeserializedCommand } from './types'
import { parametersByCommandName, CRLF } from './constants'
import type { Logger } from 'pino'
import { invariant } from './invariant'
import type { DeserializedCommand } from './types';
import { CRLF } from './constants';
import type { Logger } from 'pino';
import { invariant } from './invariant';
import { paramsByKey } from './api';
export class MultilineParser {
private logger: Logger
private linesQueue: string[] = []
private logger: Logger;
private linesQueue: string[] = [];
constructor(logger: Logger) {
this.logger = logger.child({ name: 'MultilineParser' })
}
constructor(logger: Logger) {
this.logger = logger.child({ name: 'MultilineParser' });
}
public receivedString(data: string): DeserializedCommand[] {
const res: DeserializedCommand[] = []
public receivedString(data: string): DeserializedCommand[] {
const res: DeserializedCommand[] = [];
// add new lines to processing queue
const newLines = data.split(CRLF)
// add new lines to processing queue
const newLines = data.split(CRLF);
// remove the blank line at the end from the intentionally trailing \r\n
if (newLines.length > 0 && newLines[newLines.length - 1] === '') newLines.pop()
// remove the blank line at the end from the intentionally trailing \r\n
if (newLines.length > 0 && newLines[newLines.length - 1] === '') newLines.pop();
this.linesQueue = this.linesQueue.concat(newLines)
this.linesQueue = this.linesQueue.concat(newLines);
while (this.linesQueue.length > 0) {
// skip any blank lines
if (this.linesQueue[0] === '') {
this.linesQueue.shift()
continue
}
while (this.linesQueue.length > 0) {
// skip any blank lines
if (this.linesQueue[0] === '') {
this.linesQueue.shift();
continue;
}
// if the first line has no colon, then it is a single line command
if (
!this.linesQueue[0].includes(':') ||
(this.linesQueue.length === 1 && this.linesQueue[0].includes(':'))
) {
const parsedResponse = this.parseResponse(this.linesQueue.splice(0, 1))
if (parsedResponse) {
res.push(parsedResponse)
}
continue
}
// if the first line has no colon, then it is a single line command
if (
!this.linesQueue[0].includes(':') ||
(this.linesQueue.length === 1 && this.linesQueue[0].includes(':'))
) {
const parsedResponse = this.parseResponse(this.linesQueue.splice(0, 1));
if (parsedResponse) {
res.push(parsedResponse);
}
continue;
}
const endLine = this.linesQueue.indexOf('')
if (endLine === -1) {
// Not got full response yet
break
}
const endLine = this.linesQueue.indexOf('');
if (endLine === -1) {
// Not got full response yet
break;
}
const lines = this.linesQueue.splice(0, endLine + 1)
const r = this.parseResponse(lines)
if (r) {
res.push(r)
}
}
const lines = this.linesQueue.splice(0, endLine + 1);
const r = this.parseResponse(lines);
if (r) {
res.push(r);
}
}
return res
}
return res;
}
private parseResponse(responseLines: string[]): DeserializedCommand | null {
const lines = responseLines.map((l) => l.trim())
private parseResponse(responseLines: string[]): DeserializedCommand | null {
const lines = responseLines.map((l) => l.trim());
if (lines.length === 1 && lines[0].includes(':')) {
const bits = lines[0].split(': ')
if (lines.length === 1 && lines[0].includes(':')) {
const bits = lines[0].split(': ');
const msg = bits.shift() as keyof typeof parametersByCommandName
invariant(msg, 'Unrecognised command')
const msg = bits.shift() as keyof typeof paramsByKey;
invariant(msg, 'Unrecognised command');
invariant(paramsByKey.hasOwnProperty(msg), 'Invalid command: `%s`', msg);
const params: Record<string, string> = {}
const paramNames = new Set(parametersByCommandName[msg])
let param = bits.shift()
invariant(param, 'No named parameters found')
const params: Record<string, string> = {};
const paramNames = paramsByKey[msg];
let param = bits.shift();
invariant(param, 'No named parameters found');
for (let i = 0; i < bits.length - 1; i++) {
const bit = bits[i]
const bobs = bit.split(' ')
for (let i = 0; i < bits.length - 1; i++) {
const bit = bits[i];
const bobs = bit.split(' ');
let nextParam = ''
for (let i = bobs.length - 1; i >= 0; i--) {
nextParam = (bobs.pop() + ' ' + nextParam).trim()
if (paramNames.has(nextParam)) {
break
}
}
let nextParam = '';
for (let i = bobs.length - 1; i >= 0; i--) {
nextParam = (bobs.pop() + ' ' + nextParam).trim();
if (paramNames.has(nextParam)) {
break;
}
}
invariant(bobs.length > 0, 'Command malformed / paramName not recognised: `%s`', bit)
invariant(bobs.length > 0, 'Command malformed / paramName not recognised: `%s`', bit);
params[param] = bobs.join(' ')
param = nextParam
}
params[param] = bobs.join(' ');
param = nextParam;
}
params[param] = bits[bits.length - 1]
params[param] = bits[bits.length - 1];
return {
raw: lines.join(CRLF),
name: msg,
parameters: params
}
} else {
const headerMatch = lines[0].match(/(.+?)(:|)$/im)
if (!headerMatch) {
this.logger.error({ header: lines[0] }, 'failed to parse header')
return null
}
return {
raw: lines.join(CRLF),
name: msg,
parameters: params,
};
} else {
const headerMatch = lines[0].match(/(.+?)(:|)$/im);
if (!headerMatch) {
this.logger.error({ header: lines[0] }, 'failed to parse header');
return null;
}
const msg = headerMatch[1]
const msg = headerMatch[1];
const params: Record<string, string> = {}
const params: Record<string, string> = {};
for (let i = 1; i < lines.length; i++) {
const lineMatch = lines[i].match(/^(.*?): (.*)$/im)
if (!lineMatch) {
this.logger.error({ line: lines[i] }, 'failed to parse line')
continue
}
for (let i = 1; i < lines.length; i++) {
const lineMatch = lines[i].match(/^(.*?): (.*)$/im);
if (!lineMatch) {
this.logger.error({ line: lines[i] }, 'failed to parse line');
continue;
}
params[lineMatch[1]] = lineMatch[2]
}
params[lineMatch[1]] = lineMatch[2];
}
const res: DeserializedCommand = {
raw: lines.join(CRLF),
name: msg,
parameters: params
}
return res
}
}
const res: DeserializedCommand = {
raw: lines.join(CRLF),
name: msg,
parameters: params,
};
return res;
}
}
}

@@ -1,22 +0,22 @@

import { invariant } from './invariant'
import { invariant } from './invariant';
export class Timecode {
constructor(hh: number, mm: number, ss: number, ff: number) {
const timecode = [hh, mm, ss, ff]
.map((code) => {
const codeInt = Math.floor(code)
invariant(
codeInt === code && code >= 0 && code <= 99,
'Timecode params must be an integer between 0 and 99'
)
constructor(hh: number, mm: number, ss: number, ff: number) {
const timecode = [hh, mm, ss, ff]
.map((code) => {
const codeInt = Math.floor(code);
invariant(
codeInt === code && code >= 0 && code <= 99,
'Timecode params must be an integer between 0 and 99'
);
// turn the integer into a potentially zero-prefixed string
return (codeInt + 100).toString().slice(-2)
})
.join(':')
// turn the integer into a potentially zero-prefixed string
return (codeInt + 100).toString().slice(-2);
})
.join(':');
this.toString = () => timecode
}
this.toString = () => timecode;
}
public toString: () => string
public toString: () => string;
}
export interface NotificationConfig {
transport: boolean
remote: boolean
slot: boolean
configuration: boolean
transport: boolean;
remote: boolean;
slot: boolean;
configuration: boolean;
}
export interface DeserializedCommand {
raw: string
name: string
parameters: Record<string, string | undefined>
raw: string;
name: string;
parameters: Record<string, string | undefined>;
}
export type ResponseCode = ErrorCode | SynchronousCode | AsynchronousCode
export type ResponseCode = ErrorCode | SynchronousCode | AsynchronousCode;
export enum ErrorCode {
SyntaxError = 100,
UnsupportedParameter = 101,
InvalidValue = 102,
Unsupported = 103,
DiskFull = 104,
NoDisk = 105,
DiskError = 106,
TimelineEmpty = 107,
InternalError = 108,
OutOfRange = 109,
NoInput = 110,
RemoteControlDisabled = 111,
ConnectionRejected = 120,
InvalidState = 150,
InvalidCodec = 151,
InvalidFormat = 160,
InvalidToken = 161,
FormatNotPrepared = 162
SyntaxError = 100,
UnsupportedParameter = 101,
InvalidValue = 102,
Unsupported = 103,
DiskFull = 104,
NoDisk = 105,
DiskError = 106,
TimelineEmpty = 107,
InternalError = 108,
OutOfRange = 109,
NoInput = 110,
RemoteControlDisabled = 111,
ConnectionRejected = 120,
InvalidState = 150,
InvalidCodec = 151,
InvalidFormat = 160,
InvalidToken = 161,
FormatNotPrepared = 162,
}
export enum SynchronousCode {
OK = 200,
SlotInfo = 202,
DeviceInfo = 204,
ClipsInfo = 205,
DiskList = 206,
TransportInfo = 208,
Notify = 209,
Remote = 210,
Configuration = 211,
ClipsCount = 214,
Uptime = 215,
FormatReady = 216
OK = 200,
SlotInfo = 202,
DeviceInfo = 204,
ClipsInfo = 205,
DiskList = 206,
TransportInfo = 208,
Notify = 209,
Remote = 210,
Configuration = 211,
ClipsCount = 214,
Uptime = 215,
FormatReady = 216,
}
export enum AsynchronousCode {
ConnectionInfo = 500,
SlotInfo = 502,
TransportInfo = 508,
RemoteInfo = 510,
ConfigurationInfo = 511
ConnectionInfo = 500,
SlotInfo = 502,
TransportInfo = 508,
RemoteInfo = 510,
ConfigurationInfo = 511,
}
export type NotifyType = 'slot' | 'transport' | 'remote' | 'configuration'
export type NotifyType = 'slot' | 'transport' | 'remote' | 'configuration';
export const responseNamesByCode: Record<ResponseCode, string> = {
[AsynchronousCode.ConfigurationInfo]: 'configuration info',
[AsynchronousCode.ConnectionInfo]: 'connection info',
[AsynchronousCode.RemoteInfo]: 'remote info',
[AsynchronousCode.SlotInfo]: 'slot info',
[AsynchronousCode.TransportInfo]: 'transport info',
[ErrorCode.ConnectionRejected]: 'connection rejected',
[ErrorCode.DiskError]: 'disk error',
[ErrorCode.DiskFull]: 'disk full',
[ErrorCode.FormatNotPrepared]: 'format not prepared',
[ErrorCode.InternalError]: 'internal error',
[ErrorCode.InvalidCodec]: 'invalid codec',
[ErrorCode.InvalidFormat]: 'invalid format',
[ErrorCode.InvalidState]: 'invalid state',
[ErrorCode.InvalidToken]: 'invalid token',
[ErrorCode.InvalidValue]: 'invalid value',
[ErrorCode.NoDisk]: 'no disk',
[ErrorCode.NoInput]: 'no input',
[ErrorCode.OutOfRange]: 'out of range',
[ErrorCode.RemoteControlDisabled]: 'remote control disabled',
[ErrorCode.SyntaxError]: 'syntax error',
[ErrorCode.TimelineEmpty]: 'timeline empty',
[ErrorCode.Unsupported]: 'unsupported',
[ErrorCode.UnsupportedParameter]: 'unsupported parameter',
[SynchronousCode.ClipsCount]: 'clips count',
[SynchronousCode.ClipsInfo]: 'clips info',
[SynchronousCode.Configuration]: 'configuration',
[SynchronousCode.DeviceInfo]: 'device info',
[SynchronousCode.DiskList]: 'disk list',
[SynchronousCode.FormatReady]: 'format ready',
[SynchronousCode.Notify]: 'notify',
[SynchronousCode.OK]: 'ok',
[SynchronousCode.Remote]: 'remote',
[SynchronousCode.SlotInfo]: 'slot info',
[SynchronousCode.TransportInfo]: 'transport info',
[SynchronousCode.Uptime]: 'uptime'
}
[AsynchronousCode.ConfigurationInfo]: 'configuration info',
[AsynchronousCode.ConnectionInfo]: 'connection info',
[AsynchronousCode.RemoteInfo]: 'remote info',
[AsynchronousCode.SlotInfo]: 'slot info',
[AsynchronousCode.TransportInfo]: 'transport info',
[ErrorCode.ConnectionRejected]: 'connection rejected',
[ErrorCode.DiskError]: 'disk error',
[ErrorCode.DiskFull]: 'disk full',
[ErrorCode.FormatNotPrepared]: 'format not prepared',
[ErrorCode.InternalError]: 'internal error',
[ErrorCode.InvalidCodec]: 'invalid codec',
[ErrorCode.InvalidFormat]: 'invalid format',
[ErrorCode.InvalidState]: 'invalid state',
[ErrorCode.InvalidToken]: 'invalid token',
[ErrorCode.InvalidValue]: 'invalid value',
[ErrorCode.NoDisk]: 'no disk',
[ErrorCode.NoInput]: 'no input',
[ErrorCode.OutOfRange]: 'out of range',
[ErrorCode.RemoteControlDisabled]: 'remote control disabled',
[ErrorCode.SyntaxError]: 'syntax error',
[ErrorCode.TimelineEmpty]: 'timeline empty',
[ErrorCode.Unsupported]: 'unsupported',
[ErrorCode.UnsupportedParameter]: 'unsupported parameter',
[SynchronousCode.ClipsCount]: 'clips count',
[SynchronousCode.ClipsInfo]: 'clips info',
[SynchronousCode.Configuration]: 'configuration',
[SynchronousCode.DeviceInfo]: 'device info',
[SynchronousCode.DiskList]: 'disk list',
[SynchronousCode.FormatReady]: 'format ready',
[SynchronousCode.Notify]: 'notify',
[SynchronousCode.OK]: 'ok',
[SynchronousCode.Remote]: 'remote',
[SynchronousCode.SlotInfo]: 'slot info',
[SynchronousCode.TransportInfo]: 'transport info',
[SynchronousCode.Uptime]: 'uptime',
};
export const slotStatus = {
empty: true,
mounting: true,
error: true,
mounted: true
}
empty: true,
mounting: true,
error: true,
mounted: true,
};
export type SlotStatus = keyof typeof slotStatus
export type SlotStatus = keyof typeof slotStatus;
export const isSlotStatus = (value: any): value is SlotStatus => {
return typeof value === 'string' && slotStatus.hasOwnProperty(value)
}
return typeof value === 'string' && slotStatus.hasOwnProperty(value);
};
export const videoFormats = {
NTSC: true,
PAL: true,
NTSCp: true,
PALp: true,
'720p50': true,
'720p5994': true,
'720p60': true,
'1080p23976': true,
'1080p24': true,
'1080p25': true,
'1080p2997': true,
'1080p30': true,
'1080i50': true,
'1080i5994': true,
'1080i60': true,
'4Kp23976': true,
'4Kp24': true,
'4Kp25': true,
'4Kp2997': true,
'4Kp30': true,
'4Kp50': true,
'4Kp5994': true,
'4Kp60': true
}
NTSC: true,
PAL: true,
NTSCp: true,
PALp: true,
'720p50': true,
'720p5994': true,
'720p60': true,
'1080p23976': true,
'1080p24': true,
'1080p25': true,
'1080p2997': true,
'1080p30': true,
'1080i50': true,
'1080i5994': true,
'1080i60': true,
'4Kp23976': true,
'4Kp24': true,
'4Kp25': true,
'4Kp2997': true,
'4Kp30': true,
'4Kp50': true,
'4Kp5994': true,
'4Kp60': true,
};
export type VideoFormat = keyof typeof videoFormats
export type VideoFormat = keyof typeof videoFormats;
export const isVideoFormat = (value: any): value is VideoFormat => {
return typeof value === 'string' && videoFormats.hasOwnProperty(value)
}
return typeof value === 'string' && videoFormats.hasOwnProperty(value);
};
export const transportStatus = {
preview: true,
stopped: true,
play: true,
forward: true,
rewind: true,
jog: true,
shuttle: true,
record: true
}
preview: true,
stopped: true,
play: true,
forward: true,
rewind: true,
jog: true,
shuttle: true,
record: true,
};
export type TransportStatus = keyof typeof transportStatus
export type TransportStatus = keyof typeof transportStatus;
export const isTransportStatus = (value: any): value is TransportStatus => {
return typeof value === 'string' && transportStatus.hasOwnProperty(value)
}
return typeof value === 'string' && transportStatus.hasOwnProperty(value);
};
export enum FileFormat {
QuickTimeUncompressed = 'QuickTimeUncompressed',
QuickTimeProResHQ = 'QuickTimeProResHQ',
QuickTimeProRes = 'QuickTimeProRes',
QuickTimeProResLT = 'QuickTimeProResLT',
QuickTimeProResProxy = 'QuickTimeProResProxy',
QuickTimeDNxHR220 = 'QuickTimeDNxHR220',
DNxHR220 = 'DNxHR220'
QuickTimeUncompressed = 'QuickTimeUncompressed',
QuickTimeProResHQ = 'QuickTimeProResHQ',
QuickTimeProRes = 'QuickTimeProRes',
QuickTimeProResLT = 'QuickTimeProResLT',
QuickTimeProResProxy = 'QuickTimeProResProxy',
QuickTimeDNxHR220 = 'QuickTimeDNxHR220',
DNxHR220 = 'DNxHR220',
}
export enum AudioInput {
embedded = 'embedded',
XLR = 'XLR',
RCA = 'RCA'
embedded = 'embedded',
XLR = 'XLR',
RCA = 'RCA',
}
export enum VideoInputs {
SDI = 'SDI',
HDMI = 'HDMI',
component = 'component'
SDI = 'SDI',
HDMI = 'HDMI',
component = 'component',
}

@@ -1,120 +0,120 @@

import type { DeserializedCommand } from '../types'
import type { DeserializedCommand } from '../types';
export interface PreviewCommand extends DeserializedCommand {
parameters: {
'disk id'?: string
}
parameters: {
'disk id'?: string;
};
}
export interface PlayCommand extends DeserializedCommand {
parameters: {
speed?: string
loop?: string
'single clip'?: string
}
parameters: {
speed?: string;
loop?: string;
'single clip'?: string;
};
}
export interface PlayrangeSetCommand extends DeserializedCommand {
parameters: {
'clip id'?: string
in?: string
out?: string
}
parameters: {
'clip id'?: string;
in?: string;
out?: string;
};
}
export interface RecordCommand extends DeserializedCommand {
parameters: {
name?: string
}
parameters: {
name?: string;
};
}
export interface ClipsGetCommand extends DeserializedCommand {
parameters: {
'clip id'?: string
count?: string
}
parameters: {
'clip id'?: string;
count?: string;
};
}
export interface ClipsAddCommand extends DeserializedCommand {
parameters: {
name?: string
}
parameters: {
name?: string;
};
}
export interface SlotInfoCommand extends DeserializedCommand {
parameters: {
'slot id'?: string
}
parameters: {
'slot id'?: string;
};
}
export interface SlotSelectCommand extends DeserializedCommand {
parameters: {
'slot id'?: string
'video format'?: string
}
parameters: {
'slot id'?: string;
'video format'?: string;
};
}
export interface NotifyCommand extends DeserializedCommand {
parameters: {
remote?: string
transport?: string
slot?: string
configuration?: string
'dropped frames'?: string
}
parameters: {
remote?: string;
transport?: string;
slot?: string;
configuration?: string;
'dropped frames'?: string;
};
}
export interface GoToCommand extends DeserializedCommand {
parameters: {
'clip id'?: string
clip?: string
timeline?: string
timecode?: string
'slot id'?: string
}
parameters: {
'clip id'?: string;
clip?: string;
timeline?: string;
timecode?: string;
'slot id'?: string;
};
}
export interface JogCommand extends DeserializedCommand {
parameters: {
timecode?: string
}
parameters: {
timecode?: string;
};
}
export interface ShuttleCommand extends DeserializedCommand {
parameters: {
speed?: string
}
parameters: {
speed?: string;
};
}
export interface RemoteCommand extends DeserializedCommand {
parameters: {
remote?: string
}
parameters: {
remote?: string;
};
}
export interface ConfigurationCommand extends DeserializedCommand {
parameters: {
'video input'?: string
'audio input'?: string
'file format'?: string
}
parameters: {
'video input'?: string;
'audio input'?: string;
'file format'?: string;
};
}
export interface FormatCommand extends DeserializedCommand {
parameters: {
prepare?: string
confirm?: string
}
parameters: {
prepare?: string;
confirm?: string;
};
}
export interface IdentifyCommand extends DeserializedCommand {
parameters: {
enable?: string
}
parameters: {
enable?: string;
};
}
export interface WatchdogCommand extends DeserializedCommand {
parameters: {
period?: string
}
parameters: {
period?: string;
};
}

@@ -1,80 +0,80 @@

import type { Timecode } from '../Timecode'
import type { Timecode } from '../Timecode';
import type {
TransportStatus,
VideoFormat,
SlotStatus,
AudioInput,
VideoInputs,
FileFormat
} from '../types'
TransportStatus,
VideoFormat,
SlotStatus,
AudioInput,
VideoInputs,
FileFormat,
} from '../types';
export interface DeviceInfo {
'protocol version': string
model: string
'slot count': string
'protocol version': string;
model: string;
'slot count': string;
}
export interface DiskList extends Record<string, string> {
'slot id': string
'slot id': string;
}
export interface ClipsCount {
'clip count': string
'clip count': string;
}
export interface ClipV1 {
name: string
startT: Timecode
duration: Timecode
name: string;
startT: Timecode;
duration: Timecode;
}
export interface ClipV2 {
startT: Timecode
duration: number
inT: Timecode
outT: Timecode
name: string
startT: Timecode;
duration: number;
inT: Timecode;
outT: Timecode;
name: string;
}
export interface ClipsGet {
clips: ClipV1[]
clips: ClipV1[];
}
export interface TransportInfo {
status: TransportStatus
speed: string
'slot id': string
'clip id': string
'single clip': string
'display timecode': string
timecode: string
'video format': VideoFormat
loop: string
status: TransportStatus;
speed: string;
'slot id': string;
'clip id': string;
'single clip': string;
'display timecode': string;
timecode: string;
'video format': VideoFormat;
loop: string;
}
export interface SlotInfo {
'slot id': string
status: SlotStatus
'volume name': string
'recording time': string
'video format': VideoFormat
'slot id': string;
status: SlotStatus;
'volume name': string;
'recording time': string;
'video format': VideoFormat;
}
export interface Configuration {
'audio input': AudioInput
'video input': VideoInputs
'file format': FileFormat
'audio input': AudioInput;
'video input': VideoInputs;
'file format': FileFormat;
}
export interface Uptime {
uptime: string
uptime: string;
}
export interface Format {
token: string
token: string;
}
export interface RemoteInfoResponse {
enabled: boolean
override: boolean
enabled: boolean;
override: boolean;
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc