@meyer/hyperdeck-emulator
Version:
Typescript Node.js library for emulating a Blackmagic Hyperdeck
191 lines (165 loc) • 6.01 kB
text/typescript
import type { Socket } from 'net';
import { EventEmitter } from 'events';
import {
AsynchronousCode,
DeserializedCommand,
ErrorCode,
NotifyType,
SynchronousCode,
ResponseCode,
DeserializedCommandsByName,
TypesByStringKey,
} from './types';
import { MultilineParser } from './MultilineParser';
import type { Logger } from 'pino';
import { messageForCode } from './messageForCode';
interface ResponseWithMessage {
code: ErrorCode;
message: string;
}
interface ResponseWithParams {
code: ResponseCode;
params?: Record<string, any>;
}
export type ReceivedCommandCallback = (
cmd: DeserializedCommand
) => Promise<ResponseCode | ResponseWithParams | ResponseWithMessage>;
export class HyperDeckSocket extends EventEmitter {
constructor(
private socket: Socket,
private logger: Logger,
private receivedCommand: ReceivedCommandCallback
) {
super();
this.parser = new MultilineParser(logger);
this.socket.setEncoding('utf-8');
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.sendResponse(AsynchronousCode.ConnectionInfo, {
'protocol version': '1.11',
model: 'NodeJS HyperDeck Server Library',
});
}
private parser: MultilineParser;
private lastReceivedMS = -1;
private watchdogTimer: NodeJS.Timeout | null = null;
private notifySettings: Record<
keyof DeserializedCommandsByName['notify']['parameters'],
boolean
> = {
configuration: false,
displayTimecode: false,
droppedFrames: false,
dynamicRange: false,
playrange: false,
remote: false,
slot: false,
timelinePosition: false,
transport: false,
};
private onMessage(data: string): void {
this.logger.info({ data }, '<--- received message from client');
this.lastReceivedMS = Date.now();
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) global.clearInterval(this.watchdogTimer);
const watchdogCmd = cmd as DeserializedCommandsByName['watchdog'];
if (watchdogCmd.parameters.period) {
this.watchdogTimer = global.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 DeserializedCommandsByName['notify'];
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;
}
}
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;
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)
);
}
}
sendResponse(
code: ResponseCode,
paramsOrMessage?: Record<string, TypesByStringKey[keyof TypesByStringKey]> | string,
cmd?: DeserializedCommand
): void {
try {
const responseText = messageForCode(code, paramsOrMessage);
const method = ErrorCode[code] ? 'error' : 'info';
this.logger[method]({ responseText, cmd }, '---> send response to client');
this.socket.write(responseText);
} catch (err) {
this.logger.error({ cmd }, '-x-> Error sending response: %s', err);
}
}
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');
}
}
}