UNPKG

@meyer/hyperdeck-emulator

Version:

Typescript Node.js library for emulating a Blackmagic Hyperdeck

393 lines (343 loc) 10.4 kB
import { invariant } from './invariant'; import { Timecode } from './Timecode'; import type { CommandName, CommandParamsByCommandName, CommandResponsesByCommandName } from './api'; export interface NotificationConfig { transport: boolean; remote: boolean; slot: boolean; configuration: boolean; } export type DeserializedCommandsByName = { [K in CommandName]: { raw: string; name: K; parameters: CommandParamsByCommandName[K]; }; }; export type DeserializedCommand = DeserializedCommandsByName[CommandName]; export type CommandHandler<T extends CommandName> = ( cmd: DeserializedCommandsByName[T]['parameters'] ) => Promise<CommandResponsesByCommandName[T]>; export type ResponsesByCommandName = { [K in CommandName]: { name: K; response: CommandResponsesByCommandName[K]; }; }; export type CommandResponse = ResponsesByCommandName[CommandName]; 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, } 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, } export enum AsynchronousCode { ConnectionInfo = 500, SlotInfo = 502, TransportInfo = 508, RemoteInfo = 510, ConfigurationInfo = 511, } 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', }; export const slotStatus = { empty: true, mounting: true, error: true, mounted: true, }; export type SlotStatus = keyof typeof slotStatus; export const isSlotStatus = (value: any): value is SlotStatus => { 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, }; export interface ClipV1 { name: string; startT: Timecode; duration: Timecode; } export const isClipV1 = (value: any): value is ClipV1 => { return typeof value === 'object' && value !== null && typeof value.name === 'string'; }; export interface ClipV2 { startT: Timecode; duration: number; inT: Timecode; outT: Timecode; name: string; } export type VideoFormat = keyof typeof videoFormats; export const isVideoFormat = (value: any): value is VideoFormat => { 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, }; export type TransportStatus = keyof typeof transportStatus; export const isTransportStatus = (value: any): value is TransportStatus => { return typeof value === 'string' && transportStatus.hasOwnProperty(value); }; export const stopModes = { lastframe: true, nextframe: true, black: true, }; export type StopMode = keyof typeof stopModes; export const isStopMode = (value: any): value is StopMode => { return typeof value === 'string' && stopModes.hasOwnProperty(value); }; export const videoInputs = { SDI: true, HDMI: true, component: true, }; export type VideoInput = keyof typeof videoInputs; export const isVideoInput = (value: any): value is VideoInput => { return typeof value === 'string' && videoInputs.hasOwnProperty(value); }; export const audioInputs = { XLR: true, RCA: true, // TODO(meyer) verify this embedded: true, }; export type AudioInput = keyof typeof audioInputs; export const isAudioInput = (value: any): value is AudioInput => { return typeof value === 'string' && audioInputs.hasOwnProperty(value); }; export const audioCodecs = { PCM: true, AAC: true, }; export type AudioCodec = keyof typeof audioCodecs; export const isAudioCodec = (value: any): value is AudioCodec => { return typeof value === 'string' && audioCodecs.hasOwnProperty(value); }; export const timecodeInputs = { external: true, embedded: true, preset: true, clip: true, }; export type TimecodeInput = keyof typeof timecodeInputs; export const isTimecodeInput = (value: any): value is TimecodeInput => { return typeof value === 'string' && timecodeInputs.hasOwnProperty(value); }; export const recordTriggers = { none: true, recordbit: true, timecoderun: true, }; export type RecordTrigger = keyof typeof recordTriggers; export const isRecordTrigger = (value: any): value is RecordTrigger => { return typeof value === 'string' && recordTriggers.hasOwnProperty(value); }; export type FileFormat = | 'QuickTimeUncompressed' | 'QuickTimeProResHQ' | 'QuickTimeProRes' | 'QuickTimeProResLT' | 'QuickTimeProResProxy' | 'QuickTimeDNxHR220' | 'DNxHR220'; export type ArgKey = keyof TypesByStringKey; export type ArgsTypes<T extends Record<string, ArgKey>> = { [K in keyof T]?: TypesByStringKey[T[K]]; }; export interface TypesByStringKey { boolean: boolean; string: string; timecode: Timecode; number: number; videoformat: VideoFormat; stopmode: StopMode; goto: 'start' | 'end' | string | number; videoinput: VideoInput; audioinput: AudioInput; fileformat: string; audiocodec: AudioCodec; timecodeinput: TimecodeInput; recordtrigger: RecordTrigger; clips: ClipV1[]; slotstatus: SlotStatus; transportstatus: TransportStatus; } function assertArrayOf<T>( predicate: (v: any) => v is T, value: any, message: string ): asserts value is T[] { invariant(Array.isArray(value), 'Expected an array'); for (const item of value) { invariant(predicate(item), message); } } const getStringOrThrow = (value: any): string => { invariant(typeof value === 'string', 'Expected a string'); return value; }; export const stringToValueFns: { /** Coerce string to the correct type or throw if the string cannot be converted. */ [K in keyof TypesByStringKey]: (value: unknown) => TypesByStringKey[K]; } = { boolean: (value) => { if (value === 'true') return true; if (value === 'false') return false; invariant(false, 'Unsupported value `%o` passed to `boolean`', value); }, string: getStringOrThrow, timecode: (value) => Timecode.toTimecode(getStringOrThrow(value)), number: (value) => { const valueNum = parseFloat(getStringOrThrow(value)); invariant(!isNaN(valueNum), 'valueNum `%o` is NaN', value); return valueNum; }, videoformat: (value) => { invariant(isVideoFormat(value), 'Unsupported video format: `%o`'); return value; }, stopmode: (value) => { invariant(isStopMode(value), 'Unsupported stopmode: `%o`', value); return value; }, goto: (value) => { if (value === 'start' || value === 'end') { return value; } const valueNum = parseInt(getStringOrThrow(value), 10); if (!isNaN(valueNum)) { return valueNum; } // TODO(meyer) validate further return getStringOrThrow(value); }, videoinput: (value) => { invariant(isVideoInput(value), 'Unsupported video input: `%o`', value); return value; }, audioinput: (value) => { invariant(isAudioInput(value), 'Unsupported audio input: `%o`', value); return value; }, fileformat: getStringOrThrow, audiocodec: (value) => { invariant(isAudioCodec(value), 'Unsupported audio codec: `%o`', value); return value; }, timecodeinput: (value) => { invariant(isTimecodeInput(value), 'Unsupported timecode input: `%o`', value); return value; }, recordtrigger: (value) => { invariant(isRecordTrigger(value), 'Unsupported record trigger: `%o`', value); return value; }, clips: (value) => { assertArrayOf(isClipV1, value, 'Expected an array of clips'); return value; }, slotstatus: (value) => { invariant(isSlotStatus(value), 'Unsupported slot status: `%o`', value); return value; }, transportstatus: (value) => { invariant(isTransportStatus(value), 'Unsupported slot status: `%o`', value); return value; }, };