├── .yarnrc.yml ├── README.md ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── companion-module-checks.yaml ├── package.json ├── LICENSE ├── companion ├── manifest.json └── HELP.md ├── src ├── types.ts ├── util.ts ├── choices │ └── videoFormats.ts ├── config.ts ├── upgrades.ts ├── models.ts ├── feedbacks.ts ├── choices.ts ├── variables.ts ├── presets.ts ├── index.ts └── actions.ts └── yarn.lock /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # companion-module-bmd-hyperdeck 2 | 3 | See HELP.md and LICENSE 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | /pkg 4 | /*.tgz 5 | /dist 6 | DEBUG-* 7 | .DS_Store 8 | package-lock.json 9 | 10 | .pnp.* 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/sdks 16 | */.yarn/* 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@companion-module/tools/tsconfig/node18/recommended", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"], 5 | "compilerOptions": { 6 | "outDir": "./dist", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["./node_modules/*"] 10 | }, 11 | "module": "ES2022" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/companion-module-checks.yaml: -------------------------------------------------------------------------------- 1 | name: Companion Module Checks 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | check: 8 | name: Check module 9 | 10 | if: ${{ !contains(github.repository, 'companion-module-template-') }} 11 | 12 | permissions: 13 | packages: read 14 | 15 | uses: bitfocus/actions/.github/workflows/module-checks.yaml@main 16 | # with: 17 | # upload-artifact: true # uncomment this to upload the built package as an artifact to this workflow that you can download and share with others 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bmd-hyperdeck", 3 | "version": "2.5.0", 4 | "main": "dist/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "dev": "tsc --watch", 10 | "format": "prettier --write ." 11 | }, 12 | "license": "MIT", 13 | "dependencies": { 14 | "@companion-module/base": "~1.11.3", 15 | "hyperdeck-connection": "2.0.1", 16 | "semver": "^7.7.1", 17 | "smpte-timecode": "^1.3.6" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/bitfocus/companion-module-bmd-hyperdeck.git" 22 | }, 23 | "devDependencies": { 24 | "@companion-module/tools": "^2.3.0", 25 | "@types/node": "^22.15.3", 26 | "@types/semver": "^7.7.0", 27 | "@types/smpte-timecode": "^1.2.5", 28 | "prettier": "^3.5.3", 29 | "typescript": "~5.8.3" 30 | }, 31 | "packageManager": "yarn@4.9.1" 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Bitfocus AS, William Viker & Håkon Nessjøen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /companion/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bmd-hyperdeck", 3 | "name": "bmd-hyperdeck", 4 | "shortname": "hyperdeck", 5 | "description": "Module to control Hyperdeck", 6 | "version": "0.0.0", 7 | "license": "MIT", 8 | "repository": "git+https://github.com/bitfocus/companion-module-bmd-hyperdeck.git", 9 | "bugs": "https://github.com/bitfocus/companion-module-bmd-hyperdeck/issues", 10 | "maintainers": [ 11 | { 12 | "name": "Per Roine", 13 | "email": "per.roine@gmail.com" 14 | }, 15 | { 16 | "name": "Stephen Harrison", 17 | "email": "stephen@redleopard.org" 18 | }, 19 | { 20 | "name": "Keith Rocheck" 21 | }, 22 | { 23 | "name": "Jeffrey Davidsz" 24 | }, 25 | { 26 | "name": "Mint de Wit", 27 | "email": "mint@superfly.tv" 28 | }, 29 | { 30 | "name": "Julian Waller", 31 | "email": "git@julusian.co.uk" 32 | } 33 | ], 34 | "legacyIds": ["hyperdeck"], 35 | "runtime": { 36 | "type": "node22", 37 | "api": "nodejs-ipc", 38 | "apiVersion": "0.0.0", 39 | "entrypoint": "../dist/index.js" 40 | }, 41 | "manufacturer": "Blackmagic Design", 42 | "products": ["Hyperdeck"], 43 | "keywords": ["Recording", "Video", "Media"], 44 | "bonjourQueries": { 45 | "bonjourHost": { 46 | "type": "hyperdeck_ctrl", 47 | "protocol": "tcp" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DropdownChoice, InstanceBase } from '@companion-module/base' 2 | import type { HyperdeckConfig } from './config' 3 | import type { Commands } from 'hyperdeck-connection' 4 | import type { ModelInfo } from './models' 5 | import type { SimpleClipInfo } from './util' 6 | 7 | export interface ClipDropdownChoice extends DropdownChoice { 8 | clipId: number 9 | } 10 | 11 | export interface InstanceBaseExt extends InstanceBase { 12 | transportInfo: TransportInfoStateExt 13 | deckConfig: Commands.ConfigurationCommandResponse 14 | slotInfo: Commands.SlotInfoCommandResponse[] 15 | 16 | protocolVersion: number 17 | model: ModelInfo 18 | config: HyperdeckConfig 19 | 20 | remoteInfo: Commands.RemoteInfoCommandResponse | null 21 | formatToken: string | null 22 | formatTokenTimeout: NodeJS.Timeout | null 23 | 24 | simpleClipsList: SimpleClipInfo[] 25 | fullClipsList: Commands.ClipInfo[] 26 | 27 | /** 28 | * INTERNAL: Get clip list from the hyperdeck 29 | * 30 | * @access protected 31 | */ 32 | updateClips(doFullInit?: boolean): Promise 33 | 34 | sendCommand(cmd: Commands.AbstractCommand): Promise 35 | 36 | refreshTransportInfo(): Promise 37 | } 38 | 39 | export interface TransportInfoStateExt extends Commands.TransportInfoCommandResponse { 40 | clipName: string | null 41 | } 42 | 43 | export interface IpAndPort { 44 | ip: string 45 | port: number | undefined 46 | } 47 | -------------------------------------------------------------------------------- /companion/HELP.md: -------------------------------------------------------------------------------- 1 | ## Blackmagic Design Hyperdeck 2 | 3 | Allows you to connect and control any model of Hyperdeck available from Blackmagic Design. 4 | 5 | **Connecting** 6 | Older models of Hyperdeck (pre 2023) can **only be connected to one controller at a time.** This includes ATEM switchers or other instances of Companion. If you're having trouble connecting, make sure all other controllers are disconnected. 7 | 8 | **Supported commands** 9 | 10 | - Play (options for speed %, single clip, and loop) 11 | - Record 12 | - Record (with name) 13 | - Record (with name and current date/time) 14 | - Record (with custom reel) 15 | - Stop 16 | - Goto (timecode) 17 | - Goto Clip (n) 18 | - Goto Clip (name) 19 | - Go Fwd (n) clips 20 | - Go Back (n) clips 21 | - Goto Start/End of clip 22 | - Jog forward (timecode) duration 23 | - Jog backward (timecode) duration 24 | - Shuttle (with speed) 25 | - Select (slot) 26 | - Set preview/output mode 27 | - Select video input 28 | - Select audio input 29 | - Select file format 30 | - Format Prepare 31 | - Format Confirm 32 | - Remote Control (enable/disable) 33 | 34 | **Supported feedback** 35 | 36 | - Transport Status 37 | - Active Clip 38 | - Active Slot 39 | - Slot/Disk Status 40 | - Loop Playback Status 41 | - Single Clip Playback Status 42 | - Video Input 43 | - Audio Input 44 | - Format Prepared 45 | 46 | **Supported button variables** 47 | 48 | - Transport Status 49 | - Speed 50 | - Clip ID 51 | - Slot ID 52 | - Video Format 53 | - Recording time available 54 | - Clip count 55 | - Timecode 56 | - File Format 57 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import type { Commands } from 'hyperdeck-connection' 2 | import semver from 'semver' 3 | 4 | /** 5 | * Creates a string with the current date/time 6 | * 7 | * @returns {string} the current date/time in format 'YYYYMMDD_HHMM' 8 | * @access public 9 | * @since 1.0.3 10 | */ 11 | export function getTimestamp(): string { 12 | const d = new Date() 13 | const curr_date = ('0' + d.getDate()).slice(-2) 14 | const curr_month = ('0' + (d.getMonth() + 1)).slice(-2) 15 | const curr_year = d.getFullYear() 16 | const h = ('0' + d.getHours()).slice(-2) 17 | const m = ('0' + d.getMinutes()).slice(-2) 18 | const s = ('0' + d.getSeconds()).slice(-2) 19 | return `${curr_year}${curr_month}${curr_date}_${h}${m}${s}` 20 | } 21 | 22 | /** 23 | * Compare protocol version numbers 24 | * 25 | * @access protected 26 | * @since 2.0.3 27 | */ 28 | export function protocolGte(a: string | number, b: string | number): boolean { 29 | const v1 = semver.coerce(a.toString()) 30 | const v2 = semver.coerce(b.toString()) 31 | return !!v1 && !!v2 && semver.gte(v1, v2) 32 | } 33 | 34 | export function stripExtension(fileName: string): string { 35 | const re = /(.*?)(\.([a-z]|\d){3})?$/ 36 | return fileName.replace(re, '$1') 37 | } 38 | 39 | export function mergeState(current: T, patch: Partial): T { 40 | const res = { ...current } 41 | 42 | for (let key in patch) { 43 | if (patch[key] !== undefined) { 44 | res[key] = patch[key] as any 45 | } 46 | } 47 | 48 | return res 49 | } 50 | 51 | export function toHHMMSS(secs: number): string { 52 | var hours = Math.floor(secs / 3600) 53 | var minutes = Math.floor(secs / 60) % 60 54 | var seconds = secs % 60 55 | 56 | return [hours, minutes, seconds].map((v) => (v < 10 ? '0' + v : v)).join(':') 57 | } 58 | 59 | export interface SimpleClipInfo { 60 | clipId: number 61 | name: string 62 | } 63 | export function makeSimpleClipInfos(clips: Commands.ClipInfo[]): SimpleClipInfo[] { 64 | return clips.map((clip) => ({ clipId: clip.clipId, name: clip.name })) 65 | } 66 | -------------------------------------------------------------------------------- /src/choices/videoFormats.ts: -------------------------------------------------------------------------------- 1 | import { DropdownChoice } from '@companion-module/base' 2 | import { VideoFormat } from 'hyperdeck-connection' 3 | 4 | export const FORMATS_SD: VideoFormat[] = [VideoFormat.NTSC, VideoFormat.PAL] 5 | export const FORMATS_SD_PROGRESSIVE: VideoFormat[] = [VideoFormat.NTSCp, VideoFormat.PALp] 6 | export const FORMATS_HD_SDI: VideoFormat[] = [ 7 | VideoFormat._720p50, 8 | VideoFormat._720p5994, 9 | VideoFormat._720p60, 10 | VideoFormat._1080p23976, 11 | VideoFormat._1080p24, 12 | VideoFormat._1080p25, 13 | VideoFormat._1080p2997, 14 | VideoFormat._1080p30, 15 | 16 | VideoFormat._1080i50, 17 | VideoFormat._1080i5994, 18 | VideoFormat._1080i60, 19 | ] 20 | export const FORMATS_2K_DCI: VideoFormat[] = [VideoFormat._2Kp23976DCI, VideoFormat._2Kp24DCI, VideoFormat._2Kp25DCI] 21 | export const FORMATS_3G_SDI: VideoFormat[] = [VideoFormat._1080p50, VideoFormat._1080p5994, VideoFormat._1080p60] 22 | 23 | export const FORMATS_4K30: VideoFormat[] = [ 24 | VideoFormat._4Kp23976, 25 | VideoFormat._4Kp24, 26 | VideoFormat._4Kp25, 27 | VideoFormat._4Kp2997, 28 | VideoFormat._4Kp30, 29 | ] 30 | export const FORMATS_4K_DCI: VideoFormat[] = [VideoFormat._4Kp23976DCI, VideoFormat._4Kp24DCI, VideoFormat._4Kp25DCI] 31 | export const FORMATS_4K60: VideoFormat[] = [VideoFormat._4Kp50, VideoFormat._4Kp5994, VideoFormat._4Kp60] 32 | 33 | export const FORMATS_8K: VideoFormat[] = [ 34 | VideoFormat._8Kp23976, 35 | VideoFormat._8Kp24, 36 | VideoFormat._8Kp25, 37 | VideoFormat._8Kp2997, 38 | VideoFormat._8Kp30, 39 | VideoFormat._8Kp50, 40 | VideoFormat._8Kp5994, 41 | VideoFormat._8Kp60, 42 | ] 43 | export const FORMATS_8K_DCI: VideoFormat[] = [VideoFormat._8Kp23976DCI, VideoFormat._8Kp24DCI, VideoFormat._8Kp25DCI] 44 | 45 | export const FORMATS_ALL: VideoFormat[] = [ 46 | ...FORMATS_SD, 47 | ...FORMATS_SD_PROGRESSIVE, 48 | ...FORMATS_HD_SDI, 49 | ...FORMATS_3G_SDI, 50 | // TODO - 2K DCI 51 | ...FORMATS_4K30, 52 | // TODO - 4K DCI 53 | ...FORMATS_4K60, 54 | // TODO - 8K 55 | ] 56 | 57 | export function VideoFormatsToChoices(formats: VideoFormat[]): DropdownChoice[] { 58 | return formats.map((f) => ({ 59 | id: f, 60 | label: f, 61 | })) 62 | } 63 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { Regex, SomeCompanionConfigField } from '@companion-module/base' 2 | import { CONFIG_MODELS } from './models.js' 3 | 4 | const CONFIG_NOTIFICATION_METHOD = [ 5 | { id: 'disabled', label: 'Disabled' }, 6 | { id: 'notifications', label: 'Notifications' }, 7 | { id: 'polling', label: 'Polling' }, 8 | ] 9 | 10 | const CHOICES_MODEL = Object.values(CONFIG_MODELS) 11 | // Sort alphabetical 12 | CHOICES_MODEL.sort((a, b) => { 13 | const x = a.label.toLowerCase() 14 | const y = b.label.toLowerCase() 15 | 16 | return x.localeCompare(y) 17 | }) 18 | 19 | export interface HyperdeckConfig { 20 | bonjourHost?: string 21 | host: string 22 | modelID: string 23 | reel: string 24 | timecodeVariables: 'disabled' | 'notifications' | 'polling' 25 | pollingInterval: number 26 | } 27 | 28 | export function getConfigFields(): SomeCompanionConfigField[] { 29 | return [ 30 | { 31 | type: 'static-text', 32 | id: 'info', 33 | width: 12, 34 | label: 'Warning', 35 | value: 36 | 'Some Hyperdecks only supports 1 connection at any given time. Be sure to disconect any other devices controling it. Remember to press the remote button on the frontpanel of the Hyperdeck to enable remote control.', 37 | }, 38 | { 39 | type: 'bonjour-device', 40 | id: 'bonjourHost', 41 | label: 'Device', 42 | width: 6, 43 | }, 44 | { 45 | type: 'textinput', 46 | id: 'host', 47 | label: 'Target IP', 48 | width: 6, 49 | regex: Regex.IP, 50 | isVisible: (options) => !options['bonjourHost'], 51 | }, 52 | { 53 | type: 'static-text', 54 | id: 'host-filler', 55 | width: 6, 56 | label: '', 57 | value: '', 58 | isVisible: (options) => !!options['bonjourHost'], 59 | }, 60 | { 61 | type: 'dropdown', 62 | id: 'modelID', 63 | label: 'Model', 64 | width: 6, 65 | choices: CHOICES_MODEL, 66 | default: 0, 67 | }, 68 | { 69 | type: 'static-text', 70 | id: 'modelID-filler', 71 | width: 6, 72 | label: '', 73 | value: '', 74 | }, 75 | { 76 | type: 'static-text', 77 | id: 'info', 78 | width: 12, 79 | label: 'Custom Clip Record Naming', 80 | value: 81 | "Companion is able to initiate recordings where the file names use a custom 'Reel-[####]' naming convention. The 'Reel' is a custom name defined below and [####] is auto incremented from '0' by the HyperDeck. This naming is only used when starting records using the 'Record (with custom reel)' action.", 82 | }, 83 | { 84 | type: 'textinput', 85 | id: 'reel', 86 | label: 'Custom Reel', 87 | width: 6, 88 | default: 'A001', 89 | }, 90 | { 91 | type: 'static-text', 92 | id: 'info', 93 | width: 12, 94 | label: 'Displaying Timecode Variable', 95 | value: 96 | 'Timecode variables have to be explicitly enabled by selecting "Notifications" or "Polling". Note that timecode notifications are not supported before hyperdeck firmware V7!', 97 | }, 98 | { 99 | type: 'dropdown', 100 | id: 'timecodeVariables', 101 | label: 'Timecode Variables', 102 | width: 6, 103 | choices: CONFIG_NOTIFICATION_METHOD, 104 | default: 'disabled', 105 | }, 106 | { 107 | type: 'number', 108 | id: 'pollingInterval', 109 | label: 'Polling Interval (in ms)', 110 | width: 6, 111 | min: 15, 112 | max: 10000, 113 | default: 500, 114 | required: true, 115 | }, 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /src/upgrades.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompanionStaticUpgradeProps, 3 | CompanionStaticUpgradeResult, 4 | CompanionStaticUpgradeScript, 5 | CompanionUpgradeContext, 6 | CreateConvertToBooleanFeedbackUpgradeScript, 7 | } from '@companion-module/base' 8 | import { HyperdeckConfig } from './config.js' 9 | 10 | export const upgradeScripts: CompanionStaticUpgradeScript[] = [ 11 | upgradeCombineOldPlayActions, 12 | upgradeTimecodeNotifications, 13 | upgrade126to127, 14 | CreateConvertToBooleanFeedbackUpgradeScript({ 15 | transport_status: true, 16 | transport_clip: true, 17 | transport_slot: true, 18 | slot_status: true, 19 | transport_loop: true, 20 | transport_singleClip: true, 21 | video_input: true, 22 | audio_input: true, 23 | format_ready: true, 24 | }), 25 | upgradeAddFormatToSelectSlot, 26 | ] 27 | 28 | // v1.0.* -> v1.1.0 (combine old play actions) 29 | function upgradeCombineOldPlayActions( 30 | _context: CompanionUpgradeContext, 31 | props: CompanionStaticUpgradeProps 32 | ): CompanionStaticUpgradeResult { 33 | const changes: CompanionStaticUpgradeResult = { 34 | updatedConfig: null, 35 | updatedActions: [], 36 | updatedFeedbacks: [], 37 | } 38 | 39 | for (const action of props.actions) { 40 | if (!action.options) action.options = {} 41 | 42 | switch (action.actionId) { 43 | case 'vplay': 44 | action.options.loop = false 45 | action.options.single = false 46 | action.actionId = 'play' 47 | changes.updatedActions.push(action) 48 | break 49 | case 'vplaysingle': 50 | action.options.loop = false 51 | action.options.single = true 52 | action.actionId = 'play' 53 | changes.updatedActions.push(action) 54 | break 55 | case 'vplayloop': 56 | action.options.loop = true 57 | action.options.single = false 58 | action.actionId = 'play' 59 | changes.updatedActions.push(action) 60 | break 61 | case 'playSingle': 62 | action.options.speed = 100 63 | action.options.loop = false 64 | action.options.single = true 65 | action.actionId = 'play' 66 | changes.updatedActions.push(action) 67 | break 68 | case 'playLoop': 69 | action.options.speed = 100 70 | action.options.loop = true 71 | action.options.single = false 72 | action.actionId = 'play' 73 | changes.updatedActions.push(action) 74 | break 75 | case 'play': 76 | if (action.options.speed === undefined) { 77 | action.options.speed = 100 78 | changes.updatedActions.push(action) 79 | } 80 | if (action.options.loop === undefined) { 81 | action.options.loop = false 82 | changes.updatedActions.push(action) 83 | } 84 | if (action.options.single === undefined) { 85 | action.options.single = false 86 | changes.updatedActions.push(action) 87 | } 88 | break 89 | } 90 | } 91 | 92 | return changes 93 | } 94 | 95 | // v1.1.0 -> v1.2.0 (timecode notifications) 96 | function upgradeTimecodeNotifications( 97 | _context: CompanionUpgradeContext, 98 | props: CompanionStaticUpgradeProps 99 | ): CompanionStaticUpgradeResult { 100 | const changes: CompanionStaticUpgradeResult = { 101 | updatedConfig: null, 102 | updatedActions: [], 103 | updatedFeedbacks: [], 104 | } 105 | 106 | if (props.config) { 107 | const config: HyperdeckConfig & { pollingOn?: boolean } = props.config 108 | if (config.pollingOn !== undefined) { 109 | if (config.pollingOn) { 110 | config.timecodeVariables = 'polling' 111 | } else { 112 | config.timecodeVariables = 'disabled' 113 | } 114 | delete config.pollingOn 115 | 116 | changes.updatedConfig = config 117 | } 118 | } 119 | 120 | return changes 121 | } 122 | 123 | // v1.2.6 -> 1.2.7 (gotoClip (n) bug fix) 124 | function upgrade126to127( 125 | _context: CompanionUpgradeContext, 126 | props: CompanionStaticUpgradeProps 127 | ): CompanionStaticUpgradeResult { 128 | const changes: CompanionStaticUpgradeResult = { 129 | updatedConfig: null, 130 | updatedActions: [], 131 | updatedFeedbacks: [], 132 | } 133 | 134 | for (const action of props.actions) { 135 | if (action.options === undefined) { 136 | action.options = {} 137 | } 138 | // If the clip is not a number, return early as we don't need to change it 139 | if (action.actionId === 'gotoName' && !isNaN(action.options.clip as any)) { 140 | action.actionId = 'gotoN' 141 | changes.updatedActions.push(action) 142 | } 143 | } 144 | 145 | return changes 146 | } 147 | 148 | function upgradeAddFormatToSelectSlot( 149 | _context: CompanionUpgradeContext, 150 | props: CompanionStaticUpgradeProps 151 | ): CompanionStaticUpgradeResult { 152 | const changes: CompanionStaticUpgradeResult = { 153 | updatedConfig: null, 154 | updatedActions: [], 155 | updatedFeedbacks: [], 156 | } 157 | 158 | for (const action of props.actions) { 159 | if (action.options === undefined) { 160 | action.options = {} 161 | } 162 | if (action.actionId === 'select' && !action.options.format) { 163 | action.options.format = 'unchanged' 164 | changes.updatedActions.push(action) 165 | } 166 | } 167 | 168 | return changes 169 | } 170 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | import { AudioInputType, FileFormatFamily, VideoInputType } from './choices.js' 2 | import { 3 | FORMATS_2K_DCI, 4 | FORMATS_3G_SDI, 5 | FORMATS_4K30, 6 | FORMATS_4K60, 7 | FORMATS_4K_DCI, 8 | FORMATS_8K, 9 | FORMATS_8K_DCI, 10 | FORMATS_HD_SDI, 11 | FORMATS_SD, 12 | FORMATS_SD_PROGRESSIVE, 13 | } from './choices/videoFormats.js' 14 | import { VideoFormat } from 'hyperdeck-connection' 15 | 16 | export interface ModelInfo { 17 | id: string 18 | label: string 19 | videoInputs: VideoInputType[] 20 | audioInputs: AudioInputType[] 21 | fileFormats: FileFormatFamily[] 22 | videoFormats: VideoFormat[] 23 | slotLabels: string 24 | maxShuttle: number 25 | hasSeparateInputFormat: boolean 26 | } 27 | 28 | export const CONFIG_MODELS: Record = { 29 | hdStudio: { 30 | // Specs: https://cvp.com/product/ex-showroom-blackmagic_bmd-hyperd-st2_hyper-832138 31 | id: 'hdStudio', 32 | label: 'HyperDeck Studio', 33 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI], 34 | audioInputs: [AudioInputType.Embedded], 35 | fileFormats: [ 36 | FileFormatFamily.Uncompressed, 37 | FileFormatFamily.ProRes, 38 | FileFormatFamily.Proxy, 39 | FileFormatFamily.DNxHD220, 40 | ], 41 | videoFormats: [...FORMATS_SD, ...FORMATS_SD_PROGRESSIVE, ...FORMATS_HD_SDI], 42 | slotLabels: 'SSD2', 43 | maxShuttle: 1600, 44 | hasSeparateInputFormat: false, 45 | }, 46 | hdStudioPro: { 47 | // Specs: https://cvp.com/product/bmd_hyperdeck_studio_pro 48 | id: 'hdStudioPro', 49 | label: 'HyperDeck Studio Pro', 50 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI, VideoInputType.Component], 51 | audioInputs: [AudioInputType.Embedded, AudioInputType.XLR, AudioInputType.RCA], 52 | fileFormats: [ 53 | FileFormatFamily.Uncompressed, 54 | FileFormatFamily.ProRes, 55 | FileFormatFamily.Proxy, 56 | FileFormatFamily.DNxHD220, 57 | ], 58 | videoFormats: [ 59 | ...FORMATS_SD, 60 | ...FORMATS_SD_PROGRESSIVE, 61 | ...FORMATS_HD_SDI, 62 | ...FORMATS_3G_SDI, 63 | ...FORMATS_2K_DCI, 64 | ...FORMATS_4K30, 65 | ], 66 | slotLabels: 'SSD2', 67 | maxShuttle: 1600, 68 | hasSeparateInputFormat: false, 69 | }, 70 | hdStudio12G: { 71 | id: 'hdStudio12G', 72 | label: 'HyperDeck Studio 12G', 73 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI], 74 | audioInputs: [AudioInputType.Embedded], 75 | fileFormats: [ 76 | FileFormatFamily.Uncompressed, 77 | FileFormatFamily.ProRes, 78 | FileFormatFamily.Proxy, 79 | FileFormatFamily.DNx, 80 | FileFormatFamily.DNxHD220, 81 | FileFormatFamily.DNxHR_HQX, 82 | FileFormatFamily.DNxHR_SQ, 83 | FileFormatFamily.DNxHR_LB, 84 | ], 85 | videoFormats: [ 86 | ...FORMATS_SD, 87 | ...FORMATS_SD_PROGRESSIVE, 88 | ...FORMATS_HD_SDI, 89 | ...FORMATS_3G_SDI, 90 | ...FORMATS_2K_DCI, 91 | ...FORMATS_4K30, 92 | ...FORMATS_4K60, 93 | ...FORMATS_4K_DCI, 94 | ], 95 | slotLabels: 'SSD2', 96 | maxShuttle: 1600, 97 | hasSeparateInputFormat: false, 98 | }, 99 | bmdDup4K: { 100 | id: 'bmdDup4K', 101 | label: 'Blackmagic Duplicator 4K', 102 | videoInputs: [VideoInputType.SDI, VideoInputType.Optical], 103 | audioInputs: [AudioInputType.Embedded], 104 | fileFormats: [FileFormatFamily.H264, FileFormatFamily.H265], 105 | videoFormats: [...FORMATS_SD, ...FORMATS_HD_SDI, ...FORMATS_3G_SDI, ...FORMATS_2K_DCI], 106 | slotLabels: 'SD25', //TODO check correct slots 107 | maxShuttle: 100, 108 | hasSeparateInputFormat: false, 109 | }, 110 | hdStudioMini: { 111 | // Specs: https://cvp.com/product/ex-showroom-blackmagic-bmd-hyperd-stm-hyperdeck 112 | id: 'hdStudioMini', 113 | label: 'HyperDeck Studio Mini', 114 | videoInputs: [VideoInputType.SDI], 115 | audioInputs: [AudioInputType.Embedded], 116 | fileFormats: [ 117 | FileFormatFamily.ProRes, 118 | FileFormatFamily.Proxy, 119 | FileFormatFamily.DNx, 120 | FileFormatFamily.DNxHD220, 121 | FileFormatFamily.DNxHR_HQX, 122 | FileFormatFamily.DNxHR_SQ, 123 | FileFormatFamily.DNxHR_LB, 124 | FileFormatFamily.H264, 125 | ], 126 | videoFormats: [...FORMATS_SD, ...FORMATS_HD_SDI, ...FORMATS_3G_SDI], 127 | slotLabels: 'SD2', 128 | maxShuttle: 1600, 129 | hasSeparateInputFormat: false, 130 | }, 131 | hdStudioHDMini: { 132 | id: 'hdStudioHDMini', 133 | label: 'HyperDeck Studio HD Mini', 134 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI], 135 | audioInputs: [AudioInputType.Embedded], 136 | fileFormats: [ 137 | FileFormatFamily.ProRes, 138 | FileFormatFamily.Proxy, 139 | FileFormatFamily.DNx, 140 | FileFormatFamily.DNxHD220, 141 | FileFormatFamily.H264, 142 | ], 143 | videoFormats: [...FORMATS_SD, ...FORMATS_HD_SDI, ...FORMATS_3G_SDI, ...FORMATS_2K_DCI], 144 | slotLabels: 'SD2_USB', 145 | maxShuttle: 5000, 146 | hasSeparateInputFormat: true, 147 | }, 148 | hdStudioHDPlus: { 149 | id: 'hdStudioHDPlus', 150 | label: 'HyperDeck Studio HD Plus', 151 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI], 152 | audioInputs: [AudioInputType.Embedded], 153 | fileFormats: [ 154 | FileFormatFamily.ProRes, 155 | FileFormatFamily.Proxy, 156 | FileFormatFamily.DNxHR_HQX, 157 | FileFormatFamily.DNxHR_SQ, 158 | FileFormatFamily.DNxHR_LB, 159 | FileFormatFamily.H264_SDI, 160 | FileFormatFamily.H264, 161 | ], 162 | videoFormats: [ 163 | ...FORMATS_SD, 164 | ...FORMATS_HD_SDI, 165 | ...FORMATS_3G_SDI, 166 | ...FORMATS_2K_DCI, 167 | ...FORMATS_4K30, 168 | ...FORMATS_4K_DCI, 169 | ], 170 | slotLabels: 'SD2_USB', 171 | maxShuttle: 5000, 172 | hasSeparateInputFormat: true, 173 | }, 174 | hdStudioHDPro: { 175 | id: 'hdStudioHDPro', 176 | label: 'HyperDeck Studio HD Pro', 177 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI], 178 | audioInputs: [AudioInputType.Embedded], 179 | fileFormats: [ 180 | FileFormatFamily.ProRes, 181 | FileFormatFamily.Proxy, 182 | FileFormatFamily.DNxHR_HQX, 183 | FileFormatFamily.DNxHR_SQ, 184 | FileFormatFamily.DNxHR_LB, 185 | FileFormatFamily.H264_SDI, 186 | FileFormatFamily.H264, 187 | ], 188 | videoFormats: [ 189 | ...FORMATS_SD, 190 | ...FORMATS_HD_SDI, 191 | ...FORMATS_3G_SDI, 192 | ...FORMATS_2K_DCI, 193 | ...FORMATS_4K30, 194 | ...FORMATS_4K_DCI, 195 | ], 196 | slotLabels: 'SSD2_SD2_USB', 197 | maxShuttle: 5000, 198 | hasSeparateInputFormat: true, 199 | }, 200 | hdStudio4KPro: { 201 | id: 'hdStudio4KPro', 202 | label: 'HyperDeck Studio 4K Pro', 203 | videoInputs: [VideoInputType.SDI, VideoInputType.HDMI], 204 | audioInputs: [AudioInputType.Embedded], 205 | fileFormats: [ 206 | FileFormatFamily.ProRes, 207 | FileFormatFamily.Proxy, 208 | FileFormatFamily.DNxHR_HQX, 209 | FileFormatFamily.DNxHR_SQ, 210 | FileFormatFamily.DNxHR_LB, 211 | FileFormatFamily.H264_5, 212 | ], 213 | videoFormats: [ 214 | ...FORMATS_SD, 215 | ...FORMATS_HD_SDI, 216 | ...FORMATS_3G_SDI, 217 | ...FORMATS_2K_DCI, 218 | ...FORMATS_4K30, 219 | ...FORMATS_4K60, 220 | ...FORMATS_4K_DCI, 221 | ], 222 | slotLabels: 'SSD2_SD2_USB', 223 | maxShuttle: 5000, 224 | hasSeparateInputFormat: true, 225 | }, 226 | hdExtreme8K: { 227 | id: 'hdExtreme8K', 228 | label: 'HyperDeck Extreme 8K', 229 | videoInputs: [ 230 | VideoInputType.SDI, 231 | VideoInputType.HDMI, 232 | VideoInputType.Component, 233 | VideoInputType.Composite, 234 | VideoInputType.Optical, 235 | ], 236 | audioInputs: [AudioInputType.Embedded, AudioInputType.XLR, AudioInputType.RCA], 237 | fileFormats: [FileFormatFamily.ProRes, FileFormatFamily.H265], 238 | videoFormats: [ 239 | ...FORMATS_SD, 240 | ...FORMATS_HD_SDI, 241 | ...FORMATS_3G_SDI, 242 | ...FORMATS_2K_DCI, 243 | ...FORMATS_4K30, 244 | ...FORMATS_4K60, 245 | ...FORMATS_4K_DCI, 246 | ...FORMATS_8K, 247 | ...FORMATS_8K_DCI, 248 | ], 249 | slotLabels: 'CFAST2_USBNAS', 250 | maxShuttle: 5000, 251 | hasSeparateInputFormat: true, 252 | }, 253 | hdShuttleHD: { 254 | id: 'hdShuttleHD', 255 | label: 'HyperDeck Shuttle HD', 256 | videoInputs: [VideoInputType.HDMI], 257 | audioInputs: [AudioInputType.Embedded], 258 | fileFormats: [ 259 | FileFormatFamily.ProRes, 260 | FileFormatFamily.Proxy, 261 | FileFormatFamily.DNx, 262 | FileFormatFamily.DNxHD220x, 263 | FileFormatFamily.H264, 264 | FileFormatFamily.Teleprompter, 265 | ], 266 | videoFormats: [...FORMATS_SD, ...FORMATS_HD_SDI, ...FORMATS_3G_SDI, ...FORMATS_2K_DCI], 267 | slotLabels: 'SD_USBNAS', 268 | maxShuttle: 5000, 269 | hasSeparateInputFormat: true, // TODO - verify 270 | }, 271 | } 272 | -------------------------------------------------------------------------------- /src/feedbacks.ts: -------------------------------------------------------------------------------- 1 | import { CompanionFeedbackDefinitions, Regex, combineRgb } from '@companion-module/base' 2 | import { 3 | CHOICES_AUDIOCODEC, 4 | CHOICES_AUDIOCHANNELS, 5 | CHOICES_TRANSPORTSTATUS, 6 | CHOICES_SLOTSTATUS, 7 | CHOICES_ENABLEDISABLE, 8 | CHOICES_REMOTESTATUS, 9 | createModelChoices, 10 | createClipsChoice, 11 | } from './choices.js' 12 | import { InstanceBaseExt } from './types.js' 13 | 14 | export function initFeedbacks(self: InstanceBaseExt): CompanionFeedbackDefinitions { 15 | const modelChoices = createModelChoices(self.model) 16 | const clipChoices = createClipsChoice(self) 17 | 18 | const feedbacks: CompanionFeedbackDefinitions = {} 19 | 20 | feedbacks['transport_status'] = { 21 | type: 'boolean', 22 | name: 'Transport status', 23 | description: 'Set feedback based on transport status', 24 | options: [ 25 | { 26 | type: 'dropdown', 27 | label: 'Transport Status', 28 | id: 'status', 29 | choices: CHOICES_TRANSPORTSTATUS, 30 | default: CHOICES_TRANSPORTSTATUS[0].id, 31 | }, 32 | ], 33 | defaultStyle: { 34 | color: combineRgb(255, 255, 255), 35 | bgcolor: combineRgb(0, 0, 0), 36 | }, 37 | callback: ({ options }) => { 38 | return options.status === self.transportInfo.status 39 | }, 40 | } 41 | feedbacks['transport_clip'] = { 42 | type: 'boolean', 43 | name: 'Active clip', 44 | description: 'Set feedback based on the which clip is active', 45 | options: [ 46 | { 47 | type: 'textinput', 48 | label: 'Clip Id', 49 | id: 'clipID', 50 | default: '1', 51 | regex: Regex.SIGNED_NUMBER, 52 | useVariables: { local: true }, 53 | }, 54 | { 55 | type: 'dropdown', 56 | label: 'Slot Id', 57 | id: 'slotID', 58 | choices: [{ id: 'either', label: 'Any' }, ...modelChoices.Slots], 59 | default: 'either', 60 | regex: Regex.SOMETHING, 61 | }, 62 | ], 63 | defaultStyle: { 64 | color: combineRgb(255, 255, 255), 65 | bgcolor: combineRgb(0, 0, 0), 66 | }, 67 | callback: async ({ options }, context) => { 68 | options.clipID = Number(await context.parseVariablesInString(String(options.clipID))) 69 | return ( 70 | (options.slotID == 'either' && options.clipID == self.transportInfo.clipId) || 71 | (options.slotID == self.transportInfo.slotId && options.clipID == self.transportInfo.clipId) 72 | ) 73 | }, 74 | } 75 | feedbacks['transport_clip_name'] = { 76 | type: 'boolean', 77 | name: 'Active Clip (name)', 78 | description: 'Set feedback based on the name of the active clip', 79 | options: [ 80 | { 81 | type: 'dropdown', 82 | label: 'Clip Name - select from list or enter text', 83 | id: 'clipName', 84 | default: '', 85 | choices: clipChoices, 86 | minChoicesForSearch: 0, 87 | allowCustom: true, 88 | }, 89 | { 90 | type: 'dropdown', 91 | label: 'Slot Id', 92 | id: 'slotID', 93 | choices: [{ id: 'either', label: 'Any' }, ...modelChoices.Slots], 94 | default: 'either', 95 | regex: Regex.SOMETHING, 96 | }, 97 | ], 98 | defaultStyle: { 99 | color: combineRgb(255, 255, 255), 100 | bgcolor: combineRgb(255, 0, 0), 101 | }, 102 | callback: ({ options }) => { 103 | return ( 104 | (options.slotID == 'either' && options.clipName == self.transportInfo.clipName) || 105 | (options.slotID == self.transportInfo.slotId && options.clipName == self.transportInfo.clipName) 106 | ) 107 | }, 108 | } 109 | feedbacks['transport_slot'] = { 110 | type: 'boolean', 111 | name: 'Active slot', 112 | description: 'Set feedback based on the which slot is active', 113 | options: [ 114 | { 115 | type: 'textinput', 116 | label: 'Slot Id', 117 | id: 'setting', 118 | default: '1', 119 | regex: Regex.SIGNED_NUMBER, 120 | useVariables: { local: true }, 121 | }, 122 | ], 123 | defaultStyle: { 124 | color: combineRgb(255, 255, 255), 125 | bgcolor: combineRgb(0, 0, 0), 126 | }, 127 | callback: async ({ options }, context) => { 128 | options.setting = Number(await context.parseVariablesInString(String(options.setting))) 129 | return options.setting == self.transportInfo.slotId 130 | }, 131 | } 132 | feedbacks['slot_status'] = { 133 | type: 'boolean', 134 | name: 'Slot/disk status', 135 | description: 'Set feedback based on disk status', 136 | options: [ 137 | { 138 | type: 'dropdown', 139 | label: 'Disk Status', 140 | id: 'status', 141 | choices: CHOICES_SLOTSTATUS, 142 | default: CHOICES_SLOTSTATUS[0].id, 143 | }, 144 | { 145 | type: 'textinput', 146 | label: 'Slot Id', 147 | id: 'slotId', 148 | default: '1', 149 | regex: Regex.SIGNED_NUMBER, 150 | useVariables: { local: true }, 151 | }, 152 | ], 153 | defaultStyle: { 154 | color: combineRgb(255, 255, 255), 155 | bgcolor: combineRgb(0, 0, 0), 156 | }, 157 | callback: async ({ options }, context) => { 158 | options.slotId = Number(await context.parseVariablesInString(String(options.slotId))) 159 | const slot = self.slotInfo[Number(options.slotId)] 160 | return !!slot && slot.status === options.status 161 | }, 162 | } 163 | feedbacks['transport_loop'] = { 164 | type: 'boolean', 165 | name: 'Loop playback', 166 | description: 'Set feedback based on the loop status', 167 | options: [ 168 | { 169 | type: 'dropdown', 170 | label: 'Loop', 171 | id: 'setting', 172 | choices: CHOICES_ENABLEDISABLE, 173 | default: CHOICES_ENABLEDISABLE[0].id, 174 | }, 175 | ], 176 | defaultStyle: { 177 | color: combineRgb(255, 255, 255), 178 | bgcolor: combineRgb(0, 0, 0), 179 | }, 180 | callback: ({ options }) => { 181 | return options.setting === self.transportInfo.loop 182 | }, 183 | } 184 | feedbacks['transport_singleClip'] = { 185 | type: 'boolean', 186 | name: 'Single clip playback', 187 | description: 'Set feedback for single clip playback', 188 | options: [ 189 | { 190 | type: 'dropdown', 191 | label: 'Single clip', 192 | id: 'setting', 193 | choices: CHOICES_ENABLEDISABLE, 194 | default: CHOICES_ENABLEDISABLE[0].id, 195 | }, 196 | ], 197 | defaultStyle: { 198 | color: combineRgb(255, 255, 255), 199 | bgcolor: combineRgb(0, 0, 0), 200 | }, 201 | callback: ({ options }) => { 202 | return options.setting === self.transportInfo.singleClip 203 | }, 204 | } 205 | feedbacks['video_input'] = { 206 | type: 'boolean', 207 | name: 'Video input', 208 | description: 'Set feedback based on selected video input', 209 | options: [ 210 | { 211 | type: 'dropdown', 212 | label: 'Input', 213 | id: 'setting', 214 | choices: modelChoices.VideoInputs, 215 | default: modelChoices.VideoInputs[0]?.id, 216 | }, 217 | ], 218 | defaultStyle: { 219 | color: combineRgb(255, 255, 255), 220 | bgcolor: combineRgb(0, 0, 0), 221 | }, 222 | callback: ({ options }) => { 223 | return options.setting === String(self.deckConfig.videoInput) 224 | }, 225 | } 226 | if (modelChoices.AudioInputs.length > 1) { 227 | feedbacks['audio_input'] = { 228 | type: 'boolean', 229 | name: 'Audio input', 230 | description: 'Set feedback based on selected audio input', 231 | options: [ 232 | { 233 | type: 'dropdown', 234 | label: 'Input', 235 | id: 'setting', 236 | choices: modelChoices.AudioInputs, 237 | default: modelChoices.AudioInputs[0]?.id, 238 | }, 239 | ], 240 | defaultStyle: { 241 | color: combineRgb(255, 255, 255), 242 | bgcolor: combineRgb(0, 0, 0), 243 | }, 244 | callback: ({ options }) => { 245 | return options.setting === self.deckConfig.audioInput 246 | }, 247 | } 248 | } 249 | feedbacks['audio_channels'] = { 250 | type: 'boolean', 251 | name: 'Audio channels', 252 | description: 'Set feedback based on configured audio channels', 253 | options: [ 254 | { 255 | type: 'dropdown', 256 | label: 'Codec', 257 | id: 'audioCodec', 258 | default: CHOICES_AUDIOCODEC[0].id, 259 | choices: CHOICES_AUDIOCODEC, 260 | }, 261 | { 262 | type: 'dropdown', 263 | label: 'Channels', 264 | id: 'audioChannels', 265 | default: '2', 266 | choices: CHOICES_AUDIOCHANNELS, 267 | isVisible: (options) => options.audioCodec === 'PCM', 268 | }, 269 | ], 270 | defaultStyle: { 271 | color: combineRgb(255, 255, 255), 272 | bgcolor: combineRgb(0, 0, 255), 273 | }, 274 | callback: ({ options }) => { 275 | if (options.audioCodec === 'AAC' || self.deckConfig.audioCodec === 'AAC') { 276 | return options.audioCodec === self.deckConfig.audioCodec 277 | } 278 | return options.audioChannels === String(self.deckConfig.audioInputChannels) 279 | }, 280 | } 281 | feedbacks['format_ready'] = { 282 | type: 'boolean', 283 | name: 'Format prepared', 284 | description: 'Set feedback based on a successful Format Prepare action', 285 | options: [], 286 | defaultStyle: { 287 | color: combineRgb(255, 255, 255), 288 | bgcolor: combineRgb(255, 0, 0), 289 | }, 290 | callback: ({}) => { 291 | return self.formatToken !== null 292 | }, 293 | } 294 | feedbacks['remote_status'] = { 295 | type: 'boolean', 296 | name: 'Remote Status', 297 | description: 'Set feedback based on the remote control status', 298 | options: [ 299 | { 300 | type: 'dropdown', 301 | label: 'Status', 302 | id: 'status', 303 | default: CHOICES_REMOTESTATUS[0].id, 304 | choices: CHOICES_REMOTESTATUS, 305 | }, 306 | ], 307 | defaultStyle: { 308 | color: combineRgb(255, 255, 255), 309 | bgcolor: combineRgb(0, 0, 255), 310 | }, 311 | callback: ({ options }) => { 312 | self.log('debug', `FEEDBACK: ${options.status} ${self.remoteInfo}`) 313 | return options.status === self.remoteInfo?.['enabled'] 314 | }, 315 | } 316 | 317 | return feedbacks 318 | } 319 | -------------------------------------------------------------------------------- /src/choices.ts: -------------------------------------------------------------------------------- 1 | import { DropdownChoice, DropdownChoiceId } from '@companion-module/base' 2 | import { ModelInfo } from './models.js' 3 | import { ClipDropdownChoice, InstanceBaseExt } from './types.js' 4 | import { stripExtension } from './util.js' 5 | import { VideoFormatsToChoices } from './choices/videoFormats.js' 6 | 7 | export const DropdownChoiceTrue: DropdownChoiceId = true as unknown as DropdownChoiceId 8 | export const DropdownChoiceFalse: DropdownChoiceId = false as unknown as DropdownChoiceId 9 | 10 | export const CHOICES_STARTEND: DropdownChoice[] = [ 11 | { id: 'start', label: 'Start' }, 12 | { id: 'end', label: 'End' }, 13 | ] 14 | 15 | export const CHOICES_PREVIEWMODE: DropdownChoice[] = [ 16 | { id: 'true', label: 'Preview' }, 17 | { id: 'false', label: 'Output' }, 18 | ] 19 | 20 | export const CHOICES_AUDIOCODEC: DropdownChoice[] = [ 21 | { id: 'PCM', label: 'PCM' }, 22 | { id: 'AAC', label: 'AAC (2 channels only)' }, 23 | ] 24 | 25 | export const CHOICES_AUDIOCHANNELS: DropdownChoice[] = [ 26 | { id: '2', label: '2 Channels' }, 27 | { id: '4', label: '4 Channels' }, 28 | { id: '8', label: '8 Channels' }, 29 | { id: '16', label: '16 Channels' }, 30 | { id: 'cycle', label: 'Cycle' }, 31 | ] 32 | 33 | export const CHOICES_DYNAMICRANGE: DropdownChoice[] = [ 34 | { id: 'auto', label: 'Auto' }, 35 | { id: 'Rec709', label: 'Rec.709' }, 36 | { id: 'Rec2020_SDR', label: 'Rec.2020 SDR' }, 37 | { id: 'HLG', label: 'HLG' }, 38 | { id: 'ST2084_300', label: 'ST2084 300' }, 39 | { id: 'ST2084_500', label: 'ST2084 500' }, 40 | { id: 'ST2084_800', label: 'ST2084 800' }, 41 | { id: 'ST2084_1000', label: 'ST2084 1000 ' }, 42 | { id: 'ST2084_2000', label: 'ST2084 2000' }, 43 | { id: 'ST2084_4000', label: 'ST2084 4000' }, 44 | { id: 'ST2048', label: 'ST2048' }, 45 | ] 46 | 47 | export const CHOICES_FILESYSTEM: DropdownChoice[] = [ 48 | { id: 'HFS+', label: 'HFS+' }, 49 | { id: 'exFAT', label: 'exFAT' }, 50 | ] 51 | 52 | export const CHOICES_REMOTECONTROL: DropdownChoice[] = [ 53 | { id: 'toggle', label: 'Toggle' }, 54 | { id: DropdownChoiceTrue, label: 'Enable' }, 55 | { id: DropdownChoiceFalse, label: 'Disable' }, 56 | ] 57 | 58 | export const CHOICES_TRANSPORTSTATUS: DropdownChoice[] = [ 59 | { id: 'preview', label: 'Preview' }, 60 | { id: 'stopped', label: 'Stopped' }, 61 | { id: 'play', label: 'Playing' }, 62 | { id: 'forward', label: 'Forward' }, 63 | { id: 'rewind', label: 'Rewind' }, 64 | { id: 'jog', label: 'Jog' }, 65 | { id: 'shuttle', label: 'Shuttle' }, 66 | { id: 'record', label: 'Record' }, 67 | ] 68 | 69 | export const CHOICES_SLOTSTATUS: DropdownChoice[] = [ 70 | { id: 'empty', label: 'Empty' }, 71 | { id: 'error', label: 'Error' }, 72 | { id: 'mounted', label: 'Mounted' }, 73 | { id: 'mounting', label: 'Mounting' }, 74 | ] 75 | 76 | export const CHOICES_ENABLEDISABLE: DropdownChoice[] = [ 77 | { id: DropdownChoiceTrue, label: 'Enable' }, 78 | { id: DropdownChoiceFalse, label: 'Disable' }, 79 | ] 80 | 81 | export const CHOICES_REMOTESTATUS: DropdownChoice[] = [ 82 | { id: DropdownChoiceTrue, label: 'Enabled' }, 83 | { id: DropdownChoiceFalse, label: 'Disabled' }, 84 | ] 85 | 86 | export enum AudioInputType { 87 | Embedded = 'embedded', 88 | XLR = 'XLR', 89 | RCA = 'RCA', 90 | } 91 | 92 | const CONFIG_AUDIOINPUTS: Record = { 93 | embedded: { id: AudioInputType.Embedded, label: 'Embedded' }, 94 | XLR: { id: AudioInputType.XLR, label: 'XLR' }, 95 | RCA: { id: AudioInputType.RCA, label: 'RCA' }, 96 | } 97 | 98 | export enum VideoInputType { 99 | SDI = 'SDI', 100 | HDMI = 'HDMI', 101 | Component = 'component', 102 | Composite = 'composite', 103 | Optical = 'optical', 104 | } 105 | 106 | const CONFIG_VIDEOINPUTS: Record = { 107 | SDI: { id: VideoInputType.SDI, label: 'SDI' }, 108 | HDMI: { id: VideoInputType.HDMI, label: 'HDMI' }, 109 | component: { id: VideoInputType.Component, label: 'Component' }, 110 | composite: { id: VideoInputType.Composite, label: 'Composite' }, 111 | optical: { id: VideoInputType.Optical, label: 'Optical' }, 112 | } 113 | 114 | export interface FormatDropdownChoice extends DropdownChoice { 115 | family: FileFormatFamily 116 | } 117 | 118 | export enum FileFormatFamily { 119 | Uncompressed = 'uncompressed', 120 | ProRes = 'prores', 121 | Proxy = 'proxy', 122 | DNx = 'DNx', 123 | DNxHD220 = 'DNxHD220', 124 | DNxHD220x = 'DNxHD220x', 125 | DNxHR_HQX = 'DNxHR_HQX', 126 | DNxHR_SQ = 'DNxHR_SQ', 127 | DNxHR_LB = 'DNxHR_LB', 128 | H264_SDI = 'H.264_SDI', 129 | H264 = 'H.264', 130 | H265 = 'H.265', 131 | H264_5 = 'H.264/5', 132 | Teleprompter = 'teleprompter', 133 | } 134 | 135 | export const CONFIG_FILEFORMATS: FormatDropdownChoice[] = [ 136 | { id: 'QuickTimeUncompressed', label: 'QuickTime Uncompressed', family: FileFormatFamily.Uncompressed }, 137 | { id: 'QuickTimeProResHQ', label: 'QuickTime ProRes HQ', family: FileFormatFamily.ProRes }, 138 | { id: 'QuickTimeProRes', label: 'QuickTime ProRes', family: FileFormatFamily.ProRes }, 139 | { id: 'QuickTimeProResLT', label: 'QuickTime ProRes LT', family: FileFormatFamily.ProRes }, 140 | { id: 'QuickTimeProResProxy', label: 'QuickTime ProRes Proxy', family: FileFormatFamily.Proxy }, 141 | { id: 'QuickTimeDNxHD45', label: 'DNxHD 45 QT', family: FileFormatFamily.DNx }, 142 | { id: 'DNxHD45', label: 'DNxHD 45 MXF', family: FileFormatFamily.DNx }, 143 | { id: 'QuickTimeDNxHD145', label: 'DNxHD 145 QT', family: FileFormatFamily.DNx }, 144 | { id: 'DNxHD145', label: 'DNxHD 145 MXF', family: FileFormatFamily.DNx }, 145 | { id: 'QuickTimeDNxHD220', label: 'DNxHD 220 QT', family: FileFormatFamily.DNxHD220 }, 146 | { id: 'DNxHD220', label: 'MXF DNxHD 220', family: FileFormatFamily.DNxHD220 }, 147 | { id: 'QuickTimeDNxHD220x', label: 'DNxHD 220x QT', family: FileFormatFamily.DNxHD220x }, 148 | { id: 'DNxHD220x', label: 'MXF DNxHD 220x', family: FileFormatFamily.DNxHD220x }, 149 | { id: 'QuickTimeDNxHR_HQX', label: 'DNxHR HQX QT', family: FileFormatFamily.DNxHR_HQX }, 150 | { id: 'DNxHR_HQX', label: 'DNxHR HQX MXF', family: FileFormatFamily.DNxHR_HQX }, 151 | { id: 'QuickTimeDNxHR_SQ', label: 'DNxHR SQ QT', family: FileFormatFamily.DNxHR_SQ }, 152 | { id: 'DNxHR_SQ', label: 'DNxHR SQ MXF', family: FileFormatFamily.DNxHR_SQ }, 153 | { id: 'QuickTimeDNxHR_LB', label: 'DNxHR LB QT', family: FileFormatFamily.DNxHR_LB }, 154 | { id: 'DNxHR_LB', label: 'DNxHR LB MXF', family: FileFormatFamily.DNxHR_LB }, 155 | { id: 'H.264High10_422', label: 'H.264 SDI', family: FileFormatFamily.H264_SDI }, 156 | { id: 'H.264High', label: 'H.264 High', family: FileFormatFamily.H264 }, 157 | { id: 'H.264Medium', label: 'H.264 Medium', family: FileFormatFamily.H264 }, 158 | { id: 'H.264Low', label: 'H.264 Low', family: FileFormatFamily.H264 }, 159 | { id: 'H.265High', label: 'H.265 High', family: FileFormatFamily.H265 }, 160 | { id: 'H.265Medium', label: 'H.265 Medium', family: FileFormatFamily.H265 }, 161 | { id: 'H.265Low', label: 'H.265 Low', family: FileFormatFamily.H265 }, 162 | { id: 'H.264High10_422', label: 'H.264/5 SDI', family: FileFormatFamily.H264_5 }, 163 | { id: 'H.264High', label: 'H.264/5 High', family: FileFormatFamily.H264_5 }, 164 | { id: 'H.264Medium', label: 'H.264/5 Medium', family: FileFormatFamily.H264_5 }, 165 | { id: 'H.264Low', label: 'H.264/5 Low', family: FileFormatFamily.H264_5 }, 166 | { id: 'Teleprompter', label: 'Teleprompter', family: FileFormatFamily.Teleprompter }, 167 | ] 168 | 169 | const CONFIG_SLOT_LABELS: Record = { 170 | SSD2: [ 171 | { id: 1, label: '1: SSD 1' }, 172 | { id: 2, label: '2: SSD 2' }, 173 | ], 174 | SD2: [ 175 | { id: 1, label: '1: SD 1' }, 176 | { id: 2, label: '2: SD 2' }, 177 | ], 178 | SD_USB: [ 179 | { id: 1, label: '1: SD' }, 180 | { id: 2, label: '2: USB-C' }, 181 | ], 182 | SD_USBNAS: [ 183 | { id: 1, label: '1: SD' }, 184 | { id: 2, label: '2: USB-C/NAS' }, 185 | ], 186 | SD2_USB: [ 187 | { id: 1, label: '1: SD 1' }, 188 | { id: 2, label: '2: SD 2' }, 189 | { id: 3, label: '3: USB-C' }, 190 | ], 191 | CFAST2_USBNAS: [ 192 | { id: 1, label: '1: CFast 1' }, 193 | { id: 2, label: '2: CFast 2' }, 194 | { id: 3, label: '3: USB-C/NAS' }, 195 | ], 196 | SSD2_SD2_USB: [ 197 | { id: 1, label: '1: SSD 1' }, 198 | { id: 2, label: '2: SSD 2' }, 199 | { id: 3, label: '3: USB-C' }, 200 | { id: 4, label: '4: SD 1' }, 201 | { id: 5, label: '5: SD 2' }, 202 | ], 203 | SD25: [ 204 | { id: 1, label: '1: SD 1' }, 205 | { id: 2, label: '2: SD 2' }, 206 | { id: 3, label: '3: SD 3' }, 207 | { id: 4, label: '4: SD 4' }, 208 | { id: 5, label: '5: SD 5' }, 209 | { id: 6, label: '6: SD 6' }, 210 | { id: 7, label: '7: SD 7' }, 211 | { id: 8, label: '8: SD 8' }, 212 | { id: 9, label: '9: SD 9' }, 213 | { id: 10, label: '10: SD 10' }, 214 | { id: 11, label: '11: SD 11' }, 215 | { id: 12, label: '12: SD 12' }, 216 | { id: 13, label: '13: SD 13' }, 217 | { id: 14, label: '14: SD 14' }, 218 | { id: 15, label: '15: SD 15' }, 219 | { id: 16, label: '16: SD 16' }, 220 | { id: 17, label: '17: SD 17' }, 221 | { id: 18, label: '18: SD 18' }, 222 | { id: 19, label: '19: SD 19' }, 223 | { id: 20, label: '20: SD 20' }, 224 | { id: 21, label: '21: SD 21' }, 225 | { id: 22, label: '22: SD 22' }, 226 | { id: 23, label: '23: SD 23' }, 227 | { id: 24, label: '24: SD 24' }, 228 | { id: 25, label: '25: SD 25' }, 229 | ], 230 | } 231 | 232 | export interface ModelChoices { 233 | AudioInputs: DropdownChoice[] 234 | VideoInputs: DropdownChoice[] 235 | Slots: DropdownChoice[] 236 | FileFormats: FormatDropdownChoice[] 237 | VideoFormats: DropdownChoice[] 238 | Clips: ClipDropdownChoice[] 239 | } 240 | 241 | function createDefaultChoice(id: string): DropdownChoice { 242 | return { 243 | id, 244 | label: id, 245 | } 246 | } 247 | export function createModelChoices(model: ModelInfo | undefined) { 248 | const result: ModelChoices = { 249 | AudioInputs: [], 250 | VideoInputs: [], 251 | Slots: [], 252 | FileFormats: [], 253 | VideoFormats: [], 254 | Clips: [], 255 | } 256 | 257 | if (model) { 258 | for (const inputId of model.audioInputs) { 259 | result.AudioInputs.push(CONFIG_AUDIOINPUTS[inputId] ?? createDefaultChoice(inputId)) 260 | } 261 | 262 | for (const format of CONFIG_FILEFORMATS) { 263 | if (model.fileFormats.includes(format.family)) { 264 | result.FileFormats.push(format) 265 | } 266 | } 267 | 268 | for (const inputId of model.videoInputs) { 269 | result.VideoInputs.push(CONFIG_VIDEOINPUTS[inputId] ?? createDefaultChoice(inputId)) 270 | } 271 | 272 | result.VideoFormats.push(...VideoFormatsToChoices(model.videoFormats)) 273 | 274 | //TODO define CHOICES_SLOTS based on model 275 | result.Slots = CONFIG_SLOT_LABELS[model.slotLabels] ?? [] 276 | } 277 | 278 | return result 279 | } 280 | 281 | export function createClipsChoice(instance: InstanceBaseExt) { 282 | const clips: ClipDropdownChoice[] = [] 283 | 284 | for (const { clipId, name } of instance.simpleClipsList) { 285 | clips.push({ id: name, label: stripExtension(name), clipId: clipId }) 286 | } 287 | 288 | return clips 289 | } 290 | -------------------------------------------------------------------------------- /src/variables.ts: -------------------------------------------------------------------------------- 1 | import Timecode from 'smpte-timecode' 2 | import { SlotStatus, VideoFormat } from 'hyperdeck-connection' 3 | import { CompanionVariableDefinition, CompanionVariableValues } from '@companion-module/base' 4 | import { InstanceBaseExt } from './types.js' 5 | import { stripExtension, toHHMMSS } from './util.js' 6 | import { CONFIG_FILEFORMATS } from './choices.js' 7 | 8 | const frameRates: { [k in VideoFormat]: Timecode.FRAMERATE } = { 9 | [VideoFormat.NTSC]: 29.97, 10 | [VideoFormat.PAL]: 25, 11 | [VideoFormat.NTSCp]: 29.97, 12 | [VideoFormat.PALp]: 25, 13 | 14 | [VideoFormat._720p50]: 50, 15 | [VideoFormat._720p5994]: 59.94, 16 | [VideoFormat._720p60]: 60, 17 | [VideoFormat._1080p23976]: 23.976, 18 | [VideoFormat._1080p24]: 24, 19 | [VideoFormat._1080p25]: 25, 20 | [VideoFormat._1080p2997]: 29.97, 21 | [VideoFormat._1080p30]: 30, 22 | [VideoFormat._1080i50]: 25, 23 | [VideoFormat._1080i5994]: 29.97, 24 | [VideoFormat._1080i60]: 30, 25 | 26 | [VideoFormat._1080p50]: 50, 27 | [VideoFormat._1080p5994]: 59.94, 28 | [VideoFormat._1080p60]: 60, 29 | 30 | [VideoFormat._2Kp23976DCI]: 23.976, 31 | [VideoFormat._2Kp24DCI]: 24, 32 | [VideoFormat._2Kp25DCI]: 25, 33 | 34 | [VideoFormat._4Kp23976]: 23.976, 35 | [VideoFormat._4Kp24]: 24, 36 | [VideoFormat._4Kp25]: 25, 37 | [VideoFormat._4Kp2997]: 29.97, 38 | [VideoFormat._4Kp30]: 30, 39 | 40 | [VideoFormat._4Kp50]: 50, 41 | [VideoFormat._4Kp5994]: 59.94, 42 | [VideoFormat._4Kp60]: 60, 43 | 44 | [VideoFormat._4Kp23976DCI]: 23.976, 45 | [VideoFormat._4Kp24DCI]: 24, 46 | [VideoFormat._4Kp25DCI]: 25, 47 | 48 | [VideoFormat._8Kp23976]: 23.976, 49 | [VideoFormat._8Kp24]: 24, 50 | [VideoFormat._8Kp25]: 25, 51 | [VideoFormat._8Kp2997]: 29.97, 52 | [VideoFormat._8Kp30]: 30, 53 | 54 | [VideoFormat._8Kp50]: 50, 55 | [VideoFormat._8Kp5994]: 59.94, 56 | [VideoFormat._8Kp60]: 60, 57 | 58 | [VideoFormat._8Kp23976DCI]: 23.976, 59 | [VideoFormat._8Kp24DCI]: 24, 60 | [VideoFormat._8Kp25DCI]: 25, 61 | } 62 | 63 | export function updateTransportInfoVariables(instance: InstanceBaseExt, newValues: CompanionVariableValues) { 64 | const capitalise = (s: string) => { 65 | if (typeof s !== 'string') return '' 66 | return s.charAt(0).toUpperCase() + s.slice(1) 67 | } 68 | newValues['status'] = capitalise(instance.transportInfo.status) 69 | newValues['speed'] = instance.transportInfo.speed 70 | 71 | //Clip ID and Slot ID null exceptions 72 | let clipNameVariable: string | undefined 73 | if (instance.transportInfo.clipId !== null) { 74 | const clipObj = instance.simpleClipsList.find(({ clipId }) => clipId == instance.transportInfo.clipId) 75 | if (clipObj) { 76 | clipNameVariable = stripExtension(clipObj.name) 77 | } 78 | } 79 | 80 | newValues['clipId'] = instance.transportInfo.clipId ?? '-' 81 | newValues['clipName'] = clipNameVariable ?? '-' 82 | newValues['clipCount'] = instance.simpleClipsList.length 83 | newValues['slotId'] = instance.transportInfo.slotId ?? '-' 84 | newValues['videoFormat'] = instance.transportInfo.videoFormat ?? 'none' 85 | if (instance.model.hasSeparateInputFormat) { 86 | newValues['inputVideoFormat'] = instance.transportInfo.inputVideoFormat ?? 'none' 87 | } 88 | } 89 | 90 | export function updateClipVariables(instance: InstanceBaseExt, newValues: CompanionVariableValues) { 91 | newValues['clipCount'] = instance.simpleClipsList.length 92 | // Variables for every clip in the list 93 | for (const { clipId, name } of instance.simpleClipsList) { 94 | newValues[`clip${clipId}_name`] = stripExtension(name) 95 | } 96 | } 97 | 98 | export function updateSlotInfoVariables(instance: InstanceBaseExt, newValues: CompanionVariableValues) { 99 | const activeSlotId = instance.transportInfo.slotId 100 | instance.slotInfo.forEach((slot, index) => { 101 | if (!slot) return 102 | 103 | let recordingTime = '--:--:--' 104 | if (slot.status === SlotStatus.MOUNTED) { 105 | try { 106 | if (typeof slot.recordingTime === 'number') { 107 | recordingTime = toHHMMSS(slot.recordingTime) 108 | instance.log('debug', `Slot ${index} recording time: ${slot.recordingTime} secs -> ${recordingTime}`) 109 | } 110 | } catch (e) { 111 | instance.log('error', `Slot ${index} recording time parse error: ${e}`) 112 | } 113 | } 114 | 115 | newValues[`slot${index}_recordingTime`] = recordingTime 116 | if (slot.slotId === activeSlotId) { 117 | newValues['recordingTime'] = recordingTime 118 | } 119 | }) 120 | } 121 | 122 | interface CounterValues { 123 | tcH: string | number 124 | tcM: string | number 125 | tcS: string | number 126 | tcF: string | number 127 | tcHMS: string 128 | tcHMSF: string 129 | } 130 | 131 | export function updateTimecodeVariables(instance: InstanceBaseExt, newValues: CompanionVariableValues) { 132 | const tb = instance.transportInfo.videoFormat && frameRates[instance.transportInfo.videoFormat] 133 | const countUp: CounterValues = { 134 | tcH: '--', 135 | tcM: '--', 136 | tcS: '--', 137 | tcF: '--', 138 | tcHMS: '--:--:--', 139 | tcHMSF: '--:--:--:--', 140 | } 141 | let countDown: CounterValues = { 142 | tcH: '--', 143 | tcM: '--', 144 | tcS: '--', 145 | tcF: '--', 146 | tcHMS: '--:--:--', 147 | tcHMSF: '--:--:--:--', 148 | } 149 | 150 | const pad = (n: number | string) => ('00' + n).substr(-2) 151 | 152 | const setTcVariable = (isCountdown: boolean, { tcH, tcM, tcS, tcF, tcHMS, tcHMSF }: CounterValues) => { 153 | newValues[(isCountdown ? 'countdownT' : 't') + 'imecodeHMS'] = tcHMS 154 | newValues[(isCountdown ? 'countdownT' : 't') + 'imecodeHMSF'] = tcHMSF 155 | newValues[(isCountdown ? 'countdownT' : 't') + 'imecodeH'] = pad(tcH) 156 | newValues[(isCountdown ? 'countdownT' : 't') + 'imecodeM'] = pad(tcM) 157 | newValues[(isCountdown ? 'countdownT' : 't') + 'imecodeS'] = pad(tcS) 158 | newValues[(isCountdown ? 'countdownT' : 't') + 'imecodeF'] = pad(tcF) 159 | } 160 | 161 | if (instance.transportInfo.displayTimecode) { 162 | if (tb) { 163 | try { 164 | let tc = Timecode(instance.transportInfo.displayTimecode, tb) 165 | countUp.tcH = tc.hours 166 | countUp.tcM = tc.minutes 167 | countUp.tcS = tc.seconds 168 | countUp.tcF = tc.frames 169 | countUp.tcHMS = tc.toString().substr(0, 8) 170 | countUp.tcHMSF = tc.toString() 171 | 172 | if (instance.transportInfo.slotId !== undefined) { 173 | const clip = instance.fullClipsList.find(({ clipId }) => clipId == instance.transportInfo.clipId) 174 | // instance.debug('Clip duration: ', clip.duration) 175 | const modesWhereCountdownMakesNoSense = new Set(['preview', 'record']) 176 | if (clip && clip.duration && !modesWhereCountdownMakesNoSense.has(instance.transportInfo.status)) { 177 | const tcTot = Timecode(clip.duration, tb) 178 | const tcStart = Timecode(clip.startTime, tb) 179 | const left = Math.max(0, tcTot.frameCount - (tc.frameCount - tcStart.frameCount) - 1) 180 | const tcLeft = Timecode(left, tb) // todo - unhardcode 181 | 182 | countDown.tcH = tcLeft.hours 183 | countDown.tcM = tcLeft.minutes 184 | countDown.tcS = tcLeft.seconds 185 | countDown.tcF = tcLeft.frames 186 | countDown.tcHMS = tcLeft.toString().substr(0, 8) 187 | countDown.tcHMSF = tcLeft.toString() 188 | } 189 | } 190 | } catch (err) { 191 | console.log(instance.transportInfo.displayTimecode) 192 | instance.log('error', 'Timecode error:' + JSON.stringify(err) + (err as any)?.message + err?.toString()) 193 | } 194 | } else { 195 | // no timebase implies we can't use smpte-timecode lib 196 | let tc = instance.transportInfo.displayTimecode.match( 197 | /^(?(?\d{2}):(?\d{2}):(?\d{2}))[:;](?\d{2})$/ 198 | ) 199 | if (tc && tc.groups) { 200 | countUp.tcH = tc.groups.H 201 | countUp.tcM = tc.groups.M 202 | countUp.tcS = tc.groups.S 203 | countUp.tcF = tc.groups.F 204 | countUp.tcHMS = tc.groups.HMS 205 | } 206 | countUp.tcHMSF = instance.transportInfo.displayTimecode 207 | } 208 | } 209 | 210 | setTcVariable(false, countUp) 211 | setTcVariable(true, countDown) 212 | } 213 | 214 | export function updateConfigurationVariables(instance: InstanceBaseExt, newValues: CompanionVariableValues) { 215 | const format = CONFIG_FILEFORMATS.find(({ id }) => id === instance.deckConfig.fileFormat) 216 | newValues['fileFormat'] = format?.label ?? instance.deckConfig.fileFormat 217 | 218 | newValues['audioCodec'] = instance.deckConfig.audioCodec 219 | newValues['audioChannels'] = instance.deckConfig.audioInputChannels 220 | } 221 | 222 | export function updateRemoteVariable(instance: InstanceBaseExt, newValues: CompanionVariableValues) { 223 | newValues['remoteEnabled'] = instance.remoteInfo?.enabled || false 224 | } 225 | 226 | export function initVariables(instance: InstanceBaseExt) { 227 | const variables: CompanionVariableDefinition[] = [] 228 | 229 | const values: CompanionVariableValues = {} 230 | 231 | // transport info vars: 232 | variables.push({ 233 | name: 'Transport status', 234 | variableId: 'status', 235 | }) 236 | variables.push({ 237 | name: 'Play speed', 238 | variableId: 'speed', 239 | }) 240 | variables.push({ 241 | name: 'Clip ID', 242 | variableId: 'clipId', 243 | }) 244 | variables.push({ 245 | name: 'Clip Name', 246 | variableId: 'clipName', 247 | }) 248 | variables.push({ 249 | name: 'Slot ID', 250 | variableId: 'slotId', 251 | }) 252 | variables.push({ 253 | name: 'Video format', 254 | variableId: 'videoFormat', 255 | }) 256 | if (instance.model.hasSeparateInputFormat) { 257 | variables.push({ 258 | name: 'Input video format', 259 | variableId: 'inputVideoFormat', 260 | }) 261 | } 262 | updateTransportInfoVariables(instance, values) 263 | 264 | // Slot variables 265 | variables.push({ 266 | name: 'Active slot recording time available', 267 | variableId: 'recordingTime', 268 | }) 269 | instance.slotInfo.forEach((slot, index) => { 270 | if (slot != null) { 271 | variables.push({ 272 | name: `Slot ${index} recording time available`, 273 | variableId: `slot${index}_recordingTime`, 274 | }) 275 | } 276 | }) 277 | updateSlotInfoVariables(instance, values) 278 | 279 | // Clip variables 280 | variables.push({ 281 | name: 'Clip count', 282 | variableId: 'clipCount', 283 | }) 284 | for (const { clipId } of instance.simpleClipsList) { 285 | variables.push({ 286 | name: `Clip ${clipId} Name`, 287 | variableId: `clip${clipId}_name`, 288 | }) 289 | } 290 | updateClipVariables(instance, values) 291 | 292 | // Configuration variables 293 | variables.push({ 294 | name: 'File format', 295 | variableId: 'fileFormat', 296 | }) 297 | variables.push({ 298 | name: 'Audio codec', 299 | variableId: 'audioCodec', 300 | }) 301 | variables.push({ 302 | name: 'Audio channels', 303 | variableId: 'audioChannels', 304 | }) 305 | updateConfigurationVariables(instance, values) 306 | 307 | // Remote status 308 | variables.push({ 309 | name: 'Remote enabled', 310 | variableId: 'remoteEnabled', 311 | }) 312 | 313 | // Timecode variables 314 | const initTcVariable = (isCountdown: boolean) => { 315 | variables.push({ 316 | name: (isCountdown ? 'Countdown ' : '') + 'Timecode (HH:MM:SS)', 317 | variableId: (isCountdown ? 'countdownT' : 't') + 'imecodeHMS', 318 | }) 319 | 320 | variables.push({ 321 | name: (isCountdown ? 'Countdown ' : '') + 'Timecode (HH:MM:SS:FF)', 322 | variableId: (isCountdown ? 'countdownT' : 't') + 'imecodeHMSF', 323 | }) 324 | 325 | variables.push({ 326 | name: (isCountdown ? 'Countdown ' : '') + 'Timecode (HH)', 327 | variableId: (isCountdown ? 'countdownT' : 't') + 'imecodeH', 328 | }) 329 | 330 | variables.push({ 331 | name: (isCountdown ? 'Countdown ' : '') + 'Timecode (MM)', 332 | variableId: (isCountdown ? 'countdownT' : 't') + 'imecodeM', 333 | }) 334 | 335 | variables.push({ 336 | name: (isCountdown ? 'Countdown ' : '') + 'Timecode (SS)', 337 | variableId: (isCountdown ? 'countdownT' : 't') + 'imecodeS', 338 | }) 339 | 340 | variables.push({ 341 | name: (isCountdown ? 'Countdown ' : '') + 'Timecode (FF)', 342 | variableId: (isCountdown ? 'countdownT' : 't') + 'imecodeF', 343 | }) 344 | } 345 | initTcVariable(false) 346 | initTcVariable(true) 347 | 348 | updateTimecodeVariables(instance, values) 349 | 350 | instance.setVariableDefinitions(variables) 351 | instance.setVariableValues(values) 352 | } 353 | -------------------------------------------------------------------------------- /src/presets.ts: -------------------------------------------------------------------------------- 1 | import { CompanionPresetDefinitions, combineRgb } from '@companion-module/base' 2 | import { InstanceBaseExt } from './types.js' 3 | 4 | // Define colors for different categories 5 | const colors = { 6 | white: combineRgb(255, 255, 255), 7 | black: combineRgb(0, 0, 0), 8 | red: combineRgb(255, 0, 0), 9 | green: combineRgb(0, 200, 0), 10 | blue: combineRgb(0, 80, 255), 11 | yellow: combineRgb(255, 255, 0), 12 | orange: combineRgb(255, 102, 0), 13 | purple: combineRgb(128, 0, 128), 14 | teal: combineRgb(0, 128, 128), 15 | lightBlue: combineRgb(0, 176, 255), 16 | grey: combineRgb(128, 128, 128), 17 | darkBlue: combineRgb(0, 0, 100), 18 | } 19 | 20 | export function initPresets(self: InstanceBaseExt): CompanionPresetDefinitions { 21 | self.log('info', 'Initializing Presets') 22 | 23 | const presets: CompanionPresetDefinitions = {} 24 | 25 | presets.play = { 26 | type: 'button', 27 | category: 'Transport', 28 | name: 'Play', 29 | style: { 30 | text: '▶\nPlay', 31 | size: '14', 32 | color: colors.white, 33 | bgcolor: colors.black, 34 | }, 35 | feedbacks: [ 36 | { 37 | feedbackId: 'transport_status', 38 | options: { status: 'play' }, 39 | style: { 40 | color: colors.white, 41 | bgcolor: colors.green, 42 | }, 43 | }, 44 | ], 45 | steps: [ 46 | { 47 | down: [{ actionId: 'play', options: { speed: 100, loop: false, single: false, useVariable: false } }], 48 | up: [], 49 | }, 50 | ], 51 | } 52 | 53 | presets.rec = { 54 | type: 'button', 55 | category: 'Transport', 56 | name: 'Record', 57 | style: { 58 | text: '●\nRec', 59 | size: '14', 60 | color: colors.white, 61 | bgcolor: colors.black, 62 | }, 63 | feedbacks: [ 64 | { 65 | feedbackId: 'transport_status', 66 | options: { status: 'record' }, 67 | style: { 68 | color: colors.white, 69 | bgcolor: colors.red, 70 | }, 71 | }, 72 | ], 73 | steps: [ 74 | { 75 | down: [{ actionId: 'rec', options: {} }], 76 | up: [], 77 | }, 78 | ], 79 | } 80 | 81 | presets.spill = { 82 | type: 'button', 83 | category: 'Transport', 84 | name: 'Spill', 85 | style: { 86 | text: '⏺️\nSpill', 87 | size: '14', 88 | color: colors.black, 89 | bgcolor: colors.yellow, 90 | }, 91 | feedbacks: [], 92 | steps: [ 93 | { 94 | down: [ { actionId: 'spill', options: { slot: 'next' } } ], 95 | up: [], 96 | }, 97 | ], 98 | } 99 | 100 | presets.recName = { 101 | type: 'button', 102 | category: 'Transport', 103 | name: 'Record w/ Name', 104 | style: { 105 | text: 'Rec\nNamed', 106 | size: '14', 107 | color: colors.white, 108 | bgcolor: colors.red, 109 | }, 110 | feedbacks: [], 111 | steps: [ 112 | { 113 | down: [ { actionId: 'recName', options: { name: 'Recording' } } ], 114 | up: [], 115 | }, 116 | ], 117 | } 118 | 119 | presets.recTimestamp = { 120 | type: 'button', 121 | category: 'Transport', 122 | name: 'Record w/ Timestamp', 123 | style: { 124 | text: 'Rec\nTime', 125 | size: '14', 126 | color: colors.white, 127 | bgcolor: colors.red, 128 | }, 129 | feedbacks: [], 130 | steps: [ 131 | { 132 | down: [ { actionId: 'recTimestamp', options: { prefix: '' } } ], 133 | up: [], 134 | }, 135 | ], 136 | } 137 | 138 | presets.stop = { 139 | type: 'button', 140 | category: 'Transport', 141 | name: 'Stop', 142 | style: { 143 | text: '■\nStop', 144 | size: '14', 145 | color: colors.white, 146 | bgcolor: colors.grey, 147 | }, 148 | feedbacks: [ 149 | { 150 | feedbackId: 'transport_status', 151 | options: { status: 'stopped' }, 152 | style: { 153 | color: colors.white, 154 | bgcolor: colors.black, 155 | }, 156 | }, 157 | ], 158 | steps: [ 159 | { 160 | down: [{ actionId: 'stop', options: {} }], 161 | up: [], 162 | }, 163 | ], 164 | } 165 | 166 | presets.goto = { 167 | type: 'button', 168 | category: 'Transport', 169 | name: 'Goto TC', 170 | style: { 171 | text: 'Goto\n00:00:01:00', 172 | size: '14', 173 | color: colors.white, 174 | bgcolor: colors.teal, 175 | }, 176 | feedbacks: [], 177 | steps: [ 178 | { 179 | down: [ { actionId: 'goto', options: { tc: '00:00:01:00' } } ], 180 | up: [], 181 | }, 182 | ], 183 | } 184 | 185 | presets.gotoN = { 186 | type: 'button', 187 | category: 'Transport', 188 | name: 'Goto Clip (n)', 189 | style: { 190 | text: 'Goto\nClip 1', 191 | size: '14', 192 | color: colors.white, 193 | bgcolor: colors.black, 194 | }, 195 | feedbacks: [ 196 | { 197 | feedbackId: 'transport_clip', 198 | options: { clipID: 1, slotID: 'either' }, 199 | style: { 200 | color: colors.white, 201 | bgcolor: colors.blue, 202 | }, 203 | }, 204 | ], 205 | steps: [ 206 | { 207 | down: [{ actionId: 'gotoN', options: { clip: 1, useVariable: false } }], 208 | up: [], 209 | }, 210 | ], 211 | } 212 | 213 | presets.gotoName = { 214 | type: 'button', 215 | category: 'Transport', 216 | name: 'Goto Clip (name)', 217 | style: { 218 | text: 'Goto\nNamed', 219 | size: '14', 220 | color: colors.white, 221 | bgcolor: colors.black, 222 | }, 223 | feedbacks: [ 224 | { 225 | feedbackId: 'transport_clip_name', 226 | options: { clipName: '', slotID: 'either' }, 227 | style: { 228 | color: colors.white, 229 | bgcolor: colors.blue, 230 | }, 231 | }, 232 | ], 233 | steps: [ 234 | { 235 | down: [{ actionId: 'gotoName', options: { clip: '' } }], 236 | up: [], 237 | }, 238 | ], 239 | } 240 | 241 | presets.goFwd = { 242 | type: 'button', 243 | category: 'Transport', 244 | name: 'Go Fwd Clip', 245 | style: { 246 | text: 'Next\nClip', 247 | size: '14', 248 | color: colors.black, 249 | bgcolor: colors.orange, 250 | }, 251 | feedbacks: [], 252 | steps: [ 253 | { 254 | down: [ { actionId: 'goFwd', options: { clip: 1, useVariable: false } } ], 255 | up: [], 256 | }, 257 | ], 258 | } 259 | 260 | presets.goRew = { 261 | type: 'button', 262 | category: 'Transport', 263 | name: 'Go Rew Clip', 264 | style: { 265 | text: 'Prev\nClip', 266 | size: '14', 267 | color: colors.black, 268 | bgcolor: colors.orange, 269 | }, 270 | feedbacks: [], 271 | steps: [ 272 | { 273 | down: [ { actionId: 'goRew', options: { clip: 1, useVariable: false } } ], 274 | up: [], 275 | }, 276 | ], 277 | } 278 | 279 | presets.fetchClips = { 280 | type: 'button', 281 | category: 'Utility', 282 | name: 'Fetch Clips', 283 | style: { 284 | text: 'Fetch\nClips', 285 | size: '14', 286 | color: colors.white, 287 | bgcolor: colors.purple, 288 | }, 289 | feedbacks: [], 290 | steps: [ 291 | { 292 | down: [ { actionId: 'fetchClips', options: {} } ], 293 | up: [], 294 | }, 295 | ], 296 | } 297 | 298 | presets.formatPrepare = { 299 | type: 'button', 300 | category: 'Utility', 301 | name: 'Format (Prepare)', 302 | style: { 303 | text: 'Format\nPrepare', 304 | size: '14', 305 | color: colors.white, 306 | bgcolor: colors.black, 307 | }, 308 | feedbacks: [ 309 | { 310 | feedbackId: 'format_ready', 311 | options: {}, 312 | style: { 313 | color: colors.white, 314 | bgcolor: colors.red, 315 | }, 316 | }, 317 | ], 318 | steps: [ 319 | { 320 | down: [{ actionId: 'formatPrepare', options: { filesystem: 'HFS+', timeout: 10 } }], 321 | up: [], 322 | }, 323 | ], 324 | } 325 | 326 | presets.formatConfirm = { 327 | type: 'button', 328 | category: 'Utility', 329 | name: 'Format (Confirm)', 330 | style: { 331 | text: 'Format\nConfirm', 332 | size: '14', 333 | color: colors.white, 334 | bgcolor: colors.red, 335 | }, 336 | feedbacks: [], 337 | steps: [ 338 | { 339 | down: [ { actionId: 'formatConfirm', options: {} } ], 340 | up: [], 341 | }, 342 | ], 343 | } 344 | 345 | presets.remote = { 346 | type: 'button', 347 | category: 'Config', 348 | name: 'Remote Toggle', 349 | style: { 350 | text: 'Remote\nToggle', 351 | size: '14', 352 | color: colors.white, 353 | bgcolor: colors.black, 354 | }, 355 | feedbacks: [ 356 | { 357 | feedbackId: 'remote_status', 358 | options: { status: true }, 359 | style: { 360 | color: colors.white, 361 | bgcolor: colors.blue, 362 | }, 363 | }, 364 | ], 365 | steps: [ 366 | { 367 | down: [{ actionId: 'remote', options: { remoteEnable: 'toggle' } }], 368 | up: [], 369 | }, 370 | ], 371 | } 372 | 373 | presets.transportStatus = { 374 | type: 'button', 375 | category: 'Monitor', 376 | name: 'Transport Status', 377 | style: { 378 | text: 'Status\n$(hyperdeck:status)', 379 | size: 'auto', 380 | color: colors.white, 381 | bgcolor: colors.black, 382 | }, 383 | steps: [], 384 | feedbacks: [], 385 | } 386 | 387 | presets.clipInfo = { 388 | type: 'button', 389 | category: 'Monitor', 390 | name: 'Clip Info', 391 | style: { 392 | text: 'Clip\n$(hyperdeck:clipName)', 393 | size: 'auto', 394 | color: colors.white, 395 | bgcolor: colors.black, 396 | }, 397 | steps: [], 398 | feedbacks: [], 399 | } 400 | 401 | presets.slotId = { 402 | type: 'button', 403 | category: 'Monitor', 404 | name: 'Slot ID', 405 | style: { 406 | text: 'Slot\n$(hyperdeck:slotId)', 407 | size: 'auto', 408 | color: colors.white, 409 | bgcolor: colors.black, 410 | }, 411 | steps: [], 412 | feedbacks: [], 413 | } 414 | 415 | presets.videoFormat = { 416 | type: 'button', 417 | category: 'Monitor', 418 | name: 'Video Format', 419 | style: { 420 | text: 'Format\n$(hyperdeck:videoFormat)', 421 | size: 'auto', 422 | color: colors.white, 423 | bgcolor: colors.black, 424 | }, 425 | steps: [], 426 | feedbacks: [], 427 | } 428 | 429 | presets.recordingTime = { 430 | type: 'button', 431 | category: 'Monitor', 432 | name: 'Recording Time', 433 | style: { 434 | text: 'Rec Time\n$(hyperdeck:recordingTime)', 435 | size: 'auto', 436 | color: colors.white, 437 | bgcolor: colors.black, 438 | }, 439 | steps: [], 440 | feedbacks: [], 441 | } 442 | 443 | presets.timecode = { 444 | type: 'button', 445 | category: 'Monitor', 446 | name: 'Timecode', 447 | style: { 448 | text: 'TC\n$(hyperdeck:timecodeHMSF)', 449 | size: 'auto', 450 | color: colors.white, 451 | bgcolor: colors.black, 452 | }, 453 | steps: [], 454 | feedbacks: [], 455 | } 456 | 457 | presets.countdown = { 458 | type: 'button', 459 | category: 'Monitor', 460 | name: 'Countdown TC', 461 | style: { 462 | text: '⏳\n$(hyperdeck:countdownTimecodeHMSF)', 463 | size: 'auto', 464 | color: colors.white, 465 | bgcolor: colors.black, 466 | }, 467 | steps: [], 468 | feedbacks: [], 469 | } 470 | 471 | presets.audioFormat = { 472 | type: 'button', 473 | category: 'Monitor', 474 | name: 'Audio Format', 475 | style: { 476 | text: 'Audio\n$(hyperdeck:audioCodec)', 477 | size: 'auto', 478 | color: colors.white, 479 | bgcolor: colors.black, 480 | }, 481 | steps: [], 482 | feedbacks: [], 483 | } 484 | 485 | presets.audioChannels = { 486 | type: 'button', 487 | category: 'Monitor', 488 | name: 'Audio Channels', 489 | style: { 490 | text: 'Channels\n$(hyperdeck:audioChannels)', 491 | size: 'auto', 492 | color: colors.white, 493 | bgcolor: colors.black, 494 | }, 495 | steps: [], 496 | feedbacks: [], 497 | } 498 | 499 | presets.remoteEnabled = { 500 | type: 'button', 501 | category: 'Monitor', 502 | name: 'Remote Status', 503 | style: { 504 | text: 'Remote\n$(hyperdeck:remoteEnabled)', 505 | size: 'auto', 506 | color: colors.white, 507 | bgcolor: colors.black, 508 | }, 509 | steps: [], 510 | feedbacks: [], 511 | } 512 | 513 | return presets 514 | } 515 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { runEntrypoint, InstanceBase, InstanceStatus } from '@companion-module/base' 2 | import { Hyperdeck, Commands, TransportStatus, VideoFormat, ErrorCode } from 'hyperdeck-connection' 3 | import { 4 | initVariables, 5 | updateTransportInfoVariables, 6 | updateSlotInfoVariables, 7 | updateTimecodeVariables, 8 | updateClipVariables, 9 | updateConfigurationVariables, 10 | updateRemoteVariable, 11 | } from './variables.js' 12 | import { initActions } from './actions.js' 13 | import { initFeedbacks } from './feedbacks.js' 14 | import { initPresets } from './presets.js' 15 | import { upgradeScripts } from './upgrades.js' 16 | import { CONFIG_MODELS, ModelInfo } from './models.js' 17 | import { HyperdeckConfig, getConfigFields } from './config.js' 18 | import { makeSimpleClipInfos, mergeState, protocolGte, SimpleClipInfo, stripExtension } from './util.js' 19 | import { InstanceBaseExt, IpAndPort, TransportInfoStateExt } from './types.js' 20 | import { isDeepStrictEqual } from 'util' 21 | 22 | /** 23 | * Companion instance class for the Blackmagic HyperDeck Disk Recorders. 24 | * 25 | * @extends InstanceBase 26 | * @version 1.1.0 27 | * @since 1.0.0 28 | * @author Per Roine 29 | * @author Keith Rocheck 30 | */ 31 | class HyperdeckInstance extends InstanceBase implements InstanceBaseExt { 32 | // TODO - make type safe 33 | private hyperDeck: Hyperdeck | undefined 34 | config!: HyperdeckConfig 35 | model!: ModelInfo 36 | 37 | private pollTimer: NodeJS.Timeout | null = null 38 | 39 | protocolVersion: number = 0.0 40 | transportInfo: TransportInfoStateExt 41 | deckConfig: Commands.ConfigurationCommandResponse 42 | slotInfo: Commands.SlotInfoCommandResponse[] = [] 43 | 44 | remoteInfo: Commands.RemoteInfoCommandResponse | null = null 45 | formatToken: string | null = null 46 | formatTokenTimeout: NodeJS.Timeout | null = null 47 | 48 | simpleClipsList: SimpleClipInfo[] = [] 49 | fullClipsList: Commands.ClipInfo[] = [] 50 | 51 | /** 52 | * Create an instance of a HyperDeck module. 53 | * 54 | * @since 1.0.0 55 | */ 56 | constructor(internal: unknown) { 57 | super(internal) 58 | 59 | this.transportInfo = { 60 | status: TransportStatus.STOPPED, 61 | speed: 0, 62 | slotId: null, 63 | clipId: null, 64 | clipName: null, 65 | singleClip: false, 66 | displayTimecode: '00:00:00:00', 67 | timecode: '00:00:00:00', 68 | videoFormat: VideoFormat.NTSC, 69 | loop: false, 70 | inputVideoFormat: null, 71 | } 72 | this.deckConfig = { 73 | audioInput: '', 74 | videoInput: '', 75 | fileFormat: '', 76 | } 77 | } 78 | 79 | private initActionsAndFeedbacks() { 80 | this.setActionDefinitions(initActions(this)) 81 | this.setFeedbackDefinitions(initFeedbacks(this)) 82 | } 83 | 84 | private initPresets() { 85 | this.setPresetDefinitions(initPresets(this)) 86 | } 87 | 88 | /** 89 | * Creates the configuration fields for web config. 90 | */ 91 | getConfigFields() { 92 | return getConfigFields() 93 | } 94 | 95 | /** 96 | * Main initialization function called once the module 97 | * is OK to start doing things. 98 | */ 99 | async init(config: HyperdeckConfig) { 100 | this.config = config 101 | 102 | if (this.config.modelID !== undefined) { 103 | this.model = CONFIG_MODELS[this.config.modelID] 104 | } else { 105 | this.config.modelID = 'hdStudio' 106 | this.model = CONFIG_MODELS['hdStudio'] 107 | } 108 | if (!this.model) { 109 | this.model = Object.values(CONFIG_MODELS)[0] 110 | } 111 | 112 | this.updateStatus(InstanceStatus.Connecting) 113 | 114 | this.initActionsAndFeedbacks() 115 | this.initPresets() 116 | this.initVariables() 117 | 118 | this.initHyperdeck() 119 | } 120 | 121 | /** 122 | * Clean up the instance before it is destroyed. 123 | */ 124 | async destroy() { 125 | if (this.hyperDeck !== undefined) { 126 | this.hyperDeck.disconnect() 127 | this.hyperDeck.removeAllListeners() 128 | this.hyperDeck = undefined 129 | } 130 | 131 | if (this.pollTimer) { 132 | clearInterval(this.pollTimer) 133 | } 134 | } 135 | 136 | /** 137 | * INTERNAL: initialize variables. 138 | */ 139 | private initVariables() { 140 | initVariables(this) 141 | } 142 | 143 | /** 144 | * INTERNAL: use setup data to initalize the hyperdeck library. 145 | */ 146 | private initHyperdeck() { 147 | if (this.hyperDeck !== undefined) { 148 | this.hyperDeck.disconnect() 149 | this.hyperDeck.removeAllListeners() 150 | delete this.hyperDeck 151 | } 152 | 153 | if (this.pollTimer) { 154 | clearInterval(this.pollTimer) 155 | } 156 | 157 | // if (this.config.port === undefined) { 158 | // this.config.port = 9993 159 | // } 160 | 161 | const targetAddress = this.parseIpAndPort() 162 | 163 | if (!targetAddress) { 164 | this.updateStatus(InstanceStatus.BadConfig) 165 | return 166 | } 167 | this.updateStatus(InstanceStatus.Connecting) 168 | 169 | const hyperdeck = new Hyperdeck() 170 | this.hyperDeck = hyperdeck 171 | 172 | this.hyperDeck.on('error', (msg, e: any) => { 173 | this.log('error', `${msg}: ${e?.message ?? ''}`) 174 | }) 175 | 176 | this.hyperDeck.on('connected', async (info) => { 177 | this.log('debug', `Connected to Hyperdeck`) 178 | try { 179 | this.protocolVersion = info.protocolVersion 180 | this.log('debug', `Protocol version: ${info.protocolVersion}`) 181 | try { 182 | this.updateDeviceModelId(info) 183 | 184 | // setup notification: 185 | const notify = new Commands.NotifySetCommand() 186 | notify.configuration = true 187 | notify.transport = true 188 | notify.slot = true 189 | notify.remote = true 190 | if (protocolGte(this.protocolVersion, '1.11') && this.config.timecodeVariables === 'notifications') 191 | notify.displayTimecode = true 192 | 193 | await hyperdeck.sendCommand(notify) 194 | 195 | let { slots } = await hyperdeck.sendCommand(new Commands.DeviceInfoCommand()) 196 | if (!slots) slots = 2 197 | 198 | for (let i = 1; i <= slots; i++) { 199 | this.slotInfo[i] = await hyperdeck.sendCommand(new Commands.SlotInfoCommand(i)) 200 | this.log('debug', `Slot info:${JSON.stringify(this.slotInfo)}`) 201 | } 202 | 203 | await this.refreshTransportInfo() 204 | 205 | this.deckConfig = await hyperdeck.sendCommand(new Commands.ConfigurationGetCommand()) 206 | // this.debug('Initial config:', this.deckConfig) 207 | this.remoteInfo = await hyperdeck.sendCommand(new Commands.RemoteGetCommand()) 208 | } catch (e: any) { 209 | if (e.code) { 210 | this.log('error', `Connection error - ${e.code} ${e.name}`) 211 | } 212 | 213 | this.updateStatus(InstanceStatus.ConnectionFailure) 214 | 215 | // Connection couldn't initialise, destroy it and try again 216 | hyperdeck.disconnect().catch(() => null) 217 | 218 | setTimeout(() => { 219 | this.initHyperdeck() 220 | }, 1000) 221 | 222 | return 223 | } 224 | 225 | this.updateStatus(InstanceStatus.Ok) 226 | 227 | await this.updateClips(true) 228 | this.checkFeedbacks() 229 | 230 | // If polling is enabled, setup interval command 231 | if (this.pollTimer) clearInterval(this.pollTimer) 232 | if (this.config.timecodeVariables === 'polling') { 233 | this.pollTimer = setInterval(this.sendPollCommand.bind(this), this.config.pollingInterval) 234 | } 235 | } catch (e: any) { 236 | if (e.code) { 237 | this.log('error', `Connection error - ${e.code} ${e.name}`) 238 | } 239 | } 240 | }) 241 | 242 | this.hyperDeck.on('disconnected', () => { 243 | this.updateStatus(InstanceStatus.Disconnected) 244 | 245 | if (this.pollTimer) clearInterval(this.pollTimer) 246 | }) 247 | 248 | this.hyperDeck.on('notify.slot', async (res) => { 249 | this.log('debug', 'Slot Status Changed') 250 | 251 | this.slotInfo[res.slotId] = mergeState(this.slotInfo[res.slotId], res) 252 | 253 | // Update the transport status to catch slot changes 254 | await this.refreshTransportInfo() 255 | 256 | // Update slot variables 257 | const newVariables = {} 258 | updateTimecodeVariables(this, newVariables) 259 | updateSlotInfoVariables(this, newVariables) 260 | updateTransportInfoVariables(this, newVariables) 261 | this.setVariableValues(newVariables) 262 | 263 | // Update clip variables 264 | await this.updateClips() 265 | 266 | // Update internals 267 | this.checkFeedbacks('slot_status', 'transport_slot') 268 | }) 269 | 270 | this.hyperDeck.on('notify.transport', async (res) => { 271 | this.log('debug', 'Transport Status Changed') 272 | this.transportInfo = this.extendTransportInfo(mergeState(this.transportInfo, res)) 273 | 274 | const newVariables = {} 275 | updateTransportInfoVariables(this, newVariables) 276 | updateTimecodeVariables(this, newVariables) 277 | updateSlotInfoVariables(this, newVariables) 278 | this.setVariableValues(newVariables) 279 | 280 | this.checkFeedbacks() 281 | }) 282 | 283 | this.hyperDeck.on('notify.remote', async (res) => { 284 | this.log('debug', 'Remote Status Changed') 285 | this.remoteInfo = mergeState(this.remoteInfo, res) 286 | 287 | const newVariables = {} 288 | updateRemoteVariable(this, newVariables) 289 | this.setVariableValues(newVariables) 290 | 291 | this.checkFeedbacks('remote_status') 292 | }) 293 | 294 | this.hyperDeck.on('notify.configuration', async (res) => { 295 | this.log('debug', `Configuration Changed: ${JSON.stringify(res)}`) 296 | this.deckConfig = mergeState(this.deckConfig, res) 297 | 298 | // this.debug('Config:', this.deckConfig) 299 | this.checkFeedbacks('video_input', 'audio_input', 'audio_channels') 300 | 301 | const newVariables = {} 302 | updateConfigurationVariables(this, newVariables) 303 | this.setVariableValues(newVariables) 304 | }) 305 | 306 | this.hyperDeck.on('notify.displayTimecode', (res) => { 307 | this.transportInfo.displayTimecode = res.displayTimecode 308 | 309 | const newVariables = {} 310 | updateTimecodeVariables(this, newVariables) 311 | this.setVariableValues(newVariables) 312 | }) 313 | 314 | this.hyperDeck.connect(targetAddress.ip, targetAddress.port) 315 | 316 | // hyperdeck-connection debug tool 317 | // this.hyperDeck.DEBUG = true 318 | } 319 | 320 | /** 321 | * INTERNAL: Send a poll command to refresh status 322 | */ 323 | private sendPollCommand() { 324 | this.refreshTransportInfo() 325 | .then(() => { 326 | // Update slot variables 327 | const newVariables = {} 328 | updateTimecodeVariables(this, newVariables) 329 | updateSlotInfoVariables(this, newVariables) 330 | updateTransportInfoVariables(this, newVariables) 331 | this.setVariableValues(newVariables) 332 | }) 333 | .catch((error) => { 334 | this.log('error', `Timecode polling failed: ${error}`) 335 | if (this.pollTimer) clearInterval(this.pollTimer) 336 | }) 337 | } 338 | 339 | /** 340 | * Process an updated configuration array. 341 | */ 342 | async configUpdated(config: HyperdeckConfig) { 343 | let resetConnection = false 344 | 345 | if (this.config.host !== config.host || this.config.bonjourHost !== config.bonjourHost) { 346 | resetConnection = true 347 | } 348 | 349 | // Enable/disable timecode notifications 350 | if ( 351 | this.hyperDeck?.connected && 352 | protocolGte(this.protocolVersion, '1.11') && 353 | this.config.timecodeVariables !== config.timecodeVariables && 354 | !resetConnection 355 | ) { 356 | if (this.config.timecodeVariables === 'notifications') { 357 | // old config had notifications and new config does not 358 | const notify = new Commands.NotifySetCommand() 359 | notify.displayTimecode = false 360 | this.hyperDeck.sendCommand(notify) 361 | } else if (config.timecodeVariables === 'notifications') { 362 | // old config had no notifications and new config does have them 363 | const notify = new Commands.NotifySetCommand() 364 | notify.displayTimecode = true 365 | this.hyperDeck.sendCommand(notify) 366 | } 367 | } 368 | 369 | if (this.config.modelID !== config.modelID) { 370 | this.model = CONFIG_MODELS[config.modelID] 371 | } 372 | 373 | this.config = config 374 | 375 | this.initActionsAndFeedbacks() 376 | this.initPresets() 377 | this.initVariables() 378 | 379 | if (resetConnection || !this.hyperDeck) { 380 | this.initHyperdeck() 381 | } else { 382 | // If polling is enabled, setup interval command 383 | if (this.pollTimer) clearInterval(this.pollTimer) 384 | if (this.config.timecodeVariables === 'polling') { 385 | this.pollTimer = setInterval(this.sendPollCommand.bind(this), this.config.pollingInterval) 386 | } 387 | } 388 | } 389 | 390 | /** 391 | * INTERNAL: Updates device data from the HyperDeck 392 | */ 393 | private updateDeviceModelId(info: Commands.ConnectionInfoResponse) { 394 | const modelName = info.model 395 | 396 | this.log('info', `Connected to a ${info.model}`) 397 | 398 | const oldModelId = this.config.modelID 399 | 400 | // TODO - can this be replaced? 401 | // this.debug('Model value:', value) 402 | if (modelName.match(/Extreme/)) { 403 | this.config.modelID = 'hdExtreme8K' 404 | } else if (modelName.match(/Studio Mini/)) { 405 | this.config.modelID = 'hdStudioMini' 406 | } else if (modelName.match(/Duplicator/)) { 407 | this.config.modelID = 'bmdDup4K' 408 | } else if (modelName.match(/12G/)) { 409 | this.config.modelID = 'hdStudio12G' 410 | } else if (modelName.match(/Studio Pro/)) { 411 | this.config.modelID = 'hdStudioPro' 412 | } else if (modelName.match(/HD Mini/)) { 413 | this.config.modelID = 'hdStudioHDMini' 414 | } else if (modelName.match(/HD Plus/)) { 415 | this.config.modelID = 'hdStudioHDPlus' 416 | } else if (modelName.match(/HD Pro/)) { 417 | this.config.modelID = 'hdStudioHDPro' 418 | } else if (modelName.match(/4K Pro/)) { 419 | this.config.modelID = 'hdStudio4KPro' 420 | } else if (modelName.match(/Shuttle HD/)) { 421 | this.config.modelID = 'hdShuttleHD' 422 | } else { 423 | this.config.modelID = 'hdStudio' 424 | } 425 | 426 | if (this.config.modelID !== oldModelId) { 427 | this.saveConfig(this.config) 428 | } 429 | } 430 | 431 | /** 432 | * INTERNAL: Get clip list from the hyperdeck 433 | * 434 | * @access protected 435 | */ 436 | async updateClips(doFullInit = false) { 437 | try { 438 | const newVariableValues = {} 439 | 440 | if (!this.hyperDeck) throw new Error('TODO - no hyperdeck connection') 441 | 442 | let queryResponse: Commands.ClipsGetCommandResponse 443 | try { 444 | queryResponse = await this.hyperDeck.sendCommand(new Commands.ClipsGetCommand()) 445 | } catch (e: any) { 446 | if (e.code === ErrorCode.TimelineEmpty) { 447 | // This error means there were no clips 448 | queryResponse = { 449 | clipCount: 0, 450 | clips: [], 451 | } 452 | } else { 453 | throw e 454 | } 455 | } 456 | 457 | // Check for a shorter list of clips, and unset variables if so 458 | const oldClips = this.simpleClipsList 459 | this.simpleClipsList = makeSimpleClipInfos(queryResponse.clips) 460 | this.fullClipsList = queryResponse.clips 461 | 462 | if (doFullInit || !isDeepStrictEqual(oldClips, this.simpleClipsList)) { 463 | // reinit due to clip list change 464 | this.initActionsAndFeedbacks() 465 | 466 | // Update variables, as clip count can have changed. This will update all the values too 467 | this.initVariables() 468 | } else { 469 | // Selectively update variables 470 | updateTransportInfoVariables(this, newVariableValues) // Current clip name could have changed 471 | updateClipVariables(this, newVariableValues) 472 | this.setVariableValues(newVariableValues) 473 | } 474 | } catch (e: any) { 475 | if (e.code) { 476 | this.log('error', e.code + ' ' + e.name) 477 | } 478 | } 479 | } 480 | 481 | /** 482 | * INTERNAL: Update transportInfo object 483 | */ 484 | private extendTransportInfo(rawState: Commands.TransportInfoCommandResponse): TransportInfoStateExt { 485 | const res: TransportInfoStateExt = { 486 | ...rawState, 487 | clipName: null, 488 | } 489 | 490 | if (res.clipId !== null) { 491 | const clipObj = this.simpleClipsList.find(({ clipId }) => clipId == this.transportInfo.clipId) 492 | if (clipObj) { 493 | res.clipName = stripExtension(clipObj.name) 494 | } 495 | } 496 | 497 | return res 498 | } 499 | 500 | async sendCommand(cmd: Commands.AbstractCommand): Promise { 501 | if (this.hyperDeck && this.hyperDeck.connected) { 502 | try { 503 | return await this.hyperDeck.sendCommand(cmd) 504 | } catch (e: any) { 505 | if (e.code) { 506 | throw new Error(`${e.code} ${e.name}`) 507 | } else { 508 | throw e 509 | } 510 | } 511 | } else { 512 | throw new Error('not connected') 513 | } 514 | } 515 | 516 | async refreshTransportInfo(): Promise { 517 | if (!this.hyperDeck || !this.hyperDeck.connected) { 518 | this.log('error', 'Hyperdeck not connected (refreshTransportInfo)') 519 | return 520 | } 521 | try { 522 | const rawInfo = await this.hyperDeck.sendCommand(new Commands.TransportInfoCommand()) 523 | this.transportInfo = this.extendTransportInfo(rawInfo) 524 | } catch (e: any) { 525 | if (e.code) { 526 | throw new Error(`${e.code} ${e.name}`) 527 | } else { 528 | throw e 529 | } 530 | } 531 | } 532 | 533 | parseIpAndPort(): IpAndPort | null { 534 | const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ 535 | 536 | if (this.config.bonjourHost) { 537 | const [ip, rawPort] = this.config.bonjourHost.split(':') 538 | const port = Number(rawPort) 539 | if (ip.match(ipRegex) && !isNaN(port)) { 540 | return { 541 | ip, 542 | port, 543 | } 544 | } 545 | } else if (this.config.host) { 546 | if (this.config.host.match(ipRegex)) { 547 | return { 548 | ip: this.config.host, 549 | port: undefined, 550 | } 551 | } 552 | } 553 | return null 554 | } 555 | } 556 | 557 | runEntrypoint(HyperdeckInstance, upgradeScripts) 558 | -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompanionActionContext, 3 | CompanionActionDefinitions, 4 | CompanionOptionValues, 5 | CompanionVariableValues, 6 | Regex, 7 | } from '@companion-module/base' 8 | import { Commands, FilesystemFormat, VideoFormat } from 'hyperdeck-connection' 9 | import { 10 | CHOICES_STARTEND, 11 | CHOICES_PREVIEWMODE, 12 | CHOICES_AUDIOCODEC, 13 | CHOICES_AUDIOCHANNELS, 14 | CHOICES_FILESYSTEM, 15 | CHOICES_REMOTECONTROL, 16 | createModelChoices, 17 | createClipsChoice, 18 | } from './choices.js' 19 | import { getTimestamp, protocolGte, stripExtension } from './util.js' 20 | import { updateRemoteVariable } from './variables.js' 21 | import { InstanceBaseExt } from './types.js' 22 | 23 | export function initActions(self: InstanceBaseExt) { 24 | const modelChoices = createModelChoices(self.model) 25 | const clipChoices = createClipsChoice(self) 26 | 27 | const actions: CompanionActionDefinitions = {} 28 | 29 | const maxShuttle = self.model?.maxShuttle ?? 0 30 | 31 | const getOptNumber = (options: CompanionOptionValues, key: string): number => { 32 | let value = options[key] 33 | if (typeof value === 'number') return value 34 | value = Number(value) 35 | if (!isNaN(value)) return value 36 | throw new Error(`Invalid number for ${key}: ${options[key]} (${typeof value})`) 37 | } 38 | const parseOptNumber = async ( 39 | context: CompanionActionContext, 40 | options: CompanionOptionValues, 41 | key: string 42 | ): Promise => { 43 | const parsedValue = await context.parseVariablesInString(String(options[key])) 44 | const value = Number(parsedValue) 45 | if (!isNaN(value)) return value 46 | throw new Error(`Invalid number for ${key}: ${parsedValue} (${typeof value})`) 47 | } 48 | const getOptString = (options: CompanionOptionValues, key: string): string => { 49 | const value = options[key] 50 | return value?.toString() ?? '' 51 | } 52 | const parseOptString = async ( 53 | context: CompanionActionContext, 54 | options: CompanionOptionValues, 55 | key: string 56 | ): Promise => { 57 | const value = await context.parseVariablesInString(String(options[key])) 58 | return value?.toString() ?? '' 59 | } 60 | const getOptBool = (options: CompanionOptionValues, key: string): boolean => { 61 | let value = options[key] 62 | if (typeof value === 'boolean') return value 63 | if (typeof value === 'string') { 64 | if (value.toLowerCase() === 'true') return true 65 | if (value.toLowerCase() === 'false') return false 66 | } 67 | throw new Error(`Invalid boolean for ${key}: ${options[key]} (${typeof value})`) 68 | } 69 | 70 | if (self.config.modelID != 'bmdDup4K') { 71 | actions['play'] = { 72 | name: 'Play', 73 | options: [ 74 | { 75 | type: 'number', 76 | label: 'Speed %', 77 | id: 'speed', 78 | default: 100, 79 | min: 0 - maxShuttle, 80 | max: maxShuttle, 81 | range: true, 82 | isVisible: (options) => options.useVariable === false || options.useVariable === undefined, 83 | }, 84 | { 85 | type: 'textinput', 86 | label: 'Speed %', 87 | id: 'speedVar', 88 | default: '', 89 | useVariables: { local: true }, 90 | isVisible: (options) => options.useVariable === true, 91 | }, 92 | { 93 | type: 'checkbox', 94 | label: 'Use variable for speed %', 95 | id: 'useVariable', 96 | default: false, 97 | }, 98 | { 99 | type: 'checkbox', 100 | label: 'Loop clip', 101 | id: 'loop', 102 | default: false, 103 | }, 104 | { 105 | type: 'checkbox', 106 | label: 'Single clip playback', 107 | id: 'single', 108 | default: false, 109 | }, 110 | ], 111 | callback: async ({ options }, context) => { 112 | const cmd = new Commands.PlayCommand() 113 | if (!options.useVariable) cmd.speed = getOptString(options, 'speed') 114 | else cmd.speed = await parseOptString(context, options, 'speedVar') 115 | cmd.loop = getOptBool(options, 'loop') 116 | cmd.singleClip = getOptBool(options, 'single') 117 | await self.sendCommand(cmd) 118 | }, 119 | } 120 | } 121 | 122 | actions['rec'] = { 123 | name: 'Record', 124 | options: [], 125 | callback: async () => { 126 | const cmd = new Commands.RecordCommand() 127 | await self.sendCommand(cmd) 128 | }, 129 | } 130 | 131 | actions['spill'] = { 132 | name: 'Spill', 133 | description: 'Spill current recording to specified slot', 134 | options: [ 135 | { 136 | type: 'dropdown', 137 | label: 'Slot', 138 | id: 'slot', 139 | default: 'next', 140 | choices: [ 141 | { 142 | id: 'next', 143 | label: 'next', 144 | }, 145 | { 146 | id: 'same', 147 | label: 'Same Slot', 148 | }, 149 | ...modelChoices.Slots, 150 | ], 151 | }, 152 | ], 153 | callback: async ({ options }) => { 154 | const cmd = new Commands.RecordSpillCommand() 155 | if (!options.slot || options.slot === 'next') { 156 | // No parameter 157 | } else if (options.slot === 'same') { 158 | // Split and continue on the same slot 159 | if (self.transportInfo.slotId) cmd.slot = self.transportInfo.slotId 160 | } else { 161 | cmd.slot = getOptNumber(options, 'slot') 162 | } 163 | 164 | await self.sendCommand(cmd) 165 | }, 166 | } 167 | 168 | if (self.config.modelID == 'bmdDup4K') { 169 | actions['recAppend'] = { 170 | name: 'Append Record', 171 | options: [], 172 | callback: async () => { 173 | const cmd = new Commands.RecordCommand() 174 | cmd.append = true 175 | await self.sendCommand(cmd) 176 | }, 177 | } 178 | } 179 | 180 | if (self.config.modelID != 'bmdDup4K') { 181 | actions['recName'] = { 182 | name: 'Record (with name)', 183 | options: [ 184 | { 185 | type: 'textinput', 186 | label: 'Filename (without extension)', 187 | id: 'name', 188 | default: '', 189 | regex: Regex.SOMETHING, 190 | useVariables: { local: true }, 191 | }, 192 | ], 193 | callback: async ({ options }, context) => { 194 | const name = await context.parseVariablesInString(options.name + '') 195 | const cmd = new Commands.RecordCommand(name) 196 | await self.sendCommand(cmd) 197 | }, 198 | } 199 | actions['recTimestamp'] = { 200 | name: 'Record (with name and current date/time)', 201 | options: [ 202 | { 203 | type: 'textinput', 204 | label: 'Filename (optional)', 205 | id: 'prefix', 206 | default: '', 207 | useVariables: { local: true }, 208 | }, 209 | ], 210 | callback: async ({ options }, context) => { 211 | const cmd = new Commands.RecordCommand() 212 | const timeStamp = getTimestamp() 213 | if (options.prefix) { 214 | const name = await context.parseVariablesInString(options.prefix + '') 215 | cmd.filename = name + '-' + timeStamp + '-' 216 | } else { 217 | cmd.filename = timeStamp + '-' 218 | } 219 | await self.sendCommand(cmd) 220 | }, 221 | } 222 | actions['recCustom'] = { 223 | name: 'Record (with custom reel)', 224 | options: [ 225 | { 226 | type: 'static-text', 227 | id: 'info', 228 | label: "Set 'Reel' in instance config", 229 | value: '', 230 | }, 231 | ], 232 | callback: async () => { 233 | const cmd = new Commands.RecordCommand(self.config.reel + '-') 234 | await self.sendCommand(cmd) 235 | }, 236 | } 237 | } 238 | 239 | actions['stop'] = { 240 | name: 'Stop', 241 | options: [], 242 | callback: async () => { 243 | const cmd = new Commands.StopCommand() 244 | await self.sendCommand(cmd) 245 | }, 246 | } 247 | 248 | if (self.config.modelID != 'bmdDup4K') { 249 | actions['goto'] = { 250 | name: 'Goto (TC)', 251 | options: [ 252 | { 253 | type: 'textinput', 254 | label: 'Timecode hh:mm:ss:ff', 255 | id: 'tc', 256 | default: '00:00:01:00', 257 | regex: Regex.TIMECODE, 258 | useVariables: { local: true }, 259 | }, 260 | ], 261 | callback: async ({ options }, context) => { 262 | const cmd = new Commands.GoToCommand() 263 | cmd.timecode = await parseOptString(context, options, 'tc') 264 | await self.sendCommand(cmd) 265 | }, 266 | } 267 | actions['gotoN'] = { 268 | name: 'Goto Clip (n)', 269 | options: [ 270 | { 271 | type: 'number', 272 | label: 'Clip Number', 273 | id: 'clip', 274 | default: 1, 275 | min: 1, 276 | max: 999, 277 | range: false, 278 | isVisible: (options) => options.useVariable === false || options.useVariable === undefined, 279 | }, 280 | { 281 | type: 'textinput', 282 | label: 'Clip Number', 283 | id: 'clipVar', 284 | default: '', 285 | useVariables: { local: true }, 286 | isVisible: (options) => options.useVariable === true, 287 | }, 288 | { 289 | type: 'checkbox', 290 | label: 'Use Variable', 291 | id: 'useVariable', 292 | default: false, 293 | }, 294 | ], 295 | callback: async ({ options }, context) => { 296 | const cmd = new Commands.GoToCommand() 297 | 298 | if (!options.useVariable) cmd.clipId = getOptNumber(options, 'clip') 299 | else cmd.clipId = await parseOptNumber(context, options, 'clipVar') 300 | await self.sendCommand(cmd) 301 | }, 302 | } 303 | actions['gotoName'] = { 304 | name: 'Goto Clip (name)', 305 | options: [ 306 | { 307 | type: 'dropdown', 308 | label: 'Clip Name - select from list or enter text (variables supported)', 309 | id: 'clip', 310 | default: '', 311 | choices: clipChoices, 312 | minChoicesForSearch: 0, 313 | allowCustom: true, 314 | }, 315 | ], 316 | callback: async ({ options }, context) => { 317 | await self.updateClips() 318 | 319 | const parsedRaw = await context.parseVariablesInString(options.clip + '') 320 | const parsed = stripExtension(parsedRaw.trim()) 321 | 322 | const clip = self.simpleClipsList.find((clip) => stripExtension(clip.name) === parsed) 323 | if (!clip) { 324 | self.log('info', `Clip "${parsedRaw}" does not exist`) 325 | } else { 326 | const cmd = new Commands.GoToCommand() 327 | cmd.clipId = clip.clipId 328 | await self.sendCommand(cmd) 329 | } 330 | }, 331 | } 332 | actions['goFwd'] = { 333 | name: 'Go forward (n) clips', 334 | options: [ 335 | { 336 | type: 'number', 337 | label: 'Number of clips', 338 | id: 'clip', 339 | default: 1, 340 | min: 1, 341 | max: 999, 342 | range: false, 343 | isVisible: (options) => options.useVariable === false || options.useVariable === undefined, 344 | }, 345 | { 346 | type: 'textinput', 347 | label: 'Number of clips', 348 | id: 'clipVar', 349 | default: '', 350 | useVariables: { local: true }, 351 | isVisible: (options) => options.useVariable === true, 352 | }, 353 | { 354 | type: 'checkbox', 355 | label: 'Use Variable', 356 | id: 'useVariable', 357 | default: false, 358 | }, 359 | ], 360 | callback: async ({ options }, context) => { 361 | const cmd = new Commands.GoToCommand() 362 | if (!options.useVariable) cmd.clipId = `+${options.clip}` 363 | else cmd.clipId = `+${await parseOptNumber(context, options, 'clipVar')}` 364 | await self.sendCommand(cmd) 365 | }, 366 | } 367 | actions['goRew'] = { 368 | name: 'Go backward (n) clips', 369 | options: [ 370 | { 371 | type: 'number', 372 | label: 'Number of clips', 373 | id: 'clip', 374 | default: 1, 375 | min: 1, 376 | max: 999, 377 | range: false, 378 | isVisible: (options) => options.useVariable === false || options.useVariable === undefined, 379 | }, 380 | { 381 | type: 'textinput', 382 | label: 'Number of clips', 383 | id: 'clipVar', 384 | default: '', 385 | useVariables: { local: true }, 386 | isVisible: (options) => options.useVariable === true, 387 | }, 388 | { 389 | type: 'checkbox', 390 | label: 'Use Variable', 391 | id: 'useVariable', 392 | default: false, 393 | }, 394 | ], 395 | callback: async ({ options }, context) => { 396 | const cmd = new Commands.GoToCommand() 397 | if (!options.useVariable) cmd.clipId = `-${options.clip}` 398 | else cmd.clipId = `-${await parseOptNumber(context, options, 'clipVar')}` 399 | await self.sendCommand(cmd) 400 | }, 401 | } 402 | actions['goStartEnd'] = { 403 | name: 'Go to (start|end) of clip', 404 | options: [ 405 | { 406 | type: 'dropdown', 407 | label: 'Go to', 408 | id: 'startEnd', 409 | default: 'start', 410 | choices: CHOICES_STARTEND, 411 | }, 412 | ], 413 | callback: async ({ options }) => { 414 | const cmd = new Commands.GoToCommand() 415 | cmd.clip = options.startEnd as any 416 | await self.sendCommand(cmd) 417 | }, 418 | } 419 | actions['jogFwd'] = { 420 | name: 'Jog forward (TC) duration', 421 | options: [ 422 | { 423 | type: 'textinput', 424 | label: 'Timecode hh:mm:ss:ff', 425 | id: 'jogFwdTc', 426 | default: '00:00:00:01', 427 | regex: Regex.TIMECODE, 428 | useVariables: { local: true }, 429 | }, 430 | ], 431 | callback: async ({ options }, context) => { 432 | const cmd = new Commands.JogCommand() 433 | cmd.timecode = `+${await parseOptString(context, options, 'jogFwdTc')}` 434 | await self.sendCommand(cmd) 435 | }, 436 | } 437 | actions['jogRew'] = { 438 | name: 'Jog backward (TC) duration', 439 | options: [ 440 | { 441 | type: 'textinput', 442 | label: 'Timecode hh:mm:ss:ff', 443 | id: 'jogRewTc', 444 | default: '00:00:00:01', 445 | regex: Regex.TIMECODE, 446 | useVariables: { local: true }, 447 | }, 448 | ], 449 | callback: async ({ options }, context) => { 450 | const cmd = new Commands.JogCommand() 451 | cmd.timecode = `-${await parseOptString(context, options, 'jogRewTc')}` 452 | await self.sendCommand(cmd) 453 | }, 454 | } 455 | actions['shuttle'] = { 456 | name: 'Shuttle with speed', 457 | options: [ 458 | { 459 | type: 'number', 460 | label: 'Speed %', 461 | id: 'speed', 462 | default: 100, 463 | min: 0 - maxShuttle, 464 | max: maxShuttle, 465 | range: true, 466 | isVisible: (options) => options.useVariable === false || options.useVariable === undefined, 467 | }, 468 | { 469 | type: 'textinput', 470 | label: 'Speed %', 471 | id: 'speedVar', 472 | default: '', 473 | useVariables: { local: true }, 474 | isVisible: (options) => options.useVariable === true, 475 | }, 476 | { 477 | type: 'checkbox', 478 | label: 'Use Variable', 479 | id: 'useVariable', 480 | default: false, 481 | }, 482 | ], 483 | callback: async ({ options }, context) => { 484 | const cmd = new Commands.ShuttleCommand() 485 | if (!options.useVariable) cmd.speed = getOptNumber(options, 'speed') 486 | else cmd.speed = await parseOptNumber(context, options, 'speedVar') 487 | await self.sendCommand(cmd) 488 | }, 489 | } 490 | actions['select'] = { 491 | name: 'Select slot or format', 492 | options: [ 493 | { 494 | type: 'dropdown', 495 | label: 'Slot', 496 | id: 'slot', 497 | default: 1, 498 | choices: [ 499 | { 500 | id: 'unchanged', 501 | label: 'Unchanged', 502 | }, 503 | ...modelChoices.Slots, 504 | ], 505 | }, 506 | { 507 | type: 'dropdown', 508 | label: 'Format', 509 | id: 'format', 510 | default: 'unchanged', 511 | choices: [ 512 | // nocommit - upgrade script 513 | { 514 | id: 'unchanged', 515 | label: 'Unchanged', 516 | }, 517 | ...modelChoices.VideoFormats, 518 | ], 519 | }, 520 | ], 521 | callback: async ({ options }) => { 522 | const cmd = new Commands.SlotSelectCommand() 523 | if (options.slot && options.slot !== 'unchanged') cmd.slotId = getOptNumber(options, 'slot') 524 | if (options.format && options.format !== 'unchanged') cmd.format = options.format as VideoFormat 525 | await self.sendCommand(cmd) 526 | 527 | // select will update internal cliplist so we should fetch those 528 | await self.refreshTransportInfo() 529 | await self.updateClips(true) 530 | 531 | self.checkFeedbacks() 532 | }, 533 | } 534 | actions['preview'] = { 535 | name: 'Preview', 536 | options: [ 537 | { 538 | type: 'dropdown', 539 | label: 'Set preview/output mode', 540 | id: 'enable', 541 | default: 'true', 542 | choices: CHOICES_PREVIEWMODE, 543 | }, 544 | ], 545 | callback: async ({ options }) => { 546 | const cmd = new Commands.PreviewCommand(getOptBool(options, 'enable')) 547 | await self.sendCommand(cmd) 548 | }, 549 | } 550 | } // endif (!= bmdDup4K) 551 | 552 | if (modelChoices.VideoInputs.length > 1) { 553 | actions['videoSrc'] = { 554 | name: 'Video source', 555 | options: [ 556 | { 557 | type: 'dropdown', 558 | label: 'Input', 559 | id: 'videoSrc', 560 | default: modelChoices.VideoInputs[0]?.id, 561 | choices: modelChoices.VideoInputs, 562 | }, 563 | ], 564 | callback: async ({ options }) => { 565 | const cmd = new Commands.ConfigurationCommand() 566 | cmd.videoInput = getOptString(options, 'videoSrc') 567 | await self.sendCommand(cmd) 568 | }, 569 | } 570 | } 571 | 572 | if (modelChoices.AudioInputs.length > 1) { 573 | actions['audioSrc'] = { 574 | name: 'Audio source', 575 | options: [ 576 | { 577 | type: 'dropdown', 578 | label: 'Input', 579 | id: 'audioSrc', 580 | default: modelChoices.AudioInputs[0]?.id, 581 | choices: modelChoices.AudioInputs, 582 | }, 583 | ], 584 | callback: async ({ options }) => { 585 | const cmd = new Commands.ConfigurationCommand() 586 | cmd.audioInput = getOptString(options, 'audioSrc') 587 | await self.sendCommand(cmd) 588 | }, 589 | } 590 | } 591 | 592 | if (protocolGte(self.protocolVersion, '1.11')) { 593 | actions['audioChannels'] = { 594 | name: 'Audio channels', 595 | options: [ 596 | { 597 | type: 'dropdown', 598 | label: 'Codec', 599 | id: 'audioCodec', 600 | default: CHOICES_AUDIOCODEC[0].id, 601 | choices: CHOICES_AUDIOCODEC, 602 | }, 603 | { 604 | type: 'dropdown', 605 | label: 'Channels', 606 | id: 'audioChannels', 607 | default: '2', 608 | choices: CHOICES_AUDIOCHANNELS, 609 | isVisible: (options) => options.audioCodec === 'PCM', 610 | }, 611 | ], 612 | callback: async ({ options }) => { 613 | const cmd = new Commands.ConfigurationCommand() 614 | cmd.audioCodec = getOptString(options, 'audioCodec') as any // TODO 615 | cmd.audioInputChannels = 2 616 | if (options.audioCodec == 'PCM') { 617 | let channels = 2 618 | if (options.audioChannels == 'cycle') { 619 | channels = 620 | self.deckConfig.audioInputChannels == 16 || typeof self.deckConfig.audioInputChannels !== 'number' 621 | ? 2 622 | : self.deckConfig.audioInputChannels * 2 623 | } else { 624 | channels = getOptNumber(options, 'audioChannels') 625 | } 626 | cmd.audioInputChannels = channels 627 | } 628 | await self.sendCommand(cmd) 629 | }, 630 | } 631 | } 632 | 633 | if (modelChoices.FileFormats.length > 1) { 634 | actions['fileFormat'] = { 635 | name: 'File format', 636 | options: [ 637 | { 638 | type: 'dropdown', 639 | label: 'Format', 640 | id: 'fileFormat', 641 | default: modelChoices.FileFormats[0]?.id, 642 | choices: modelChoices.FileFormats, 643 | }, 644 | ], 645 | callback: async ({ options }) => { 646 | const cmd = new Commands.ConfigurationCommand() 647 | cmd.fileFormat = getOptString(options, 'fileFormat') 648 | await self.sendCommand(cmd) 649 | }, 650 | } 651 | } 652 | 653 | actions['fetchClips'] = { 654 | name: 'Fetch Clips', 655 | options: [], 656 | callback: async () => { 657 | await self.updateClips(true) 658 | }, 659 | } 660 | 661 | /** 662 | * Not currently implemented 663 | * 664 | if (self.config.modelID == 'hdExtreme8K') { 665 | actions['dynamicRange'] = { 666 | name: 'Set playback dyanmic range', 667 | options: [ 668 | { 669 | type: 'dropdown', 670 | label: 'Dynamic Range', 671 | id: 'dynamicRange', 672 | default: 'auto', 673 | choices: CHOICES_DYNAMICRANGE 674 | } 675 | ], 676 | callback: async ({options}) => { 677 | const cmd = new Commands.ConfigurationCommand() 678 | cmd.dynamicRange = action.options.dynamicRange 679 | await self.sendCommand(cmd) 680 | }, 681 | }; 682 | } 683 | */ 684 | 685 | actions['formatPrepare'] = { 686 | name: 'Format drive/card (prepare)', 687 | options: [ 688 | { 689 | type: 'dropdown', 690 | label: 'Filesystem', 691 | id: 'filesystem', 692 | default: 'HFS+', 693 | choices: CHOICES_FILESYSTEM, 694 | }, 695 | { 696 | type: 'number', 697 | label: 'Confirmation timeout (sec)', 698 | id: 'timeout', 699 | default: 10, 700 | min: 0, 701 | max: 600, 702 | }, 703 | ], 704 | callback: async ({ options }) => { 705 | const cmd = new Commands.FormatCommand() 706 | cmd.filesystem = getOptString(options, 'filesystem') as FilesystemFormat 707 | const response = await self.sendCommand(cmd) 708 | 709 | if (response && response.code) { 710 | self.log('debug', 'Format token: ' + response.code) 711 | self.formatToken = response.code 712 | self.checkFeedbacks('format_ready') 713 | } 714 | 715 | if (self.formatTokenTimeout) clearTimeout(self.formatTokenTimeout) 716 | 717 | self.formatTokenTimeout = setTimeout( 718 | () => { 719 | self.formatToken = null 720 | self.checkFeedbacks('format_ready') 721 | }, 722 | getOptNumber(options, 'timeout') * 1000 723 | ) 724 | }, 725 | } 726 | 727 | actions['formatConfirm'] = { 728 | name: 'Format drive/card (confirm)', 729 | options: [], 730 | callback: async () => { 731 | if (self.formatToken) { 732 | if (self.formatTokenTimeout) { 733 | clearTimeout(self.formatTokenTimeout) 734 | self.formatTokenTimeout = null 735 | } 736 | 737 | const cmd = new Commands.FormatConfirmCommand() 738 | cmd.code = self.formatToken 739 | 740 | self.formatToken = null 741 | self.checkFeedbacks('format_ready') 742 | 743 | await self.sendCommand(cmd) 744 | } 745 | }, 746 | } 747 | 748 | actions['remote'] = { 749 | name: 'Remote Control (enable/disable)', 750 | options: [ 751 | { 752 | type: 'dropdown', 753 | label: 'Enable/Disable', 754 | id: 'remoteEnable', 755 | default: 'toggle', 756 | choices: CHOICES_REMOTECONTROL, 757 | }, 758 | ], 759 | callback: async ({ options }) => { 760 | let setRemote = true 761 | if (options.remoteEnable == 'toggle') { 762 | setRemote = !self.remoteInfo?.enabled 763 | } else { 764 | setRemote = getOptBool(options, 'remoteEnable') 765 | } 766 | 767 | const cmd = new Commands.RemoteCommand() 768 | cmd.enable = setRemote 769 | 770 | await self.sendCommand(cmd) 771 | 772 | const newVariables: CompanionVariableValues = {} 773 | updateRemoteVariable(self, newVariables) 774 | self.setVariableValues(newVariables) 775 | self.checkFeedbacks('remote_status') 776 | }, 777 | } 778 | 779 | return actions 780 | } 781 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "@companion-module/base@npm:~1.11.3": 9 | version: 1.11.3 10 | resolution: "@companion-module/base@npm:1.11.3" 11 | dependencies: 12 | ajv: "npm:^8.17.1" 13 | colord: "npm:^2.9.3" 14 | ejson: "npm:^2.2.3" 15 | eventemitter3: "npm:^5.0.1" 16 | mimic-fn: "npm:^3.1.0" 17 | nanoid: "npm:^3.3.8" 18 | p-queue: "npm:^6.6.2" 19 | p-timeout: "npm:^4.1.0" 20 | tslib: "npm:^2.8.1" 21 | checksum: 10c0/19acbc544de97ed8f02abf03e1d657085e51f0e92363456a84bdc1975723ba1bdfc0e943ff17444cb4e6a2b588abb794a9c5b48efb4d784be32784362e7c4efd 22 | languageName: node 23 | linkType: hard 24 | 25 | "@companion-module/tools@npm:^2.3.0": 26 | version: 2.3.0 27 | resolution: "@companion-module/tools@npm:2.3.0" 28 | dependencies: 29 | "@eslint/js": "npm:^9.19.0" 30 | eslint-config-prettier: "npm:^10.0.1" 31 | eslint-plugin-n: "npm:^17.15.1" 32 | eslint-plugin-prettier: "npm:^5.2.3" 33 | find-up: "npm:^7.0.0" 34 | parse-author: "npm:^2.0.0" 35 | semver: "npm:^7.7.1" 36 | tar: "npm:^7.4.3" 37 | webpack: "npm:^5.97.1" 38 | webpack-cli: "npm:^6.0.1" 39 | zx: "npm:^8.3.2" 40 | peerDependencies: 41 | "@companion-module/base": ^1.11.0 42 | eslint: ^9.0.0 43 | prettier: ^3.0.0 44 | typescript-eslint: ^8.2.0 45 | peerDependenciesMeta: 46 | eslint: 47 | optional: true 48 | prettier: 49 | optional: true 50 | typescript-eslint: 51 | optional: true 52 | bin: 53 | companion-generate-manifest: scripts/generate-manifest.js 54 | companion-module-build: scripts/build.js 55 | companion-module-check: scripts/check.js 56 | checksum: 10c0/49baa2415a62e9763d8d185d3d944a68cbfb5e41f2d382a0d8e0cd69902ccc4a80a92783a0ab95c9b5fbc0ca4464c028423bb5fc3347149cd1bae1ab6bf4b10c 57 | languageName: node 58 | linkType: hard 59 | 60 | "@discoveryjs/json-ext@npm:^0.6.1": 61 | version: 0.6.3 62 | resolution: "@discoveryjs/json-ext@npm:0.6.3" 63 | checksum: 10c0/778a9f9d5c3696da3c1f9fa4186613db95a1090abbfb6c2601430645c0d0158cd5e4ba4f32c05904e2dd2747d57710f6aab22bd2f8aa3c4e8feab9b247c65d85 64 | languageName: node 65 | linkType: hard 66 | 67 | "@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.5.0": 68 | version: 4.6.1 69 | resolution: "@eslint-community/eslint-utils@npm:4.6.1" 70 | dependencies: 71 | eslint-visitor-keys: "npm:^3.4.3" 72 | peerDependencies: 73 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 74 | checksum: 10c0/cdeb6f8fc33a83726357d7f736075cdbd6e79dc7ac4b00b15680f1111d0f33bda583e7fafa5937245a058cc66302dc47568bba57b251302dc74964d8e87f56d7 75 | languageName: node 76 | linkType: hard 77 | 78 | "@eslint-community/regexpp@npm:^4.11.0": 79 | version: 4.12.1 80 | resolution: "@eslint-community/regexpp@npm:4.12.1" 81 | checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 82 | languageName: node 83 | linkType: hard 84 | 85 | "@eslint/js@npm:^9.19.0": 86 | version: 9.25.1 87 | resolution: "@eslint/js@npm:9.25.1" 88 | checksum: 10c0/87d86b512ab109bfd3b9317ced3220ea3d444ac3bfa7abd853ca7f724d72c36e213062f9def16a632365d97dc29e0094312e3682a9767590ee6f43b3d5d873fd 89 | languageName: node 90 | linkType: hard 91 | 92 | "@isaacs/fs-minipass@npm:^4.0.0": 93 | version: 4.0.1 94 | resolution: "@isaacs/fs-minipass@npm:4.0.1" 95 | dependencies: 96 | minipass: "npm:^7.0.4" 97 | checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 98 | languageName: node 99 | linkType: hard 100 | 101 | "@jridgewell/gen-mapping@npm:^0.3.5": 102 | version: 0.3.8 103 | resolution: "@jridgewell/gen-mapping@npm:0.3.8" 104 | dependencies: 105 | "@jridgewell/set-array": "npm:^1.2.1" 106 | "@jridgewell/sourcemap-codec": "npm:^1.4.10" 107 | "@jridgewell/trace-mapping": "npm:^0.3.24" 108 | checksum: 10c0/c668feaf86c501d7c804904a61c23c67447b2137b813b9ce03eca82cb9d65ac7006d766c218685d76e3d72828279b6ee26c347aa1119dab23fbaf36aed51585a 109 | languageName: node 110 | linkType: hard 111 | 112 | "@jridgewell/resolve-uri@npm:^3.1.0": 113 | version: 3.1.2 114 | resolution: "@jridgewell/resolve-uri@npm:3.1.2" 115 | checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e 116 | languageName: node 117 | linkType: hard 118 | 119 | "@jridgewell/set-array@npm:^1.2.1": 120 | version: 1.2.1 121 | resolution: "@jridgewell/set-array@npm:1.2.1" 122 | checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 123 | languageName: node 124 | linkType: hard 125 | 126 | "@jridgewell/source-map@npm:^0.3.3": 127 | version: 0.3.6 128 | resolution: "@jridgewell/source-map@npm:0.3.6" 129 | dependencies: 130 | "@jridgewell/gen-mapping": "npm:^0.3.5" 131 | "@jridgewell/trace-mapping": "npm:^0.3.25" 132 | checksum: 10c0/6a4ecc713ed246ff8e5bdcc1ef7c49aaa93f7463d948ba5054dda18b02dcc6a055e2828c577bcceee058f302ce1fc95595713d44f5c45e43d459f88d267f2f04 133 | languageName: node 134 | linkType: hard 135 | 136 | "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": 137 | version: 1.5.0 138 | resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" 139 | checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 140 | languageName: node 141 | linkType: hard 142 | 143 | "@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": 144 | version: 0.3.25 145 | resolution: "@jridgewell/trace-mapping@npm:0.3.25" 146 | dependencies: 147 | "@jridgewell/resolve-uri": "npm:^3.1.0" 148 | "@jridgewell/sourcemap-codec": "npm:^1.4.14" 149 | checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 150 | languageName: node 151 | linkType: hard 152 | 153 | "@pkgr/core@npm:^0.2.3": 154 | version: 0.2.4 155 | resolution: "@pkgr/core@npm:0.2.4" 156 | checksum: 10c0/2528a443bbbef5d4686614e1d73f834f19ccbc975f62b2a64974a6b97bcdf677b9c5e8948e04808ac4f0d853e2f422adfaae2a06e9e9f4f5cf8af76f1adf8dc1 157 | languageName: node 158 | linkType: hard 159 | 160 | "@types/eslint-scope@npm:^3.7.7": 161 | version: 3.7.7 162 | resolution: "@types/eslint-scope@npm:3.7.7" 163 | dependencies: 164 | "@types/eslint": "npm:*" 165 | "@types/estree": "npm:*" 166 | checksum: 10c0/a0ecbdf2f03912679440550817ff77ef39a30fa8bfdacaf6372b88b1f931828aec392f52283240f0d648cf3055c5ddc564544a626bcf245f3d09fcb099ebe3cc 167 | languageName: node 168 | linkType: hard 169 | 170 | "@types/eslint@npm:*": 171 | version: 9.6.1 172 | resolution: "@types/eslint@npm:9.6.1" 173 | dependencies: 174 | "@types/estree": "npm:*" 175 | "@types/json-schema": "npm:*" 176 | checksum: 10c0/69ba24fee600d1e4c5abe0df086c1a4d798abf13792d8cfab912d76817fe1a894359a1518557d21237fbaf6eda93c5ab9309143dee4c59ef54336d1b3570420e 177 | languageName: node 178 | linkType: hard 179 | 180 | "@types/estree@npm:*, @types/estree@npm:^1.0.6": 181 | version: 1.0.7 182 | resolution: "@types/estree@npm:1.0.7" 183 | checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c 184 | languageName: node 185 | linkType: hard 186 | 187 | "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": 188 | version: 7.0.15 189 | resolution: "@types/json-schema@npm:7.0.15" 190 | checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db 191 | languageName: node 192 | linkType: hard 193 | 194 | "@types/node@npm:*, @types/node@npm:^22.15.3": 195 | version: 22.15.3 196 | resolution: "@types/node@npm:22.15.3" 197 | dependencies: 198 | undici-types: "npm:~6.21.0" 199 | checksum: 10c0/2879f012d1aeba0bfdb5fed80d165f4f2cb3d1f2e1f98a24b18d4a211b4ace7d64bf2622784c78355982ffc1081ba79d0934efc2fb8353913e5871a63609661f 200 | languageName: node 201 | linkType: hard 202 | 203 | "@types/semver@npm:^7.7.0": 204 | version: 7.7.0 205 | resolution: "@types/semver@npm:7.7.0" 206 | checksum: 10c0/6b5f65f647474338abbd6ee91a6bbab434662ddb8fe39464edcbcfc96484d388baad9eb506dff217b6fc1727a88894930eb1f308617161ac0f376fe06be4e1ee 207 | languageName: node 208 | linkType: hard 209 | 210 | "@types/smpte-timecode@npm:^1.2.5": 211 | version: 1.2.5 212 | resolution: "@types/smpte-timecode@npm:1.2.5" 213 | checksum: 10c0/d57fd3d77b57f41aba6736255b1c62ccbd83b40354e8762d055368ac349e5280dbe35c8f869b2397c475755e4311c5062e0447479c4a12915e4af258bd702503 214 | languageName: node 215 | linkType: hard 216 | 217 | "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": 218 | version: 1.14.1 219 | resolution: "@webassemblyjs/ast@npm:1.14.1" 220 | dependencies: 221 | "@webassemblyjs/helper-numbers": "npm:1.13.2" 222 | "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" 223 | checksum: 10c0/67a59be8ed50ddd33fbb2e09daa5193ac215bf7f40a9371be9a0d9797a114d0d1196316d2f3943efdb923a3d809175e1563a3cb80c814fb8edccd1e77494972b 224 | languageName: node 225 | linkType: hard 226 | 227 | "@webassemblyjs/floating-point-hex-parser@npm:1.13.2": 228 | version: 1.13.2 229 | resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.13.2" 230 | checksum: 10c0/0e88bdb8b50507d9938be64df0867f00396b55eba9df7d3546eb5dc0ca64d62e06f8d881ec4a6153f2127d0f4c11d102b6e7d17aec2f26bb5ff95a5e60652412 231 | languageName: node 232 | linkType: hard 233 | 234 | "@webassemblyjs/helper-api-error@npm:1.13.2": 235 | version: 1.13.2 236 | resolution: "@webassemblyjs/helper-api-error@npm:1.13.2" 237 | checksum: 10c0/31be497f996ed30aae4c08cac3cce50c8dcd5b29660383c0155fce1753804fc55d47fcba74e10141c7dd2899033164e117b3bcfcda23a6b043e4ded4f1003dfb 238 | languageName: node 239 | linkType: hard 240 | 241 | "@webassemblyjs/helper-buffer@npm:1.14.1": 242 | version: 1.14.1 243 | resolution: "@webassemblyjs/helper-buffer@npm:1.14.1" 244 | checksum: 10c0/0d54105dc373c0fe6287f1091e41e3a02e36cdc05e8cf8533cdc16c59ff05a646355415893449d3768cda588af451c274f13263300a251dc11a575bc4c9bd210 245 | languageName: node 246 | linkType: hard 247 | 248 | "@webassemblyjs/helper-numbers@npm:1.13.2": 249 | version: 1.13.2 250 | resolution: "@webassemblyjs/helper-numbers@npm:1.13.2" 251 | dependencies: 252 | "@webassemblyjs/floating-point-hex-parser": "npm:1.13.2" 253 | "@webassemblyjs/helper-api-error": "npm:1.13.2" 254 | "@xtuc/long": "npm:4.2.2" 255 | checksum: 10c0/9c46852f31b234a8fb5a5a9d3f027bc542392a0d4de32f1a9c0075d5e8684aa073cb5929b56df565500b3f9cc0a2ab983b650314295b9bf208d1a1651bfc825a 256 | languageName: node 257 | linkType: hard 258 | 259 | "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2": 260 | version: 1.13.2 261 | resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2" 262 | checksum: 10c0/c4355d14f369b30cf3cbdd3acfafc7d0488e086be6d578e3c9780bd1b512932352246be96e034e2a7fcfba4f540ec813352f312bfcbbfe5bcfbf694f82ccc682 263 | languageName: node 264 | linkType: hard 265 | 266 | "@webassemblyjs/helper-wasm-section@npm:1.14.1": 267 | version: 1.14.1 268 | resolution: "@webassemblyjs/helper-wasm-section@npm:1.14.1" 269 | dependencies: 270 | "@webassemblyjs/ast": "npm:1.14.1" 271 | "@webassemblyjs/helper-buffer": "npm:1.14.1" 272 | "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" 273 | "@webassemblyjs/wasm-gen": "npm:1.14.1" 274 | checksum: 10c0/1f9b33731c3c6dbac3a9c483269562fa00d1b6a4e7133217f40e83e975e636fd0f8736e53abd9a47b06b66082ecc976c7384391ab0a68e12d509ea4e4b948d64 275 | languageName: node 276 | linkType: hard 277 | 278 | "@webassemblyjs/ieee754@npm:1.13.2": 279 | version: 1.13.2 280 | resolution: "@webassemblyjs/ieee754@npm:1.13.2" 281 | dependencies: 282 | "@xtuc/ieee754": "npm:^1.2.0" 283 | checksum: 10c0/2e732ca78c6fbae3c9b112f4915d85caecdab285c0b337954b180460290ccd0fb00d2b1dc4bb69df3504abead5191e0d28d0d17dfd6c9d2f30acac8c4961c8a7 284 | languageName: node 285 | linkType: hard 286 | 287 | "@webassemblyjs/leb128@npm:1.13.2": 288 | version: 1.13.2 289 | resolution: "@webassemblyjs/leb128@npm:1.13.2" 290 | dependencies: 291 | "@xtuc/long": "npm:4.2.2" 292 | checksum: 10c0/dad5ef9e383c8ab523ce432dfd80098384bf01c45f70eb179d594f85ce5db2f80fa8c9cba03adafd85684e6d6310f0d3969a882538975989919329ac4c984659 293 | languageName: node 294 | linkType: hard 295 | 296 | "@webassemblyjs/utf8@npm:1.13.2": 297 | version: 1.13.2 298 | resolution: "@webassemblyjs/utf8@npm:1.13.2" 299 | checksum: 10c0/d3fac9130b0e3e5a1a7f2886124a278e9323827c87a2b971e6d0da22a2ba1278ac9f66a4f2e363ecd9fac8da42e6941b22df061a119e5c0335f81006de9ee799 300 | languageName: node 301 | linkType: hard 302 | 303 | "@webassemblyjs/wasm-edit@npm:^1.14.1": 304 | version: 1.14.1 305 | resolution: "@webassemblyjs/wasm-edit@npm:1.14.1" 306 | dependencies: 307 | "@webassemblyjs/ast": "npm:1.14.1" 308 | "@webassemblyjs/helper-buffer": "npm:1.14.1" 309 | "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" 310 | "@webassemblyjs/helper-wasm-section": "npm:1.14.1" 311 | "@webassemblyjs/wasm-gen": "npm:1.14.1" 312 | "@webassemblyjs/wasm-opt": "npm:1.14.1" 313 | "@webassemblyjs/wasm-parser": "npm:1.14.1" 314 | "@webassemblyjs/wast-printer": "npm:1.14.1" 315 | checksum: 10c0/5ac4781086a2ca4b320bdbfd965a209655fe8a208ca38d89197148f8597e587c9a2c94fb6bd6f1a7dbd4527c49c6844fcdc2af981f8d793a97bf63a016aa86d2 316 | languageName: node 317 | linkType: hard 318 | 319 | "@webassemblyjs/wasm-gen@npm:1.14.1": 320 | version: 1.14.1 321 | resolution: "@webassemblyjs/wasm-gen@npm:1.14.1" 322 | dependencies: 323 | "@webassemblyjs/ast": "npm:1.14.1" 324 | "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" 325 | "@webassemblyjs/ieee754": "npm:1.13.2" 326 | "@webassemblyjs/leb128": "npm:1.13.2" 327 | "@webassemblyjs/utf8": "npm:1.13.2" 328 | checksum: 10c0/d678810d7f3f8fecb2e2bdadfb9afad2ec1d2bc79f59e4711ab49c81cec578371e22732d4966f59067abe5fba8e9c54923b57060a729d28d408e608beef67b10 329 | languageName: node 330 | linkType: hard 331 | 332 | "@webassemblyjs/wasm-opt@npm:1.14.1": 333 | version: 1.14.1 334 | resolution: "@webassemblyjs/wasm-opt@npm:1.14.1" 335 | dependencies: 336 | "@webassemblyjs/ast": "npm:1.14.1" 337 | "@webassemblyjs/helper-buffer": "npm:1.14.1" 338 | "@webassemblyjs/wasm-gen": "npm:1.14.1" 339 | "@webassemblyjs/wasm-parser": "npm:1.14.1" 340 | checksum: 10c0/515bfb15277ee99ba6b11d2232ddbf22aed32aad6d0956fe8a0a0a004a1b5a3a277a71d9a3a38365d0538ac40d1b7b7243b1a244ad6cd6dece1c1bb2eb5de7ee 341 | languageName: node 342 | linkType: hard 343 | 344 | "@webassemblyjs/wasm-parser@npm:1.14.1, @webassemblyjs/wasm-parser@npm:^1.14.1": 345 | version: 1.14.1 346 | resolution: "@webassemblyjs/wasm-parser@npm:1.14.1" 347 | dependencies: 348 | "@webassemblyjs/ast": "npm:1.14.1" 349 | "@webassemblyjs/helper-api-error": "npm:1.13.2" 350 | "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2" 351 | "@webassemblyjs/ieee754": "npm:1.13.2" 352 | "@webassemblyjs/leb128": "npm:1.13.2" 353 | "@webassemblyjs/utf8": "npm:1.13.2" 354 | checksum: 10c0/95427b9e5addbd0f647939bd28e3e06b8deefdbdadcf892385b5edc70091bf9b92fa5faac3fce8333554437c5d85835afef8c8a7d9d27ab6ba01ffab954db8c6 355 | languageName: node 356 | linkType: hard 357 | 358 | "@webassemblyjs/wast-printer@npm:1.14.1": 359 | version: 1.14.1 360 | resolution: "@webassemblyjs/wast-printer@npm:1.14.1" 361 | dependencies: 362 | "@webassemblyjs/ast": "npm:1.14.1" 363 | "@xtuc/long": "npm:4.2.2" 364 | checksum: 10c0/8d7768608996a052545251e896eac079c98e0401842af8dd4de78fba8d90bd505efb6c537e909cd6dae96e09db3fa2e765a6f26492553a675da56e2db51f9d24 365 | languageName: node 366 | linkType: hard 367 | 368 | "@webpack-cli/configtest@npm:^3.0.1": 369 | version: 3.0.1 370 | resolution: "@webpack-cli/configtest@npm:3.0.1" 371 | peerDependencies: 372 | webpack: ^5.82.0 373 | webpack-cli: 6.x.x 374 | checksum: 10c0/edd24ecfc429298fe86446f7d7daedfe82d72e7f6236c81420605484fdadade5d59c6bcef3d76bd724e11d9727f74e75de183223ae62d3a568b2d54199688cbe 375 | languageName: node 376 | linkType: hard 377 | 378 | "@webpack-cli/info@npm:^3.0.1": 379 | version: 3.0.1 380 | resolution: "@webpack-cli/info@npm:3.0.1" 381 | peerDependencies: 382 | webpack: ^5.82.0 383 | webpack-cli: 6.x.x 384 | checksum: 10c0/b23b94e7dc8c93e79248f20d5f1bd0fbb7b9ba4b012803e2fdc5440b8f2ee1f3eca7f4933bbca346c8168673bf572b1858169a3cb2c17d9b8bcd833d480c2170 385 | languageName: node 386 | linkType: hard 387 | 388 | "@webpack-cli/serve@npm:^3.0.1": 389 | version: 3.0.1 390 | resolution: "@webpack-cli/serve@npm:3.0.1" 391 | peerDependencies: 392 | webpack: ^5.82.0 393 | webpack-cli: 6.x.x 394 | peerDependenciesMeta: 395 | webpack-dev-server: 396 | optional: true 397 | checksum: 10c0/65245e45bfa35e11a5b30631b99cfed0c1b39b2cc8320fa2d2a4185264535618827d349ec032c58af4201d6236cbc43bec894fcb840fdd06314611537a80e210 398 | languageName: node 399 | linkType: hard 400 | 401 | "@xtuc/ieee754@npm:^1.2.0": 402 | version: 1.2.0 403 | resolution: "@xtuc/ieee754@npm:1.2.0" 404 | checksum: 10c0/a8565d29d135039bd99ae4b2220d3e167d22cf53f867e491ed479b3f84f895742d0097f935b19aab90265a23d5d46711e4204f14c479ae3637fbf06c4666882f 405 | languageName: node 406 | linkType: hard 407 | 408 | "@xtuc/long@npm:4.2.2": 409 | version: 4.2.2 410 | resolution: "@xtuc/long@npm:4.2.2" 411 | checksum: 10c0/8582cbc69c79ad2d31568c412129bf23d2b1210a1dfb60c82d5a1df93334da4ee51f3057051658569e2c196d8dc33bc05ae6b974a711d0d16e801e1d0647ccd1 412 | languageName: node 413 | linkType: hard 414 | 415 | "acorn@npm:^8.14.0, acorn@npm:^8.8.2": 416 | version: 8.14.1 417 | resolution: "acorn@npm:8.14.1" 418 | bin: 419 | acorn: bin/acorn 420 | checksum: 10c0/dbd36c1ed1d2fa3550140000371fcf721578095b18777b85a79df231ca093b08edc6858d75d6e48c73e431c174dcf9214edbd7e6fa5911b93bd8abfa54e47123 421 | languageName: node 422 | linkType: hard 423 | 424 | "ajv-formats@npm:^2.1.1": 425 | version: 2.1.1 426 | resolution: "ajv-formats@npm:2.1.1" 427 | dependencies: 428 | ajv: "npm:^8.0.0" 429 | peerDependencies: 430 | ajv: ^8.0.0 431 | peerDependenciesMeta: 432 | ajv: 433 | optional: true 434 | checksum: 10c0/e43ba22e91b6a48d96224b83d260d3a3a561b42d391f8d3c6d2c1559f9aa5b253bfb306bc94bbeca1d967c014e15a6efe9a207309e95b3eaae07fcbcdc2af662 435 | languageName: node 436 | linkType: hard 437 | 438 | "ajv-keywords@npm:^5.1.0": 439 | version: 5.1.0 440 | resolution: "ajv-keywords@npm:5.1.0" 441 | dependencies: 442 | fast-deep-equal: "npm:^3.1.3" 443 | peerDependencies: 444 | ajv: ^8.8.2 445 | checksum: 10c0/18bec51f0171b83123ba1d8883c126e60c6f420cef885250898bf77a8d3e65e3bfb9e8564f497e30bdbe762a83e0d144a36931328616a973ee669dc74d4a9590 446 | languageName: node 447 | linkType: hard 448 | 449 | "ajv@npm:^8.0.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": 450 | version: 8.17.1 451 | resolution: "ajv@npm:8.17.1" 452 | dependencies: 453 | fast-deep-equal: "npm:^3.1.3" 454 | fast-uri: "npm:^3.0.1" 455 | json-schema-traverse: "npm:^1.0.0" 456 | require-from-string: "npm:^2.0.2" 457 | checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35 458 | languageName: node 459 | linkType: hard 460 | 461 | "author-regex@npm:^1.0.0": 462 | version: 1.0.0 463 | resolution: "author-regex@npm:1.0.0" 464 | checksum: 10c0/3f3a5ad6660be010bd5b979fac180f435bd9615e81db2b1cdac081eb3f639461f6c3927ced956e377a5c91cc789e3de3a3e900e296c1423971043c8fd8be0b73 465 | languageName: node 466 | linkType: hard 467 | 468 | "balanced-match@npm:^1.0.0": 469 | version: 1.0.2 470 | resolution: "balanced-match@npm:1.0.2" 471 | checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee 472 | languageName: node 473 | linkType: hard 474 | 475 | "bmd-hyperdeck@workspace:.": 476 | version: 0.0.0-use.local 477 | resolution: "bmd-hyperdeck@workspace:." 478 | dependencies: 479 | "@companion-module/base": "npm:~1.11.3" 480 | "@companion-module/tools": "npm:^2.3.0" 481 | "@types/node": "npm:^22.15.3" 482 | "@types/semver": "npm:^7.7.0" 483 | "@types/smpte-timecode": "npm:^1.2.5" 484 | hyperdeck-connection: "npm:2.0.1" 485 | prettier: "npm:^3.5.3" 486 | semver: "npm:^7.7.1" 487 | smpte-timecode: "npm:^1.3.6" 488 | typescript: "npm:~5.8.3" 489 | languageName: unknown 490 | linkType: soft 491 | 492 | "brace-expansion@npm:^2.0.1": 493 | version: 2.0.1 494 | resolution: "brace-expansion@npm:2.0.1" 495 | dependencies: 496 | balanced-match: "npm:^1.0.0" 497 | checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f 498 | languageName: node 499 | linkType: hard 500 | 501 | "browserslist@npm:^4.24.0": 502 | version: 4.24.4 503 | resolution: "browserslist@npm:4.24.4" 504 | dependencies: 505 | caniuse-lite: "npm:^1.0.30001688" 506 | electron-to-chromium: "npm:^1.5.73" 507 | node-releases: "npm:^2.0.19" 508 | update-browserslist-db: "npm:^1.1.1" 509 | bin: 510 | browserslist: cli.js 511 | checksum: 10c0/db7ebc1733cf471e0b490b4f47e3e2ea2947ce417192c9246644e92c667dd56a71406cc58f62ca7587caf828364892e9952904a02b7aead752bc65b62a37cfe9 512 | languageName: node 513 | linkType: hard 514 | 515 | "buffer-from@npm:^1.0.0": 516 | version: 1.1.2 517 | resolution: "buffer-from@npm:1.1.2" 518 | checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 519 | languageName: node 520 | linkType: hard 521 | 522 | "caniuse-lite@npm:^1.0.30001688": 523 | version: 1.0.30001716 524 | resolution: "caniuse-lite@npm:1.0.30001716" 525 | checksum: 10c0/5cca5089f7ee214a346ea38ecbd114c1a675c94254675150e535aa0766fe2c446961990637f25f65dc29ce9c543aed12006d679dd797d2e6159c46aa518da0cb 526 | languageName: node 527 | linkType: hard 528 | 529 | "chownr@npm:^3.0.0": 530 | version: 3.0.0 531 | resolution: "chownr@npm:3.0.0" 532 | checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 533 | languageName: node 534 | linkType: hard 535 | 536 | "chrome-trace-event@npm:^1.0.2": 537 | version: 1.0.4 538 | resolution: "chrome-trace-event@npm:1.0.4" 539 | checksum: 10c0/3058da7a5f4934b87cf6a90ef5fb68ebc5f7d06f143ed5a4650208e5d7acae47bc03ec844b29fbf5ba7e46e8daa6acecc878f7983a4f4bb7271593da91e61ff5 540 | languageName: node 541 | linkType: hard 542 | 543 | "clone-deep@npm:^4.0.1": 544 | version: 4.0.1 545 | resolution: "clone-deep@npm:4.0.1" 546 | dependencies: 547 | is-plain-object: "npm:^2.0.4" 548 | kind-of: "npm:^6.0.2" 549 | shallow-clone: "npm:^3.0.0" 550 | checksum: 10c0/637753615aa24adf0f2d505947a1bb75e63964309034a1cf56ba4b1f30af155201edd38d26ffe26911adaae267a3c138b344a4947d39f5fc1b6d6108125aa758 551 | languageName: node 552 | linkType: hard 553 | 554 | "colord@npm:^2.9.3": 555 | version: 2.9.3 556 | resolution: "colord@npm:2.9.3" 557 | checksum: 10c0/9699e956894d8996b28c686afe8988720785f476f59335c80ce852ded76ab3ebe252703aec53d9bef54f6219aea6b960fb3d9a8300058a1d0c0d4026460cd110 558 | languageName: node 559 | linkType: hard 560 | 561 | "colorette@npm:^2.0.14": 562 | version: 2.0.20 563 | resolution: "colorette@npm:2.0.20" 564 | checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 565 | languageName: node 566 | linkType: hard 567 | 568 | "commander@npm:^12.1.0": 569 | version: 12.1.0 570 | resolution: "commander@npm:12.1.0" 571 | checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 572 | languageName: node 573 | linkType: hard 574 | 575 | "commander@npm:^2.20.0": 576 | version: 2.20.3 577 | resolution: "commander@npm:2.20.3" 578 | checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 579 | languageName: node 580 | linkType: hard 581 | 582 | "cross-spawn@npm:^7.0.3": 583 | version: 7.0.6 584 | resolution: "cross-spawn@npm:7.0.6" 585 | dependencies: 586 | path-key: "npm:^3.1.0" 587 | shebang-command: "npm:^2.0.0" 588 | which: "npm:^2.0.1" 589 | checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 590 | languageName: node 591 | linkType: hard 592 | 593 | "ejson@npm:^2.2.3": 594 | version: 2.2.3 595 | resolution: "ejson@npm:2.2.3" 596 | checksum: 10c0/648ea347f5e57441b7b9341adc6de244445b6da1d0e7747ea7a083f906299b92e4c44fc29e6de0b240d8fa4a73159e85f9367780d7af2ecbc50aae8a4e4961ae 597 | languageName: node 598 | linkType: hard 599 | 600 | "electron-to-chromium@npm:^1.5.73": 601 | version: 1.5.148 602 | resolution: "electron-to-chromium@npm:1.5.148" 603 | checksum: 10c0/e6de6bf2f632775ef426d346f0453ff9551c321a5d0e0a336e6c646e43b723ec0d2ce95199f3f3ab5ccb8a9c1996a409eaae056954366c491113928c66f60173 604 | languageName: node 605 | linkType: hard 606 | 607 | "enhanced-resolve@npm:^5.17.1": 608 | version: 5.18.1 609 | resolution: "enhanced-resolve@npm:5.18.1" 610 | dependencies: 611 | graceful-fs: "npm:^4.2.4" 612 | tapable: "npm:^2.2.0" 613 | checksum: 10c0/4cffd9b125225184e2abed9fdf0ed3dbd2224c873b165d0838fd066cde32e0918626cba2f1f4bf6860762f13a7e2364fd89a82b99566be2873d813573ac71846 614 | languageName: node 615 | linkType: hard 616 | 617 | "envinfo@npm:^7.14.0": 618 | version: 7.14.0 619 | resolution: "envinfo@npm:7.14.0" 620 | bin: 621 | envinfo: dist/cli.js 622 | checksum: 10c0/059a031eee101e056bd9cc5cbfe25c2fab433fe1780e86cf0a82d24a000c6931e327da6a8ffb3dce528a24f83f256e7efc0b36813113eff8fdc6839018efe327 623 | languageName: node 624 | linkType: hard 625 | 626 | "es-module-lexer@npm:^1.2.1": 627 | version: 1.7.0 628 | resolution: "es-module-lexer@npm:1.7.0" 629 | checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b 630 | languageName: node 631 | linkType: hard 632 | 633 | "escalade@npm:^3.2.0": 634 | version: 3.2.0 635 | resolution: "escalade@npm:3.2.0" 636 | checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 637 | languageName: node 638 | linkType: hard 639 | 640 | "eslint-compat-utils@npm:^0.5.1": 641 | version: 0.5.1 642 | resolution: "eslint-compat-utils@npm:0.5.1" 643 | dependencies: 644 | semver: "npm:^7.5.4" 645 | peerDependencies: 646 | eslint: ">=6.0.0" 647 | checksum: 10c0/325e815205fab70ebcd379f6d4b5d44c7d791bb8dfe0c9888233f30ebabd9418422595b53a781b946c768d9244d858540e5e6129a6b3dd6d606f467d599edc6c 648 | languageName: node 649 | linkType: hard 650 | 651 | "eslint-config-prettier@npm:^10.0.1": 652 | version: 10.1.2 653 | resolution: "eslint-config-prettier@npm:10.1.2" 654 | peerDependencies: 655 | eslint: ">=7.0.0" 656 | bin: 657 | eslint-config-prettier: bin/cli.js 658 | checksum: 10c0/c22c8e29193cc8fd70becf1c2dd072513f2b3004a175c2a49404c79d1745ba4dc0edc2afd00d16b0e26d24f95813a0469e7445a25104aec218f6d84cdb1697e9 659 | languageName: node 660 | linkType: hard 661 | 662 | "eslint-plugin-es-x@npm:^7.8.0": 663 | version: 7.8.0 664 | resolution: "eslint-plugin-es-x@npm:7.8.0" 665 | dependencies: 666 | "@eslint-community/eslint-utils": "npm:^4.1.2" 667 | "@eslint-community/regexpp": "npm:^4.11.0" 668 | eslint-compat-utils: "npm:^0.5.1" 669 | peerDependencies: 670 | eslint: ">=8" 671 | checksum: 10c0/002fda8c029bc5da41e24e7ac11654062831d675fc4f5f20d0de460e24bf1e05cd559000678ef3e46c48641190f4fc07ae3d57aa5e8b085ef5f67e5f63742614 672 | languageName: node 673 | linkType: hard 674 | 675 | "eslint-plugin-n@npm:^17.15.1": 676 | version: 17.17.0 677 | resolution: "eslint-plugin-n@npm:17.17.0" 678 | dependencies: 679 | "@eslint-community/eslint-utils": "npm:^4.5.0" 680 | enhanced-resolve: "npm:^5.17.1" 681 | eslint-plugin-es-x: "npm:^7.8.0" 682 | get-tsconfig: "npm:^4.8.1" 683 | globals: "npm:^15.11.0" 684 | ignore: "npm:^5.3.2" 685 | minimatch: "npm:^9.0.5" 686 | semver: "npm:^7.6.3" 687 | peerDependencies: 688 | eslint: ">=8.23.0" 689 | checksum: 10c0/ac6b2e2bbdc8f49a84be1bf1add8a412093a56fe95e8820610ecd5185fa00a348197a06fe3fe36080c09dc5d5a8f0f4f543cb3cf193265ace3fd071a79a07e88 690 | languageName: node 691 | linkType: hard 692 | 693 | "eslint-plugin-prettier@npm:^5.2.3": 694 | version: 5.2.6 695 | resolution: "eslint-plugin-prettier@npm:5.2.6" 696 | dependencies: 697 | prettier-linter-helpers: "npm:^1.0.0" 698 | synckit: "npm:^0.11.0" 699 | peerDependencies: 700 | "@types/eslint": ">=8.0.0" 701 | eslint: ">=8.0.0" 702 | eslint-config-prettier: ">= 7.0.0 <10.0.0 || >=10.1.0" 703 | prettier: ">=3.0.0" 704 | peerDependenciesMeta: 705 | "@types/eslint": 706 | optional: true 707 | eslint-config-prettier: 708 | optional: true 709 | checksum: 10c0/9911740a5edac7933d92671381908671c61ffa32a3cee7aed667ebab89831ee2c0b69eb9530f68dbe172ca9d4b3fa3d47350762dc1eb096a3ce125fa31c0e616 710 | languageName: node 711 | linkType: hard 712 | 713 | "eslint-scope@npm:5.1.1": 714 | version: 5.1.1 715 | resolution: "eslint-scope@npm:5.1.1" 716 | dependencies: 717 | esrecurse: "npm:^4.3.0" 718 | estraverse: "npm:^4.1.1" 719 | checksum: 10c0/d30ef9dc1c1cbdece34db1539a4933fe3f9b14e1ffb27ecc85987902ee663ad7c9473bbd49a9a03195a373741e62e2f807c4938992e019b511993d163450e70a 720 | languageName: node 721 | linkType: hard 722 | 723 | "eslint-visitor-keys@npm:^3.4.3": 724 | version: 3.4.3 725 | resolution: "eslint-visitor-keys@npm:3.4.3" 726 | checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 727 | languageName: node 728 | linkType: hard 729 | 730 | "esrecurse@npm:^4.3.0": 731 | version: 4.3.0 732 | resolution: "esrecurse@npm:4.3.0" 733 | dependencies: 734 | estraverse: "npm:^5.2.0" 735 | checksum: 10c0/81a37116d1408ded88ada45b9fb16dbd26fba3aadc369ce50fcaf82a0bac12772ebd7b24cd7b91fc66786bf2c1ac7b5f196bc990a473efff972f5cb338877cf5 736 | languageName: node 737 | linkType: hard 738 | 739 | "estraverse@npm:^4.1.1": 740 | version: 4.3.0 741 | resolution: "estraverse@npm:4.3.0" 742 | checksum: 10c0/9cb46463ef8a8a4905d3708a652d60122a0c20bb58dec7e0e12ab0e7235123d74214fc0141d743c381813e1b992767e2708194f6f6e0f9fd00c1b4e0887b8b6d 743 | languageName: node 744 | linkType: hard 745 | 746 | "estraverse@npm:^5.2.0": 747 | version: 5.3.0 748 | resolution: "estraverse@npm:5.3.0" 749 | checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 750 | languageName: node 751 | linkType: hard 752 | 753 | "eventemitter3@npm:^4.0.4, eventemitter3@npm:^4.0.7": 754 | version: 4.0.7 755 | resolution: "eventemitter3@npm:4.0.7" 756 | checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b 757 | languageName: node 758 | linkType: hard 759 | 760 | "eventemitter3@npm:^5.0.1": 761 | version: 5.0.1 762 | resolution: "eventemitter3@npm:5.0.1" 763 | checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 764 | languageName: node 765 | linkType: hard 766 | 767 | "events@npm:^3.2.0": 768 | version: 3.3.0 769 | resolution: "events@npm:3.3.0" 770 | checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 771 | languageName: node 772 | linkType: hard 773 | 774 | "fast-deep-equal@npm:^3.1.3": 775 | version: 3.1.3 776 | resolution: "fast-deep-equal@npm:3.1.3" 777 | checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 778 | languageName: node 779 | linkType: hard 780 | 781 | "fast-diff@npm:^1.1.2": 782 | version: 1.3.0 783 | resolution: "fast-diff@npm:1.3.0" 784 | checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 785 | languageName: node 786 | linkType: hard 787 | 788 | "fast-uri@npm:^3.0.1": 789 | version: 3.0.6 790 | resolution: "fast-uri@npm:3.0.6" 791 | checksum: 10c0/74a513c2af0584448aee71ce56005185f81239eab7a2343110e5bad50c39ad4fb19c5a6f99783ead1cac7ccaf3461a6034fda89fffa2b30b6d99b9f21c2f9d29 792 | languageName: node 793 | linkType: hard 794 | 795 | "fastest-levenshtein@npm:^1.0.12": 796 | version: 1.0.16 797 | resolution: "fastest-levenshtein@npm:1.0.16" 798 | checksum: 10c0/7e3d8ae812a7f4fdf8cad18e9cde436a39addf266a5986f653ea0d81e0de0900f50c0f27c6d5aff3f686bcb48acbd45be115ae2216f36a6a13a7dbbf5cad878b 799 | languageName: node 800 | linkType: hard 801 | 802 | "find-up@npm:^4.0.0": 803 | version: 4.1.0 804 | resolution: "find-up@npm:4.1.0" 805 | dependencies: 806 | locate-path: "npm:^5.0.0" 807 | path-exists: "npm:^4.0.0" 808 | checksum: 10c0/0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 809 | languageName: node 810 | linkType: hard 811 | 812 | "find-up@npm:^7.0.0": 813 | version: 7.0.0 814 | resolution: "find-up@npm:7.0.0" 815 | dependencies: 816 | locate-path: "npm:^7.2.0" 817 | path-exists: "npm:^5.0.0" 818 | unicorn-magic: "npm:^0.1.0" 819 | checksum: 10c0/e6ee3e6154560bc0ab3bc3b7d1348b31513f9bdf49a5dd2e952495427d559fa48cdf33953e85a309a323898b43fa1bfbc8b80c880dfc16068384783034030008 820 | languageName: node 821 | linkType: hard 822 | 823 | "flat@npm:^5.0.2": 824 | version: 5.0.2 825 | resolution: "flat@npm:5.0.2" 826 | bin: 827 | flat: cli.js 828 | checksum: 10c0/f178b13482f0cd80c7fede05f4d10585b1f2fdebf26e12edc138e32d3150c6ea6482b7f12813a1091143bad52bb6d3596bca51a162257a21163c0ff438baa5fe 829 | languageName: node 830 | linkType: hard 831 | 832 | "function-bind@npm:^1.1.2": 833 | version: 1.1.2 834 | resolution: "function-bind@npm:1.1.2" 835 | checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 836 | languageName: node 837 | linkType: hard 838 | 839 | "get-tsconfig@npm:^4.8.1": 840 | version: 4.10.0 841 | resolution: "get-tsconfig@npm:4.10.0" 842 | dependencies: 843 | resolve-pkg-maps: "npm:^1.0.0" 844 | checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86 845 | languageName: node 846 | linkType: hard 847 | 848 | "glob-to-regexp@npm:^0.4.1": 849 | version: 0.4.1 850 | resolution: "glob-to-regexp@npm:0.4.1" 851 | checksum: 10c0/0486925072d7a916f052842772b61c3e86247f0a80cc0deb9b5a3e8a1a9faad5b04fb6f58986a09f34d3e96cd2a22a24b7e9882fb1cf904c31e9a310de96c429 852 | languageName: node 853 | linkType: hard 854 | 855 | "globals@npm:^15.11.0": 856 | version: 15.15.0 857 | resolution: "globals@npm:15.15.0" 858 | checksum: 10c0/f9ae80996392ca71316495a39bec88ac43ae3525a438b5626cd9d5ce9d5500d0a98a266409605f8cd7241c7acf57c354a48111ea02a767ba4f374b806d6861fe 859 | languageName: node 860 | linkType: hard 861 | 862 | "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4": 863 | version: 4.2.11 864 | resolution: "graceful-fs@npm:4.2.11" 865 | checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 866 | languageName: node 867 | linkType: hard 868 | 869 | "has-flag@npm:^4.0.0": 870 | version: 4.0.0 871 | resolution: "has-flag@npm:4.0.0" 872 | checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 873 | languageName: node 874 | linkType: hard 875 | 876 | "hasown@npm:^2.0.2": 877 | version: 2.0.2 878 | resolution: "hasown@npm:2.0.2" 879 | dependencies: 880 | function-bind: "npm:^1.1.2" 881 | checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 882 | languageName: node 883 | linkType: hard 884 | 885 | "hyperdeck-connection@npm:2.0.1": 886 | version: 2.0.1 887 | resolution: "hyperdeck-connection@npm:2.0.1" 888 | dependencies: 889 | eventemitter3: "npm:^4.0.7" 890 | tslib: "npm:^2.6.2" 891 | checksum: 10c0/1046ae30504978737c5efd36476b035edb294f0ab64507acd0b4760463673f0f71848bf66cfbaf72ded937d119a1cbf6f8d51661d3228adb4a7a0a9b364f0e3f 892 | languageName: node 893 | linkType: hard 894 | 895 | "ignore@npm:^5.3.2": 896 | version: 5.3.2 897 | resolution: "ignore@npm:5.3.2" 898 | checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 899 | languageName: node 900 | linkType: hard 901 | 902 | "import-local@npm:^3.0.2": 903 | version: 3.2.0 904 | resolution: "import-local@npm:3.2.0" 905 | dependencies: 906 | pkg-dir: "npm:^4.2.0" 907 | resolve-cwd: "npm:^3.0.0" 908 | bin: 909 | import-local-fixture: fixtures/cli.js 910 | checksum: 10c0/94cd6367a672b7e0cb026970c85b76902d2710a64896fa6de93bd5c571dd03b228c5759308959de205083e3b1c61e799f019c9e36ee8e9c523b993e1057f0433 911 | languageName: node 912 | linkType: hard 913 | 914 | "interpret@npm:^3.1.1": 915 | version: 3.1.1 916 | resolution: "interpret@npm:3.1.1" 917 | checksum: 10c0/6f3c4d0aa6ec1b43a8862375588a249e3c917739895cbe67fe12f0a76260ea632af51e8e2431b50fbcd0145356dc28ca147be08dbe6a523739fd55c0f91dc2a5 918 | languageName: node 919 | linkType: hard 920 | 921 | "is-core-module@npm:^2.16.0": 922 | version: 2.16.1 923 | resolution: "is-core-module@npm:2.16.1" 924 | dependencies: 925 | hasown: "npm:^2.0.2" 926 | checksum: 10c0/898443c14780a577e807618aaae2b6f745c8538eca5c7bc11388a3f2dc6de82b9902bcc7eb74f07be672b11bbe82dd6a6edded44a00cb3d8f933d0459905eedd 927 | languageName: node 928 | linkType: hard 929 | 930 | "is-plain-object@npm:^2.0.4": 931 | version: 2.0.4 932 | resolution: "is-plain-object@npm:2.0.4" 933 | dependencies: 934 | isobject: "npm:^3.0.1" 935 | checksum: 10c0/f050fdd5203d9c81e8c4df1b3ff461c4bc64e8b5ca383bcdde46131361d0a678e80bcf00b5257646f6c636197629644d53bd8e2375aea633de09a82d57e942f4 936 | languageName: node 937 | linkType: hard 938 | 939 | "isexe@npm:^2.0.0": 940 | version: 2.0.0 941 | resolution: "isexe@npm:2.0.0" 942 | checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d 943 | languageName: node 944 | linkType: hard 945 | 946 | "isobject@npm:^3.0.1": 947 | version: 3.0.1 948 | resolution: "isobject@npm:3.0.1" 949 | checksum: 10c0/03344f5064a82f099a0cd1a8a407f4c0d20b7b8485e8e816c39f249e9416b06c322e8dec5b842b6bb8a06de0af9cb48e7bc1b5352f0fadc2f0abac033db3d4db 950 | languageName: node 951 | linkType: hard 952 | 953 | "jest-worker@npm:^27.4.5": 954 | version: 27.5.1 955 | resolution: "jest-worker@npm:27.5.1" 956 | dependencies: 957 | "@types/node": "npm:*" 958 | merge-stream: "npm:^2.0.0" 959 | supports-color: "npm:^8.0.0" 960 | checksum: 10c0/8c4737ffd03887b3c6768e4cc3ca0269c0336c1e4b1b120943958ddb035ed2a0fc6acab6dc99631720a3720af4e708ff84fb45382ad1e83c27946adf3623969b 961 | languageName: node 962 | linkType: hard 963 | 964 | "json-parse-even-better-errors@npm:^2.3.1": 965 | version: 2.3.1 966 | resolution: "json-parse-even-better-errors@npm:2.3.1" 967 | checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 968 | languageName: node 969 | linkType: hard 970 | 971 | "json-schema-traverse@npm:^1.0.0": 972 | version: 1.0.0 973 | resolution: "json-schema-traverse@npm:1.0.0" 974 | checksum: 10c0/71e30015d7f3d6dc1c316d6298047c8ef98a06d31ad064919976583eb61e1018a60a0067338f0f79cabc00d84af3fcc489bd48ce8a46ea165d9541ba17fb30c6 975 | languageName: node 976 | linkType: hard 977 | 978 | "kind-of@npm:^6.0.2": 979 | version: 6.0.3 980 | resolution: "kind-of@npm:6.0.3" 981 | checksum: 10c0/61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 982 | languageName: node 983 | linkType: hard 984 | 985 | "loader-runner@npm:^4.2.0": 986 | version: 4.3.0 987 | resolution: "loader-runner@npm:4.3.0" 988 | checksum: 10c0/a44d78aae0907a72f73966fe8b82d1439c8c485238bd5a864b1b9a2a3257832effa858790241e6b37876b5446a78889adf2fcc8dd897ce54c089ecc0a0ce0bf0 989 | languageName: node 990 | linkType: hard 991 | 992 | "locate-path@npm:^5.0.0": 993 | version: 5.0.0 994 | resolution: "locate-path@npm:5.0.0" 995 | dependencies: 996 | p-locate: "npm:^4.1.0" 997 | checksum: 10c0/33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 998 | languageName: node 999 | linkType: hard 1000 | 1001 | "locate-path@npm:^7.2.0": 1002 | version: 7.2.0 1003 | resolution: "locate-path@npm:7.2.0" 1004 | dependencies: 1005 | p-locate: "npm:^6.0.0" 1006 | checksum: 10c0/139e8a7fe11cfbd7f20db03923cacfa5db9e14fa14887ea121345597472b4a63c1a42a8a5187defeeff6acf98fd568da7382aa39682d38f0af27433953a97751 1007 | languageName: node 1008 | linkType: hard 1009 | 1010 | "merge-stream@npm:^2.0.0": 1011 | version: 2.0.0 1012 | resolution: "merge-stream@npm:2.0.0" 1013 | checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 1014 | languageName: node 1015 | linkType: hard 1016 | 1017 | "mime-db@npm:1.52.0": 1018 | version: 1.52.0 1019 | resolution: "mime-db@npm:1.52.0" 1020 | checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa 1021 | languageName: node 1022 | linkType: hard 1023 | 1024 | "mime-types@npm:^2.1.27": 1025 | version: 2.1.35 1026 | resolution: "mime-types@npm:2.1.35" 1027 | dependencies: 1028 | mime-db: "npm:1.52.0" 1029 | checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 1030 | languageName: node 1031 | linkType: hard 1032 | 1033 | "mimic-fn@npm:^3.1.0": 1034 | version: 3.1.0 1035 | resolution: "mimic-fn@npm:3.1.0" 1036 | checksum: 10c0/a07cdd8ed6490c2dff5b11f889b245d9556b80f5a653a552a651d17cff5a2d156e632d235106c2369f00cccef4071704589574cf3601bc1b1400a1f620dff067 1037 | languageName: node 1038 | linkType: hard 1039 | 1040 | "minimatch@npm:^9.0.5": 1041 | version: 9.0.5 1042 | resolution: "minimatch@npm:9.0.5" 1043 | dependencies: 1044 | brace-expansion: "npm:^2.0.1" 1045 | checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed 1046 | languageName: node 1047 | linkType: hard 1048 | 1049 | "minipass@npm:^7.0.4, minipass@npm:^7.1.2": 1050 | version: 7.1.2 1051 | resolution: "minipass@npm:7.1.2" 1052 | checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 1053 | languageName: node 1054 | linkType: hard 1055 | 1056 | "minizlib@npm:^3.0.1": 1057 | version: 3.0.2 1058 | resolution: "minizlib@npm:3.0.2" 1059 | dependencies: 1060 | minipass: "npm:^7.1.2" 1061 | checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78 1062 | languageName: node 1063 | linkType: hard 1064 | 1065 | "mkdirp@npm:^3.0.1": 1066 | version: 3.0.1 1067 | resolution: "mkdirp@npm:3.0.1" 1068 | bin: 1069 | mkdirp: dist/cjs/src/bin.js 1070 | checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d 1071 | languageName: node 1072 | linkType: hard 1073 | 1074 | "nanoid@npm:^3.3.8": 1075 | version: 3.3.11 1076 | resolution: "nanoid@npm:3.3.11" 1077 | bin: 1078 | nanoid: bin/nanoid.cjs 1079 | checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b 1080 | languageName: node 1081 | linkType: hard 1082 | 1083 | "neo-async@npm:^2.6.2": 1084 | version: 2.6.2 1085 | resolution: "neo-async@npm:2.6.2" 1086 | checksum: 10c0/c2f5a604a54a8ec5438a342e1f356dff4bc33ccccdb6dc668d94fe8e5eccfc9d2c2eea6064b0967a767ba63b33763f51ccf2cd2441b461a7322656c1f06b3f5d 1087 | languageName: node 1088 | linkType: hard 1089 | 1090 | "node-releases@npm:^2.0.19": 1091 | version: 2.0.19 1092 | resolution: "node-releases@npm:2.0.19" 1093 | checksum: 10c0/52a0dbd25ccf545892670d1551690fe0facb6a471e15f2cfa1b20142a5b255b3aa254af5f59d6ecb69c2bec7390bc643c43aa63b13bf5e64b6075952e716b1aa 1094 | languageName: node 1095 | linkType: hard 1096 | 1097 | "p-finally@npm:^1.0.0": 1098 | version: 1.0.0 1099 | resolution: "p-finally@npm:1.0.0" 1100 | checksum: 10c0/6b8552339a71fe7bd424d01d8451eea92d379a711fc62f6b2fe64cad8a472c7259a236c9a22b4733abca0b5666ad503cb497792a0478c5af31ded793d00937e7 1101 | languageName: node 1102 | linkType: hard 1103 | 1104 | "p-limit@npm:^2.2.0": 1105 | version: 2.3.0 1106 | resolution: "p-limit@npm:2.3.0" 1107 | dependencies: 1108 | p-try: "npm:^2.0.0" 1109 | checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 1110 | languageName: node 1111 | linkType: hard 1112 | 1113 | "p-limit@npm:^4.0.0": 1114 | version: 4.0.0 1115 | resolution: "p-limit@npm:4.0.0" 1116 | dependencies: 1117 | yocto-queue: "npm:^1.0.0" 1118 | checksum: 10c0/a56af34a77f8df2ff61ddfb29431044557fcbcb7642d5a3233143ebba805fc7306ac1d448de724352861cb99de934bc9ab74f0d16fe6a5460bdbdf938de875ad 1119 | languageName: node 1120 | linkType: hard 1121 | 1122 | "p-locate@npm:^4.1.0": 1123 | version: 4.1.0 1124 | resolution: "p-locate@npm:4.1.0" 1125 | dependencies: 1126 | p-limit: "npm:^2.2.0" 1127 | checksum: 10c0/1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 1128 | languageName: node 1129 | linkType: hard 1130 | 1131 | "p-locate@npm:^6.0.0": 1132 | version: 6.0.0 1133 | resolution: "p-locate@npm:6.0.0" 1134 | dependencies: 1135 | p-limit: "npm:^4.0.0" 1136 | checksum: 10c0/d72fa2f41adce59c198270aa4d3c832536c87a1806e0f69dffb7c1a7ca998fb053915ca833d90f166a8c082d3859eabfed95f01698a3214c20df6bb8de046312 1137 | languageName: node 1138 | linkType: hard 1139 | 1140 | "p-queue@npm:^6.6.2": 1141 | version: 6.6.2 1142 | resolution: "p-queue@npm:6.6.2" 1143 | dependencies: 1144 | eventemitter3: "npm:^4.0.4" 1145 | p-timeout: "npm:^3.2.0" 1146 | checksum: 10c0/5739ecf5806bbeadf8e463793d5e3004d08bb3f6177bd1a44a005da8fd81bb90f80e4633e1fb6f1dfd35ee663a5c0229abe26aebb36f547ad5a858347c7b0d3e 1147 | languageName: node 1148 | linkType: hard 1149 | 1150 | "p-timeout@npm:^3.2.0": 1151 | version: 3.2.0 1152 | resolution: "p-timeout@npm:3.2.0" 1153 | dependencies: 1154 | p-finally: "npm:^1.0.0" 1155 | checksum: 10c0/524b393711a6ba8e1d48137c5924749f29c93d70b671e6db761afa784726572ca06149c715632da8f70c090073afb2af1c05730303f915604fd38ee207b70a61 1156 | languageName: node 1157 | linkType: hard 1158 | 1159 | "p-timeout@npm:^4.1.0": 1160 | version: 4.1.0 1161 | resolution: "p-timeout@npm:4.1.0" 1162 | checksum: 10c0/25aaf13ae9ebfff4ab45591f6647f3bee1ecede073c367175fb0157c27efb170cfb51259a4d2e9118d2ca595453bc885f086ad0ca7476d1c26cee3d13a7c9f6d 1163 | languageName: node 1164 | linkType: hard 1165 | 1166 | "p-try@npm:^2.0.0": 1167 | version: 2.2.0 1168 | resolution: "p-try@npm:2.2.0" 1169 | checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f 1170 | languageName: node 1171 | linkType: hard 1172 | 1173 | "parse-author@npm:^2.0.0": 1174 | version: 2.0.0 1175 | resolution: "parse-author@npm:2.0.0" 1176 | dependencies: 1177 | author-regex: "npm:^1.0.0" 1178 | checksum: 10c0/8b4c19523588a4271c89f64e8167be7c80b4765059c865d38a996c37d080c7e5123e3e2d07d47290891628ad27f4dfb692e3b61156c52fcc0af6cdce3ed57afd 1179 | languageName: node 1180 | linkType: hard 1181 | 1182 | "path-exists@npm:^4.0.0": 1183 | version: 4.0.0 1184 | resolution: "path-exists@npm:4.0.0" 1185 | checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b 1186 | languageName: node 1187 | linkType: hard 1188 | 1189 | "path-exists@npm:^5.0.0": 1190 | version: 5.0.0 1191 | resolution: "path-exists@npm:5.0.0" 1192 | checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a 1193 | languageName: node 1194 | linkType: hard 1195 | 1196 | "path-key@npm:^3.1.0": 1197 | version: 3.1.1 1198 | resolution: "path-key@npm:3.1.1" 1199 | checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c 1200 | languageName: node 1201 | linkType: hard 1202 | 1203 | "path-parse@npm:^1.0.7": 1204 | version: 1.0.7 1205 | resolution: "path-parse@npm:1.0.7" 1206 | checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 1207 | languageName: node 1208 | linkType: hard 1209 | 1210 | "picocolors@npm:^1.1.1": 1211 | version: 1.1.1 1212 | resolution: "picocolors@npm:1.1.1" 1213 | checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 1214 | languageName: node 1215 | linkType: hard 1216 | 1217 | "pkg-dir@npm:^4.2.0": 1218 | version: 4.2.0 1219 | resolution: "pkg-dir@npm:4.2.0" 1220 | dependencies: 1221 | find-up: "npm:^4.0.0" 1222 | checksum: 10c0/c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 1223 | languageName: node 1224 | linkType: hard 1225 | 1226 | "prettier-linter-helpers@npm:^1.0.0": 1227 | version: 1.0.0 1228 | resolution: "prettier-linter-helpers@npm:1.0.0" 1229 | dependencies: 1230 | fast-diff: "npm:^1.1.2" 1231 | checksum: 10c0/81e0027d731b7b3697ccd2129470ed9913ecb111e4ec175a12f0fcfab0096516373bf0af2fef132af50cafb0a905b74ff57996d615f59512bb9ac7378fcc64ab 1232 | languageName: node 1233 | linkType: hard 1234 | 1235 | "prettier@npm:^3.5.3": 1236 | version: 3.5.3 1237 | resolution: "prettier@npm:3.5.3" 1238 | bin: 1239 | prettier: bin/prettier.cjs 1240 | checksum: 10c0/3880cb90b9dc0635819ab52ff571518c35bd7f15a6e80a2054c05dbc8a3aa6e74f135519e91197de63705bcb38388ded7e7230e2178432a1468005406238b877 1241 | languageName: node 1242 | linkType: hard 1243 | 1244 | "randombytes@npm:^2.1.0": 1245 | version: 2.1.0 1246 | resolution: "randombytes@npm:2.1.0" 1247 | dependencies: 1248 | safe-buffer: "npm:^5.1.0" 1249 | checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 1250 | languageName: node 1251 | linkType: hard 1252 | 1253 | "rechoir@npm:^0.8.0": 1254 | version: 0.8.0 1255 | resolution: "rechoir@npm:0.8.0" 1256 | dependencies: 1257 | resolve: "npm:^1.20.0" 1258 | checksum: 10c0/1a30074124a22abbd5d44d802dac26407fa72a0a95f162aa5504ba8246bc5452f8b1a027b154d9bdbabcd8764920ff9333d934c46a8f17479c8912e92332f3ff 1259 | languageName: node 1260 | linkType: hard 1261 | 1262 | "require-from-string@npm:^2.0.2": 1263 | version: 2.0.2 1264 | resolution: "require-from-string@npm:2.0.2" 1265 | checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 1266 | languageName: node 1267 | linkType: hard 1268 | 1269 | "resolve-cwd@npm:^3.0.0": 1270 | version: 3.0.0 1271 | resolution: "resolve-cwd@npm:3.0.0" 1272 | dependencies: 1273 | resolve-from: "npm:^5.0.0" 1274 | checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 1275 | languageName: node 1276 | linkType: hard 1277 | 1278 | "resolve-from@npm:^5.0.0": 1279 | version: 5.0.0 1280 | resolution: "resolve-from@npm:5.0.0" 1281 | checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 1282 | languageName: node 1283 | linkType: hard 1284 | 1285 | "resolve-pkg-maps@npm:^1.0.0": 1286 | version: 1.0.0 1287 | resolution: "resolve-pkg-maps@npm:1.0.0" 1288 | checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab 1289 | languageName: node 1290 | linkType: hard 1291 | 1292 | "resolve@npm:^1.20.0": 1293 | version: 1.22.10 1294 | resolution: "resolve@npm:1.22.10" 1295 | dependencies: 1296 | is-core-module: "npm:^2.16.0" 1297 | path-parse: "npm:^1.0.7" 1298 | supports-preserve-symlinks-flag: "npm:^1.0.0" 1299 | bin: 1300 | resolve: bin/resolve 1301 | checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203 1302 | languageName: node 1303 | linkType: hard 1304 | 1305 | "resolve@patch:resolve@npm%3A^1.20.0#optional!builtin": 1306 | version: 1.22.10 1307 | resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" 1308 | dependencies: 1309 | is-core-module: "npm:^2.16.0" 1310 | path-parse: "npm:^1.0.7" 1311 | supports-preserve-symlinks-flag: "npm:^1.0.0" 1312 | bin: 1313 | resolve: bin/resolve 1314 | checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939 1315 | languageName: node 1316 | linkType: hard 1317 | 1318 | "safe-buffer@npm:^5.1.0": 1319 | version: 5.2.1 1320 | resolution: "safe-buffer@npm:5.2.1" 1321 | checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 1322 | languageName: node 1323 | linkType: hard 1324 | 1325 | "schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.2": 1326 | version: 4.3.2 1327 | resolution: "schema-utils@npm:4.3.2" 1328 | dependencies: 1329 | "@types/json-schema": "npm:^7.0.9" 1330 | ajv: "npm:^8.9.0" 1331 | ajv-formats: "npm:^2.1.1" 1332 | ajv-keywords: "npm:^5.1.0" 1333 | checksum: 10c0/981632f9bf59f35b15a9bcdac671dd183f4946fe4b055ae71a301e66a9797b95e5dd450de581eb6cca56fb6583ce8f24d67b2d9f8e1b2936612209697f6c277e 1334 | languageName: node 1335 | linkType: hard 1336 | 1337 | "semver@npm:^7.5.4, semver@npm:^7.6.3, semver@npm:^7.7.1": 1338 | version: 7.7.1 1339 | resolution: "semver@npm:7.7.1" 1340 | bin: 1341 | semver: bin/semver.js 1342 | checksum: 10c0/fd603a6fb9c399c6054015433051bdbe7b99a940a8fb44b85c2b524c4004b023d7928d47cb22154f8d054ea7ee8597f586605e05b52047f048278e4ac56ae958 1343 | languageName: node 1344 | linkType: hard 1345 | 1346 | "serialize-javascript@npm:^6.0.2": 1347 | version: 6.0.2 1348 | resolution: "serialize-javascript@npm:6.0.2" 1349 | dependencies: 1350 | randombytes: "npm:^2.1.0" 1351 | checksum: 10c0/2dd09ef4b65a1289ba24a788b1423a035581bef60817bea1f01eda8e3bda623f86357665fe7ac1b50f6d4f583f97db9615b3f07b2a2e8cbcb75033965f771dd2 1352 | languageName: node 1353 | linkType: hard 1354 | 1355 | "shallow-clone@npm:^3.0.0": 1356 | version: 3.0.1 1357 | resolution: "shallow-clone@npm:3.0.1" 1358 | dependencies: 1359 | kind-of: "npm:^6.0.2" 1360 | checksum: 10c0/7bab09613a1b9f480c85a9823aebec533015579fa055ba6634aa56ba1f984380670eaf33b8217502931872aa1401c9fcadaa15f9f604d631536df475b05bcf1e 1361 | languageName: node 1362 | linkType: hard 1363 | 1364 | "shebang-command@npm:^2.0.0": 1365 | version: 2.0.0 1366 | resolution: "shebang-command@npm:2.0.0" 1367 | dependencies: 1368 | shebang-regex: "npm:^3.0.0" 1369 | checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e 1370 | languageName: node 1371 | linkType: hard 1372 | 1373 | "shebang-regex@npm:^3.0.0": 1374 | version: 3.0.0 1375 | resolution: "shebang-regex@npm:3.0.0" 1376 | checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 1377 | languageName: node 1378 | linkType: hard 1379 | 1380 | "smpte-timecode@npm:^1.3.6": 1381 | version: 1.3.6 1382 | resolution: "smpte-timecode@npm:1.3.6" 1383 | checksum: 10c0/31d37c2fa2789eda914690d639a36e0e19b5d4d97b563a65a36fd34c2a6707859aefec57b983abbcd5a1e66b26a14a9007066fabb86b6a0aa8c5d6968a02926e 1384 | languageName: node 1385 | linkType: hard 1386 | 1387 | "source-map-support@npm:~0.5.20": 1388 | version: 0.5.21 1389 | resolution: "source-map-support@npm:0.5.21" 1390 | dependencies: 1391 | buffer-from: "npm:^1.0.0" 1392 | source-map: "npm:^0.6.0" 1393 | checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d 1394 | languageName: node 1395 | linkType: hard 1396 | 1397 | "source-map@npm:^0.6.0": 1398 | version: 0.6.1 1399 | resolution: "source-map@npm:0.6.1" 1400 | checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 1401 | languageName: node 1402 | linkType: hard 1403 | 1404 | "supports-color@npm:^8.0.0": 1405 | version: 8.1.1 1406 | resolution: "supports-color@npm:8.1.1" 1407 | dependencies: 1408 | has-flag: "npm:^4.0.0" 1409 | checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 1410 | languageName: node 1411 | linkType: hard 1412 | 1413 | "supports-preserve-symlinks-flag@npm:^1.0.0": 1414 | version: 1.0.0 1415 | resolution: "supports-preserve-symlinks-flag@npm:1.0.0" 1416 | checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 1417 | languageName: node 1418 | linkType: hard 1419 | 1420 | "synckit@npm:^0.11.0": 1421 | version: 0.11.4 1422 | resolution: "synckit@npm:0.11.4" 1423 | dependencies: 1424 | "@pkgr/core": "npm:^0.2.3" 1425 | tslib: "npm:^2.8.1" 1426 | checksum: 10c0/dd2965a37c93c0b652bf07b1fd8d1639a803b65cf34c0cb1b827b8403044fc3b09ec87f681d922a324825127ee95b2e0394e7caccb502f407892d63e903c5276 1427 | languageName: node 1428 | linkType: hard 1429 | 1430 | "tapable@npm:^2.1.1, tapable@npm:^2.2.0": 1431 | version: 2.2.1 1432 | resolution: "tapable@npm:2.2.1" 1433 | checksum: 10c0/bc40e6efe1e554d075469cedaba69a30eeb373552aaf41caeaaa45bf56ffacc2674261b106245bd566b35d8f3329b52d838e851ee0a852120acae26e622925c9 1434 | languageName: node 1435 | linkType: hard 1436 | 1437 | "tar@npm:^7.4.3": 1438 | version: 7.4.3 1439 | resolution: "tar@npm:7.4.3" 1440 | dependencies: 1441 | "@isaacs/fs-minipass": "npm:^4.0.0" 1442 | chownr: "npm:^3.0.0" 1443 | minipass: "npm:^7.1.2" 1444 | minizlib: "npm:^3.0.1" 1445 | mkdirp: "npm:^3.0.1" 1446 | yallist: "npm:^5.0.0" 1447 | checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d 1448 | languageName: node 1449 | linkType: hard 1450 | 1451 | "terser-webpack-plugin@npm:^5.3.11": 1452 | version: 5.3.14 1453 | resolution: "terser-webpack-plugin@npm:5.3.14" 1454 | dependencies: 1455 | "@jridgewell/trace-mapping": "npm:^0.3.25" 1456 | jest-worker: "npm:^27.4.5" 1457 | schema-utils: "npm:^4.3.0" 1458 | serialize-javascript: "npm:^6.0.2" 1459 | terser: "npm:^5.31.1" 1460 | peerDependencies: 1461 | webpack: ^5.1.0 1462 | peerDependenciesMeta: 1463 | "@swc/core": 1464 | optional: true 1465 | esbuild: 1466 | optional: true 1467 | uglify-js: 1468 | optional: true 1469 | checksum: 10c0/9b060947241af43bd6fd728456f60e646186aef492163672a35ad49be6fbc7f63b54a7356c3f6ff40a8f83f00a977edc26f044b8e106cc611c053c8c0eaf8569 1470 | languageName: node 1471 | linkType: hard 1472 | 1473 | "terser@npm:^5.31.1": 1474 | version: 5.39.0 1475 | resolution: "terser@npm:5.39.0" 1476 | dependencies: 1477 | "@jridgewell/source-map": "npm:^0.3.3" 1478 | acorn: "npm:^8.8.2" 1479 | commander: "npm:^2.20.0" 1480 | source-map-support: "npm:~0.5.20" 1481 | bin: 1482 | terser: bin/terser 1483 | checksum: 10c0/83326545ea1aecd6261030568b6191ccfa4cb6aa61d9ea41746a52479f50017a78b77e4725fbbc207c5df841ffa66a773c5ac33636e95c7ab94fe7e0379ae5c7 1484 | languageName: node 1485 | linkType: hard 1486 | 1487 | "tslib@npm:^2.6.2, tslib@npm:^2.8.1": 1488 | version: 2.8.1 1489 | resolution: "tslib@npm:2.8.1" 1490 | checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 1491 | languageName: node 1492 | linkType: hard 1493 | 1494 | "typescript@npm:~5.8.3": 1495 | version: 5.8.3 1496 | resolution: "typescript@npm:5.8.3" 1497 | bin: 1498 | tsc: bin/tsc 1499 | tsserver: bin/tsserver 1500 | checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 1501 | languageName: node 1502 | linkType: hard 1503 | 1504 | "typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": 1505 | version: 5.8.3 1506 | resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" 1507 | bin: 1508 | tsc: bin/tsc 1509 | tsserver: bin/tsserver 1510 | checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb 1511 | languageName: node 1512 | linkType: hard 1513 | 1514 | "undici-types@npm:~6.21.0": 1515 | version: 6.21.0 1516 | resolution: "undici-types@npm:6.21.0" 1517 | checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 1518 | languageName: node 1519 | linkType: hard 1520 | 1521 | "unicorn-magic@npm:^0.1.0": 1522 | version: 0.1.0 1523 | resolution: "unicorn-magic@npm:0.1.0" 1524 | checksum: 10c0/e4ed0de05b0a05e735c7d8a2930881e5efcfc3ec897204d5d33e7e6247f4c31eac92e383a15d9a6bccb7319b4271ee4bea946e211bf14951fec6ff2cbbb66a92 1525 | languageName: node 1526 | linkType: hard 1527 | 1528 | "update-browserslist-db@npm:^1.1.1": 1529 | version: 1.1.3 1530 | resolution: "update-browserslist-db@npm:1.1.3" 1531 | dependencies: 1532 | escalade: "npm:^3.2.0" 1533 | picocolors: "npm:^1.1.1" 1534 | peerDependencies: 1535 | browserslist: ">= 4.21.0" 1536 | bin: 1537 | update-browserslist-db: cli.js 1538 | checksum: 10c0/682e8ecbf9de474a626f6462aa85927936cdd256fe584c6df2508b0df9f7362c44c957e9970df55dfe44d3623807d26316ea2c7d26b80bb76a16c56c37233c32 1539 | languageName: node 1540 | linkType: hard 1541 | 1542 | "watchpack@npm:^2.4.1": 1543 | version: 2.4.2 1544 | resolution: "watchpack@npm:2.4.2" 1545 | dependencies: 1546 | glob-to-regexp: "npm:^0.4.1" 1547 | graceful-fs: "npm:^4.1.2" 1548 | checksum: 10c0/ec60a5f0e9efaeca0102fd9126346b3b2d523e01c34030d3fddf5813a7125765121ebdc2552981136dcd2c852deb1af0b39340f2fcc235f292db5399d0283577 1549 | languageName: node 1550 | linkType: hard 1551 | 1552 | "webpack-cli@npm:^6.0.1": 1553 | version: 6.0.1 1554 | resolution: "webpack-cli@npm:6.0.1" 1555 | dependencies: 1556 | "@discoveryjs/json-ext": "npm:^0.6.1" 1557 | "@webpack-cli/configtest": "npm:^3.0.1" 1558 | "@webpack-cli/info": "npm:^3.0.1" 1559 | "@webpack-cli/serve": "npm:^3.0.1" 1560 | colorette: "npm:^2.0.14" 1561 | commander: "npm:^12.1.0" 1562 | cross-spawn: "npm:^7.0.3" 1563 | envinfo: "npm:^7.14.0" 1564 | fastest-levenshtein: "npm:^1.0.12" 1565 | import-local: "npm:^3.0.2" 1566 | interpret: "npm:^3.1.1" 1567 | rechoir: "npm:^0.8.0" 1568 | webpack-merge: "npm:^6.0.1" 1569 | peerDependencies: 1570 | webpack: ^5.82.0 1571 | peerDependenciesMeta: 1572 | webpack-bundle-analyzer: 1573 | optional: true 1574 | webpack-dev-server: 1575 | optional: true 1576 | bin: 1577 | webpack-cli: ./bin/cli.js 1578 | checksum: 10c0/2aaca78e277427f03f528602abd707d224696048fb46286ea636c7975592409c4381ca94d68bbbb3900f195ca97f256e619583e8feb34a80da531461323bf3e2 1579 | languageName: node 1580 | linkType: hard 1581 | 1582 | "webpack-merge@npm:^6.0.1": 1583 | version: 6.0.1 1584 | resolution: "webpack-merge@npm:6.0.1" 1585 | dependencies: 1586 | clone-deep: "npm:^4.0.1" 1587 | flat: "npm:^5.0.2" 1588 | wildcard: "npm:^2.0.1" 1589 | checksum: 10c0/bf1429567858b353641801b8a2696ca0aac270fc8c55d4de8a7b586fe07d27fdcfc83099a98ab47e6162383db8dd63bb8cc25b1beb2ec82150422eec843b0dc0 1590 | languageName: node 1591 | linkType: hard 1592 | 1593 | "webpack-sources@npm:^3.2.3": 1594 | version: 3.2.3 1595 | resolution: "webpack-sources@npm:3.2.3" 1596 | checksum: 10c0/2ef63d77c4fad39de4a6db17323d75eb92897b32674e97d76f0a1e87c003882fc038571266ad0ef581ac734cbe20952912aaa26155f1905e96ce251adbb1eb4e 1597 | languageName: node 1598 | linkType: hard 1599 | 1600 | "webpack@npm:^5.97.1": 1601 | version: 5.99.7 1602 | resolution: "webpack@npm:5.99.7" 1603 | dependencies: 1604 | "@types/eslint-scope": "npm:^3.7.7" 1605 | "@types/estree": "npm:^1.0.6" 1606 | "@types/json-schema": "npm:^7.0.15" 1607 | "@webassemblyjs/ast": "npm:^1.14.1" 1608 | "@webassemblyjs/wasm-edit": "npm:^1.14.1" 1609 | "@webassemblyjs/wasm-parser": "npm:^1.14.1" 1610 | acorn: "npm:^8.14.0" 1611 | browserslist: "npm:^4.24.0" 1612 | chrome-trace-event: "npm:^1.0.2" 1613 | enhanced-resolve: "npm:^5.17.1" 1614 | es-module-lexer: "npm:^1.2.1" 1615 | eslint-scope: "npm:5.1.1" 1616 | events: "npm:^3.2.0" 1617 | glob-to-regexp: "npm:^0.4.1" 1618 | graceful-fs: "npm:^4.2.11" 1619 | json-parse-even-better-errors: "npm:^2.3.1" 1620 | loader-runner: "npm:^4.2.0" 1621 | mime-types: "npm:^2.1.27" 1622 | neo-async: "npm:^2.6.2" 1623 | schema-utils: "npm:^4.3.2" 1624 | tapable: "npm:^2.1.1" 1625 | terser-webpack-plugin: "npm:^5.3.11" 1626 | watchpack: "npm:^2.4.1" 1627 | webpack-sources: "npm:^3.2.3" 1628 | peerDependenciesMeta: 1629 | webpack-cli: 1630 | optional: true 1631 | bin: 1632 | webpack: bin/webpack.js 1633 | checksum: 10c0/e121880d921d5500e9dd61c3428f5d8dca4786c559e7f37488173186447cd22079be677d470a4db1643febf8031b9be900f501c9b95ba94ffe3d2065ad486d31 1634 | languageName: node 1635 | linkType: hard 1636 | 1637 | "which@npm:^2.0.1": 1638 | version: 2.0.2 1639 | resolution: "which@npm:2.0.2" 1640 | dependencies: 1641 | isexe: "npm:^2.0.0" 1642 | bin: 1643 | node-which: ./bin/node-which 1644 | checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f 1645 | languageName: node 1646 | linkType: hard 1647 | 1648 | "wildcard@npm:^2.0.1": 1649 | version: 2.0.1 1650 | resolution: "wildcard@npm:2.0.1" 1651 | checksum: 10c0/08f70cd97dd9a20aea280847a1fe8148e17cae7d231640e41eb26d2388697cbe65b67fd9e68715251c39b080c5ae4f76d71a9a69fa101d897273efdfb1b58bf7 1652 | languageName: node 1653 | linkType: hard 1654 | 1655 | "yallist@npm:^5.0.0": 1656 | version: 5.0.0 1657 | resolution: "yallist@npm:5.0.0" 1658 | checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 1659 | languageName: node 1660 | linkType: hard 1661 | 1662 | "yocto-queue@npm:^1.0.0": 1663 | version: 1.2.1 1664 | resolution: "yocto-queue@npm:1.2.1" 1665 | checksum: 10c0/5762caa3d0b421f4bdb7a1926b2ae2189fc6e4a14469258f183600028eb16db3e9e0306f46e8ebf5a52ff4b81a881f22637afefbef5399d6ad440824e9b27f9f 1666 | languageName: node 1667 | linkType: hard 1668 | 1669 | "zx@npm:^8.3.2": 1670 | version: 8.8.5 1671 | resolution: "zx@npm:8.8.5" 1672 | bin: 1673 | zx: build/cli.js 1674 | checksum: 10c0/1273e4f72cfe35a59041aef5a56fd87318bc4e11947d101810b67e5c486ab30574042938728e8a15e085de985e762b8585fcdaab4cf87fd113153b63a5846611 1675 | languageName: node 1676 | linkType: hard 1677 | --------------------------------------------------------------------------------