@meyer/hyperdeck-emulator
Advanced tools
Comparing version 0.0.4-canary.35.72ad3f9 to 0.0.4-canary.37.241f21f
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 |
@@ -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; | ||
} |
280
src/types.ts
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
462722
25
4524
10