UNPKG

@meyer/hyperdeck-emulator

Version:

Typescript Node.js library for emulating a Blackmagic Hyperdeck

3 lines (2 loc) 18.3 kB
"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var t,n,o,r=require("events"),i=e(require("util")),s=require("net"),a=e(require("pino"));class p extends Error{constructor(e,...t){super(i.format(e,...t)),this.template=e,this.args=t}}function d(e,t,...n){if(!e)throw new p(t,...n)}class l{constructor(e,t,n,o){const r=[e,t,n,o].map(e=>{const t=Math.floor(e);return t===e&&e>=0&&e<=99||d(!1),(t+100).toString().slice(-2)}).join(":");this.toString=()=>r}}l.toTimecode=e=>{const t=e.split(":");4!==t.length&&d(!1);const n=t.map(e=>{const t=parseInt(e,10);return isNaN(t)&&d(!1),t});return new l(n[0],n[1],n[2],n[3])},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"}(n||(n={})),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={}));const c={[o.ConfigurationInfo]:"configuration info",[o.ConnectionInfo]:"connection info",[o.RemoteInfo]:"remote info",[o.SlotInfo]:"slot info",[o.TransportInfo]:"transport info",[t.ConnectionRejected]:"connection rejected",[t.DiskError]:"disk error",[t.DiskFull]:"disk full",[t.FormatNotPrepared]:"format not prepared",[t.InternalError]:"internal error",[t.InvalidCodec]:"invalid codec",[t.InvalidFormat]:"invalid format",[t.InvalidState]:"invalid state",[t.InvalidToken]:"invalid token",[t.InvalidValue]:"invalid value",[t.NoDisk]:"no disk",[t.NoInput]:"no input",[t.OutOfRange]:"out of range",[t.RemoteControlDisabled]:"remote control disabled",[t.SyntaxError]:"syntax error",[t.TimelineEmpty]:"timeline empty",[t.Unsupported]:"unsupported",[t.UnsupportedParameter]:"unsupported parameter",[n.ClipsCount]:"clips count",[n.ClipsInfo]:"clips info",[n.Configuration]:"configuration",[n.DeviceInfo]:"device info",[n.DiskList]:"disk list",[n.FormatReady]:"format ready",[n.Notify]:"notify",[n.OK]:"ok",[n.Remote]:"remote",[n.SlotInfo]:"slot info",[n.TransportInfo]:"transport info",[n.Uptime]:"uptime"},u={empty:!0,mounting:!0,error:!0,mounted:!0},m={NTSC:!0,PAL:!0,NTSCp:!0,PALp:!0,"720p50":!0,"720p5994":!0,"720p60":!0,"1080p23976":!0,"1080p24":!0,"1080p25":!0,"1080p2997":!0,"1080p30":!0,"1080i50":!0,"1080i5994":!0,"1080i60":!0,"4Kp23976":!0,"4Kp24":!0,"4Kp25":!0,"4Kp2997":!0,"4Kp30":!0,"4Kp50":!0,"4Kp5994":!0,"4Kp60":!0},f=e=>"object"==typeof e&&null!==e&&"string"==typeof e.name,g={preview:!0,stopped:!0,play:!0,forward:!0,rewind:!0,jog:!0,shuttle:!0,record:!0},h={lastframe:!0,nextframe:!0,black:!0},y={SDI:!0,HDMI:!0,component:!0},b={XLR:!0,RCA:!0,embedded:!0},v={PCM:!0,AAC:!0},I={external:!0,embedded:!0,preset:!0,clip:!0},O={none:!0,recordbit:!0,timecoderun:!0},C=e=>("string"!=typeof e&&d(!1),e),k={boolean:e=>"true"===e||"false"!==e&&void d(!1),string:C,timecode:e=>l.toTimecode(C(e)),number:e=>{const t=parseFloat(C(e));return isNaN(t)&&d(!1),t},videoformat:e=>((e=>"string"==typeof e&&m.hasOwnProperty(e))(e)||d(!1),e),stopmode:e=>((e=>"string"==typeof e&&h.hasOwnProperty(e))(e)||d(!1),e),goto:e=>{if("start"===e||"end"===e)return e;const t=parseInt(C(e),10);return isNaN(t)?C(e):t},videoinput:e=>((e=>"string"==typeof e&&y.hasOwnProperty(e))(e)||d(!1),e),audioinput:e=>((e=>"string"==typeof e&&b.hasOwnProperty(e))(e)||d(!1),e),fileformat:C,audiocodec:e=>((e=>"string"==typeof e&&v.hasOwnProperty(e))(e)||d(!1),e),timecodeinput:e=>((e=>"string"==typeof e&&I.hasOwnProperty(e))(e)||d(!1),e),recordtrigger:e=>((e=>"string"==typeof e&&O.hasOwnProperty(e))(e)||d(!1),e),clips:e=>(function(e,t,n){Array.isArray(t)||d(!1);for(const n of t)e(n)||d(!1)}(f,e),e),slotstatus:e=>((e=>"string"==typeof e&&u.hasOwnProperty(e))(e)||d(!1),e),transportstatus:e=>((e=>"string"==typeof e&&g.hasOwnProperty(e))(e)||d(!1),e)},w=e=>e.replace(/([a-z])([A-Z]+)/g,"$1 $2").toLowerCase();class R{constructor(e={}){this.options=e,this.addOption=(e,t)=>{const n=Array.isArray(e)?e[0]:e;return this.options.hasOwnProperty(n)&&d(!1),Object.assign(this.options,{[n]:t}),this},this.getParamsByCommandName=()=>Object.entries(this.options).reduce((e,[t,n])=>n.arguments?(e[t]=Object.entries(n.arguments).reduce((e,[t,n])=>(e[w(t)]={paramType:n,paramName:t},e),{}),e):(e[t]={},e),{})}}const P=(new R).addOption(["help","?"],{description:"Provides help text on all commands and parameters",returnValue:{}}).addOption("commands",{description:"return commands in XML format",returnValue:{commands:"string"}}).addOption("device info",{description:"return device information",returnValue:{protocolVersion:"string",model:"string",slotCount:"string"}}).addOption("disk list",{description:"query clip list on active disk",arguments:{slotId:"number"},returnValue:{slotId:"number"}}).addOption("quit",{description:"disconnect ethernet control",returnValue:{}}).addOption("ping",{description:"check device is responding",returnValue:{}}).addOption("preview",{description:"switch to preview or output",arguments:{enable:"boolean"},returnValue:{}}).addOption("play",{description:"play from current timecode",arguments:{speed:"number",loop:"boolean",singleClip:"boolean"},returnValue:{}}).addOption("playrange",{description:"query playrange setting",returnValue:{}}).addOption("playrange set",{description:"set play range to play clip {n} only",arguments:{clipId:"number",in:"timecode",out:"timecode",timelineIn:"number",timelineOut:"number"},returnValue:{}}).addOption("playrange clear",{description:"clear/reset play range setting",returnValue:{}}).addOption("play on startup",{description:"query unit play on startup state",arguments:{enable:"boolean",singleClip:"boolean"},returnValue:{}}).addOption("play option",{description:"query play options",arguments:{stopMode:"stopmode"},returnValue:{}}).addOption("record",{description:"record from current input",arguments:{name:"string"},returnValue:{}}).addOption("record spill",{description:"spill current recording to next slot",arguments:{slotId:"number"},returnValue:{}}).addOption("stop",{description:"stop playback or recording",returnValue:{}}).addOption("clips count",{description:"query number of clips on timeline",returnValue:{clipCount:"number"}}).addOption("clips get",{description:"query all timeline clips",arguments:{clipId:"number",count:"number",version:"number"},returnValue:{clips:"clips"}}).addOption("clips add",{description:"append a clip to timeline",arguments:{name:"string",clipId:"number",in:"timecode",out:"timecode"},returnValue:{}}).addOption("clips remove",{description:"remove clip {n} from the timeline (invalidates clip ids following clip {n})",arguments:{clipId:"number"},returnValue:{}}).addOption("clips clear",{description:"empty timeline clip list",returnValue:{}}).addOption("transport info",{description:"query current activity",returnValue:{status:"transportstatus",speed:"number",slotId:"number",clipId:"number",singleClip:"boolean",displayTimecode:"timecode",timecode:"timecode",videoFormat:"videoformat",loop:"boolean"}}).addOption("slot info",{description:"query active slot",arguments:{slotId:"number"},returnValue:{slotId:"number",status:"slotstatus",volumeName:"string",recordingTime:"timecode",videoFormat:"videoformat"}}).addOption("slot select",{description:"switch to specified slot",arguments:{slotId:"number",videoFormat:"videoformat"},returnValue:{}}).addOption("slot unblock",{description:"unblock active slot",arguments:{slotId:"number"},returnValue:{}}).addOption("dynamic range",{description:"query dynamic range settings",arguments:{playbackOverride:"string"},returnValue:{}}).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"},returnValue:{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:"number",clip:"goto",timeline:"goto",timecode:"timecode",slotId:"number"},returnValue:{}}).addOption("jog",{description:"jog forward or backward",arguments:{timecode:"timecode"},returnValue:{}}).addOption("shuttle",{description:"shuttle with speed",arguments:{speed:"number"},returnValue:{}}).addOption("remote",{description:"query unit remote control state",arguments:{enable:"boolean",override:"boolean"},returnValue:{}}).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"},returnValue:{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",returnValue:{uptime:"number"}}).addOption("format",{description:"prepare a disk formatting operation to filesystem {format}",arguments:{prepare:"string",confirm:"string"},returnValue:{token:"string"}}).addOption("identify",{description:"identify the device",arguments:{enable:"boolean"},returnValue:{}}).addOption("watchdog",{description:"client connection timeout",arguments:{period:"number"},returnValue:{}}).getParamsByCommandName();function T(e){"string"==typeof e&&P.hasOwnProperty(e)||d(!1)}class S{constructor(e){this.linesQueue=[],this.logger=e.child({name:"MultilineParser"})}receivedString(e){const t=[],n=e.split("\r\n");for(n.length>0&&""===n[n.length-1]&&n.pop(),this.linesQueue=this.linesQueue.concat(n);this.linesQueue.length>0;){if(""===this.linesQueue[0]){this.linesQueue.shift();continue}if(!this.linesQueue[0].includes(":")||1===this.linesQueue.length&&this.linesQueue[0].includes(":")){const e=this.parseResponse(this.linesQueue.splice(0,1));e&&t.push(e);continue}const e=this.linesQueue.indexOf("");if(-1===e)break;const n=this.linesQueue.splice(0,e+1),o=this.parseResponse(n);o&&t.push(o)}return t}parseResponse(e){try{const t=e.map(e=>e.trim()),n=t[0];if(1===t.length){if(!n.includes(":"))return T(n),{raw:t.join("\r\n"),name:n,parameters:{}};const e=n.split(": "),o=e.shift();T(o);const r={},i=P[o];let s=e.shift();s||d(!1);for(let t=0;t<e.length-1;t++){const n=e[t].split(" ");let o="";for(let e=n.length-1;e>=0&&(o=(n.pop()+" "+o).trim(),!i.hasOwnProperty(o));e--);n.length>0||d(!1),i.hasOwnProperty(s)||d(!1);const a=n.join(" "),{paramName:p,paramType:l}=i[s];r[p]=(0,k[l])(a),s=o}i.hasOwnProperty(s)||d(!1);const a=e[e.length-1],{paramName:p,paramType:l}=i[s];return r[p]=(0,k[l])(a),{raw:t.join("\r\n"),name:o,parameters:r}}n.endsWith(":")||d(!1);const o=n.slice(0,-1);T(o);const r=P[o],i={};for(const e of t){const t=e.match(/^(.*?): (.*)$/im);t||d(!1);const n=t[1],o=t[2];r.hasOwnProperty(n)||d(!1);const{paramName:s,paramType:a}=r[n];i[s]=(0,k[a])(o)}return{raw:t.join("\r\n"),name:o,parameters:i}}catch(e){return e instanceof p?this.logger.error(e.template,...e.args):this.logger.error({err:e+""},"parseResponse error"),null}}}class V extends r.EventEmitter{constructor(e,t,n){super(),this.socket=e,this.logger=t,this.receivedCommand=n,this.lastReceivedMS=-1,this.watchdogTimer=null,this.notifySettings={configuration:!1,displayTimecode:!1,droppedFrames:!1,dynamicRange:!1,playrange:!1,remote:!1,slot:!1,timelinePosition:!1,transport:!1},this.parser=new S(t),this.socket.setEncoding("utf-8"),this.socket.on("data",e=>{this.onMessage(e)}),this.socket.on("error",e=>{t.info({err:e},"error"),this.socket.destroy(),this.emit("disconnected"),t.info("manually disconnected")}),this.sendResponse(o.ConnectionInfo,{"protocol version":"1.11",model:"NodeJS HyperDeck Server Library"})}onMessage(e){this.logger.info({data:e},"<--- received message from client"),this.lastReceivedMS=Date.now();const r=this.parser.receivedString(e);this.logger.info({cmds:r},"parsed commands");for(const e of r){if("watchdog"===e.name){this.watchdogTimer&&global.clearInterval(this.watchdogTimer);const t=e;t.parameters.period&&(this.watchdogTimer=global.setInterval(()=>{Date.now()-this.lastReceivedMS>Number(t.parameters.period)&&(this.socket.destroy(),this.emit("disconnected"),this.watchdogTimer&&clearInterval(this.watchdogTimer))},1e3*Number(t.parameters.period)))}else if("notify"===e.name){const t=e;if(!(Object.keys(t.parameters).length>0)){const t={};for(const e of Object.keys(this.notifySettings))t[e]=this.notifySettings[e]?"true":"false";this.sendResponse(n.Notify,t,e);continue}for(const e of Object.keys(t.parameters))void 0!==this.notifySettings[e]&&(this.notifySettings[e]=!0===t.parameters[e])}this.receivedCommand(e).then(r=>"object"==typeof r?this.sendResponse(r.code,"params"in r&&r.params||"message"in r&&r.message||void 0,e):"number"==typeof r&&(t[r]||n[r]||o[r])?this.sendResponse(r,void 0,e):(this.logger.error({cmd:e,codeOrObj:r},"codeOrObj was neither a ResponseCode nor a response object"),void this.sendResponse(t.InternalError,void 0,e)),()=>this.sendResponse(t.Unsupported,void 0,e))}}sendResponse(e,n,o){try{const r=((e,t)=>{if("string"==typeof t)return e+" "+t.replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/:/g,"")+"\r\n";const n=`${e} ${c[e]}`;if(!t)return n+"\r\n";const o=Object.entries(t).filter(([,e])=>null!=e);return 0===o.length?n+"\r\n":o.reduce((e,[t,n])=>{let o;return"string"==typeof n?o=n:"boolean"==typeof n?o=n?"true":"false":"number"==typeof n||n instanceof l?o=n.toString():d(!1),e+w(t)+": "+o+"\r\n"},n+":\r\n")+"\r\n"})(e,n);this.logger[t[e]?"error":"info"]({responseText:r,cmd:o},"---\x3e send response to client"),this.socket.write(r)}catch(e){this.logger.error({cmd:o},"-x-> Error sending response: %s",e)}}notify(e,t){this.logger.info({type:e,params:t},"notify"),"configuration"===e&&this.notifySettings.configuration?this.sendResponse(o.ConfigurationInfo,t):"remote"===e&&this.notifySettings.remote?this.sendResponse(o.RemoteInfo,t):"slot"===e&&this.notifySettings.slot?this.sendResponse(o.SlotInfo,t):"transport"===e&&this.notifySettings.transport?this.sendResponse(o.TransportInfo,t):this.logger.error({type:e,params:t},"unhandled notify type")}}const N=e=>{if(!e.clips)return{clipsCount:0};const t=e.clips.length,n={clipsCount:t};for(let o=0;o<t;o++){const t=e.clips[o];n[(o+1).toString()]=`${t.name} ${t.startT} ${t.duration}`}return n};exports.HyperDeckServer=class{constructor(e,o=a()){const r=this;this.sockets={},this.commandHandlers={},this.on=(e,t)=>{P.hasOwnProperty(e)||d(!1),this.commandHandlers.hasOwnProperty(e)&&d(!1),this.commandHandlers[e]=t},this.receivedCommand=function(e){try{return Promise.resolve(new Promise(e=>setTimeout(()=>e(),200))).then((function(){if(r.logger.info({cmd:e},"receivedCommand %s",e.name),"remote"===e.name)return{code:n.Remote,params:{enabled:!0,override:!1}};if("notify"===e.name||"watchdog"===e.name||"ping"===e.name)return n.OK;const o=r.commandHandlers[e.name];return o?Promise.resolve(o(e)).then((function(o){const i={name:e.name,response:o};return"clips add"===i.name||"clips clear"===i.name||"goto"===i.name||"identify"===i.name||"jog"===i.name||"play"===i.name||"playrange clear"===i.name||"playrange set"===i.name||"preview"===i.name||"record"===i.name||"shuttle"===i.name||"slot select"===i.name||"stop"===i.name?n.OK:"device info"===i.name?{code:n.DeviceInfo,params:i.response}:"disk list"===i.name?{code:n.DiskList,params:i.response}:"clips count"===i.name?{code:n.ClipsCount,params:i.response}:"clips get"===i.name?{code:n.ClipsInfo,params:N(i.response)}:"transport info"===i.name?{code:n.TransportInfo,params:i.response}:"slot info"===i.name?{code:n.SlotInfo,params:i.response}:"configuration"===i.name?i?{code:n.Configuration,params:i.response}:n.OK:"uptime"===i.name?{code:n.Uptime,params:i.response}:"format"===i.name?i?{code:n.FormatReady,params:i.response}:n.OK:(r.logger.error({cmd:e,res:i},"Unsupported command"),t.Unsupported)})):(r.logger.error({cmd:e},"unimplemented"),t.Unsupported)}))}catch(e){return Promise.reject(e)}},this.logger=o.child({name:"HyperDeck Emulator"}),this.server=s.createServer(e=>{this.logger.info("connection");const t=Math.random().toString(35).substr(-6),n=this.logger.child({name:"HyperDeck socket "+t});this.sockets[t]=new V(e,n,this.receivedCommand),this.sockets[t].on("disconnected",()=>{n.info("disconnected"),delete this.sockets[t]})}),this.server.on("listening",()=>{this.logger.info("listening",{address:this.server.address()})}),this.server.on("close",()=>this.logger.info("connection closed")),this.server.on("error",e=>this.logger.error("server error:",e)),this.server.maxConnections=1,"ip"in e?this.server.listen(e.port||9993,e.ip):"fd"in e?this.server.listen({fd:e.fd}):d(!1)}close(){this.server.unref()}notifySlot(e){this.notify("slot",e)}notifyTransport(e){this.notify("transport",e)}notify(e,t){for(const n of Object.keys(this.sockets))this.sockets[n].notify(e,t)}},exports.Timecode=l; //# sourceMappingURL=hyperdeck-emulator.cjs.production.min.js.map