├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── other.yml │ ├── question.yml │ └── rfc.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── node.yaml │ ├── publish.yaml │ └── sonar.yaml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .vscode └── launch.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-version.cjs └── releases │ └── yarn-3.5.0.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── LICENSE ├── README.md ├── assets └── roboto │ ├── LICENSE.txt │ └── Roboto-Regular.ttf ├── examples ├── testUtils │ └── generate-data-transfer-spec.ts └── upload.ts ├── jest.config.js ├── notes └── multiviewLabels.md ├── package.json ├── sonar-project.properties ├── src ├── @types │ └── nanotimer.d.ts ├── __tests__ │ ├── atem.spec.ts │ ├── connection.spec.ts │ ├── connection │ │ ├── 1me-v8.1.data │ │ ├── 1me4k-v8.2.data │ │ ├── 2me-v8.1.2.data │ │ ├── 2me-v8.1.data │ │ ├── 2me4k-v8.4.data │ │ ├── 4me4k-v7.5.2.data │ │ ├── 4me4k-v8.2.data │ │ ├── constellation-2me-hd-v8.7.0.data │ │ ├── constellation-2me-hd-v9.6.2.data │ │ ├── constellation-4me-4k-v9.1.data │ │ ├── constellation-v8.0.2.data │ │ ├── constellation-v8.2.3.data │ │ ├── dump.js │ │ ├── mini-extreme-iso-v9.5.data │ │ ├── mini-extreme-v8.6.data │ │ ├── mini-pro-iso-v8.4.data │ │ ├── mini-pro-v8.2.data │ │ ├── mini-v8.1.1.data │ │ ├── mini-v8.1.data │ │ ├── mini-v8.6.data │ │ ├── ps4k-v7.2.data │ │ ├── sdi-extreme-iso-v8.8.data │ │ ├── tvs-4k8-v9.3.data │ │ ├── tvs-hd8-v9.0.data │ │ ├── tvshd-v8.0.0.data │ │ ├── tvshd-v8.1.0.data │ │ └── tvshd-v8.2.0.data │ ├── index.spec.ts │ ├── tally.spec.ts │ ├── tally │ │ ├── chain-me2-pgm-state.json │ │ ├── chain-me2-pgm-tally.json │ │ ├── chain-me2-pvw-state.json │ │ ├── chain-me2-pvw-tally.json │ │ ├── dsk-active-state.json │ │ ├── dsk-active-tally.json │ │ ├── dsk-in-auto-state.json │ │ ├── dsk-in-auto-tally.json │ │ ├── dump.js │ │ ├── me2-in-ssrc-state.json │ │ ├── me2-in-ssrc-tally.json │ │ ├── mid-dip-state.json │ │ ├── mid-dip-tally.json │ │ ├── mid-preview-trans-state.json │ │ ├── mid-preview-trans-tally.json │ │ ├── mid-trans-dve-state.json │ │ ├── mid-trans-dve-tally.json │ │ ├── mid-trans-dve-with-fill-and-key-state.json │ │ ├── mid-trans-dve-with-fill-and-key-tally.json │ │ ├── mid-trans-dve-with-fill-state.json │ │ ├── mid-trans-dve-with-fill-tally.json │ │ ├── mid-trans-sting-state.json │ │ ├── mid-trans-sting-tally.json │ │ ├── mid-wipe-no-border-state.json │ │ ├── mid-wipe-no-border-tally.json │ │ ├── mid-wipe-with-border-state.json │ │ ├── mid-wipe-with-border-tally.json │ │ ├── preview-trans-state.json │ │ ├── preview-trans-tally.json │ │ ├── ssrc-state.json │ │ ├── ssrc-tally.json │ │ ├── ssrc2-state.json │ │ ├── ssrc2-tally.json │ │ ├── upstream-keyers-state.json │ │ └── upstream-keyers-tally.json │ └── util.ts ├── atem.ts ├── commands │ ├── Audio │ │ ├── AudioMixerHeadphonesCommand.ts │ │ ├── AudioMixerInputCommand.ts │ │ ├── AudioMixerMasterCommand.ts │ │ ├── AudioMixerMonitorCommand.ts │ │ ├── AudioMixerPropertiesCommand.ts │ │ ├── AudioMixerResetPeaksCommand.ts │ │ └── index.ts │ ├── AuxSourceCommand.ts │ ├── CameraControlCommand.ts │ ├── ColorGeneratorCommand.ts │ ├── CommandBase.ts │ ├── DataTransfer │ │ ├── DataTransferAckCommand.ts │ │ ├── DataTransferCompleteCommand.ts │ │ ├── DataTransferDataCommand.ts │ │ ├── DataTransferDownloadRequestCommand.ts │ │ ├── DataTransferErrorCommand.ts │ │ ├── DataTransferFileDescriptionCommand.ts │ │ ├── DataTransferUploadContinueCommand.ts │ │ ├── DataTransferUploadRequestCommand.ts │ │ ├── LockObtainedCommand.ts │ │ ├── LockStateCommand.ts │ │ └── index.ts │ ├── DeviceProfile │ │ ├── audioMixerConfigCommand.ts │ │ ├── fairlightAudioMixerConfigCommand.ts │ │ ├── index.ts │ │ ├── macroPoolConfigCommand.ts │ │ ├── mediaPoolConfigCommand.ts │ │ ├── mixEffectBlockConfigCommand.ts │ │ ├── multiviewerConfigCommand.ts │ │ ├── productIdentifierCommand.ts │ │ ├── superSourceConfigCommand.ts │ │ ├── topologyCommand.ts │ │ ├── versionCommand.ts │ │ └── videoMixerConfigCommand.ts │ ├── DisplayClock │ │ ├── DisplayClockCurrentTimeCommand.ts │ │ ├── DisplayClockPropertiesGetCommand.ts │ │ ├── DisplayClockPropertiesSetCommand.ts │ │ ├── DisplayClockRequestTimeCommand.ts │ │ ├── DisplayClockStateSetCommand.ts │ │ └── index.ts │ ├── DownstreamKey │ │ ├── DownstreamKeyAutoCommand.ts │ │ ├── DownstreamKeyCutSourceCommand.ts │ │ ├── DownstreamKeyFillSourceCommand.ts │ │ ├── DownstreamKeyGeneralCommand.ts │ │ ├── DownstreamKeyMaskCommand.ts │ │ ├── DownstreamKeyOnAirCommand.ts │ │ ├── DownstreamKeyPropertiesCommand.ts │ │ ├── DownstreamKeyRateCommand.ts │ │ ├── DownstreamKeySourcesCommand.ts │ │ ├── DownstreamKeyStateCommand.ts │ │ ├── DownstreamKeyTieCommand.ts │ │ └── index.ts │ ├── Fairlight │ │ ├── AudioRouting │ │ │ ├── AudioRoutingOutput.ts │ │ │ ├── AudioRoutingSource.ts │ │ │ └── index.ts │ │ ├── FairlightMixerAnalogAudioCommand.ts_ │ │ ├── FairlightMixerInputCommand.ts │ │ ├── FairlightMixerMasterCommand.ts │ │ ├── FairlightMixerMasterCompressorCommand.ts │ │ ├── FairlightMixerMasterDynamicsResetCommand.ts │ │ ├── FairlightMixerMasterEqualizerBandCommand.ts │ │ ├── FairlightMixerMasterEqualizerResetCommand.ts │ │ ├── FairlightMixerMasterLevelsCommand.ts │ │ ├── FairlightMixerMasterLimiterCommand.ts │ │ ├── FairlightMixerMasterPropertiesCommand.ts │ │ ├── FairlightMixerMonitorCommand.ts │ │ ├── FairlightMixerMonitorSoloCommand.ts │ │ ├── FairlightMixerResetPeakLevelsCommand.ts │ │ ├── FairlightMixerSendLevelsCommand.ts │ │ ├── FairlightMixerSourceCommand.ts │ │ ├── FairlightMixerSourceCompressorCommand.ts │ │ ├── FairlightMixerSourceDynamicsResetCommand.ts │ │ ├── FairlightMixerSourceEqualizerBandCommand.ts │ │ ├── FairlightMixerSourceEqualizerResetCommand.ts │ │ ├── FairlightMixerSourceExpanderCommand.ts │ │ ├── FairlightMixerSourceLevelsCommand.ts │ │ ├── FairlightMixerSourceLimiterCommand.ts │ │ ├── FairlightMixerSourceResetPeakLevelsCommand.ts │ │ ├── common.ts │ │ └── index.ts │ ├── InitCompleteCommand.ts │ ├── Inputs │ │ ├── InputPropertiesCommand.ts │ │ └── index.ts │ ├── Macro │ │ ├── MacroActionCommand.ts │ │ ├── MacroAddTimedPauseCommand.ts │ │ ├── MacroPropertiesCommand.ts │ │ ├── MacroRecordCommand.ts │ │ ├── MacroRecordingStatusCommand.ts │ │ ├── MacroRunStatusCommand.ts │ │ └── index.ts │ ├── Media │ │ ├── MediaPlayerSourceCommand.ts │ │ ├── MediaPlayerStatusCommand.ts │ │ ├── MediaPoolCaptureStillCommand.ts │ │ ├── MediaPoolClearClipCommand.ts │ │ ├── MediaPoolClearStillCommand.ts │ │ ├── MediaPoolClipDescription.ts │ │ ├── MediaPoolFrameDescription.ts │ │ ├── MediaPoolSetClipCommand.ts │ │ └── index.ts │ ├── MixEffects │ │ ├── AutoTransitionCommand.ts │ │ ├── CutCommand.ts │ │ ├── FadeToBlack │ │ │ ├── FadeToBlackAutoCommand.ts │ │ │ ├── FadeToBlackRateCommand.ts │ │ │ ├── FadeToBlackStateCommand.ts │ │ │ └── index.ts │ │ ├── Key │ │ │ ├── MixEffectKeyAdvancedChromaPropertiesCommand.ts │ │ │ ├── MixEffectKeyAdvancedChromaSampleCommand.ts │ │ │ ├── MixEffectKeyAdvancedChromaSampleResetCommand.ts │ │ │ ├── MixEffectKeyChromaCommand.ts │ │ │ ├── MixEffectKeyCutSourceSetCommand.ts │ │ │ ├── MixEffectKeyDVECommand.ts │ │ │ ├── MixEffectKeyFillSourceSetCommand.ts │ │ │ ├── MixEffectKeyFlyKeyframeCommand.ts │ │ │ ├── MixEffectKeyFlyPropertiesGetCommand.ts │ │ │ ├── MixEffectKeyLumaCommand.ts │ │ │ ├── MixEffectKeyMaskSetCommand.ts │ │ │ ├── MixEffectKeyOnAirCommand.ts │ │ │ ├── MixEffectKeyPatternCommand.ts │ │ │ ├── MixEffectKeyPropertiesGetCommand.ts │ │ │ ├── MixEffectKeyRunToCommand.ts │ │ │ ├── MixEffectKeyTypeSetCommand.ts │ │ │ └── index.ts │ │ ├── PreviewInputCommand.ts │ │ ├── ProgramInputCommand.ts │ │ ├── Transition │ │ │ ├── TransitionDVECommand.ts │ │ │ ├── TransitionDipCommand.ts │ │ │ ├── TransitionMixCommand.ts │ │ │ ├── TransitionPositionCommand.ts │ │ │ ├── TransitionPreviewCommand.ts │ │ │ ├── TransitionPropertiesCommand.ts │ │ │ ├── TransitionStingerCommand.ts │ │ │ ├── TransitionWipeCommand.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── PowerStatusCommand.ts │ ├── Recording │ │ ├── RecordingDiskCommand.ts │ │ ├── RecordingDurationCommand.ts │ │ ├── RecordingISOCommand.ts │ │ ├── RecordingSettingsCommand.ts │ │ ├── RecordingStatusCommand.ts │ │ └── index.ts │ ├── Settings │ │ ├── MediaPool.ts │ │ ├── MultiViewerPropertiesCommand.ts │ │ ├── MultiViewerSourceCommand.ts │ │ ├── MultiViewerVuOpacityCommand.ts │ │ ├── MultiViewerWindowSafeAreaCommand.ts │ │ ├── MultiViewerWindowVuMeterCommand.ts │ │ ├── VideoMode.ts │ │ └── index.ts │ ├── StartupStateCommand.ts │ ├── Streaming │ │ ├── StreamingAudioBitratesCommand.ts │ │ ├── StreamingDurationCommand.ts │ │ ├── StreamingServiceCommand.ts │ │ ├── StreamingStatsCommand.ts │ │ ├── StreamingStatusCommand.ts │ │ └── index.ts │ ├── SuperSource │ │ ├── SuperSourceBoxParametersCommand.ts │ │ ├── SuperSourcePropertiesCommand.ts │ │ └── index.ts │ ├── TallyBySourceCommand.ts │ ├── TimeCommand.ts │ ├── TimeConfigCommand.ts │ ├── __tests__ │ │ ├── converters-8.0.ts │ │ ├── converters-default.ts │ │ ├── index.spec.ts │ │ └── libatem-data.json │ └── index.ts ├── dataTransfer │ ├── __tests__ │ │ ├── download-macro-sequence.json │ │ ├── index.spec.ts │ │ ├── sampleAudio.wav │ │ ├── upload-clip-sequence.json │ │ ├── upload-macro-sequence.json │ │ ├── upload-multiviewer-sequence.json │ │ ├── upload-still-sequence.json │ │ └── upload-wav-sequence.json │ ├── dataTransfer.ts │ ├── dataTransferDownloadMacro.ts │ ├── dataTransferDownloadStill.ts │ ├── dataTransferQueue.ts │ ├── dataTransferUploadAudio.ts │ ├── dataTransferUploadBuffer.ts │ ├── dataTransferUploadClip.ts │ ├── dataTransferUploadMacro.ts │ ├── dataTransferUploadMultiViewerLabel.ts │ ├── dataTransferUploadStill.ts │ └── index.ts ├── enums │ └── index.ts ├── index.ts ├── lib │ ├── __mocks__ │ │ └── dgram.ts │ ├── __tests__ │ │ ├── atemSocket.spec.ts │ │ ├── multiviewLabel.spec.ts │ │ ├── packetBuilder.spec.ts │ │ └── socket-child.spec.ts │ ├── atemCommandParser.ts │ ├── atemSocket.ts │ ├── atemSocketChild.ts │ ├── atemUtil.ts │ ├── converters │ │ ├── __tests__ │ │ │ └── rle.spec.ts │ │ ├── colorConstants.ts │ │ ├── rgbaToYuv422.ts │ │ ├── rle.ts │ │ ├── wavAudio.ts │ │ └── yuv422ToRgba.ts │ ├── multiviewLabel.ts │ ├── packetBuilder.ts │ ├── tally.ts │ ├── types.ts │ └── videoMode.ts └── state │ ├── audio.ts │ ├── color.ts │ ├── common.ts │ ├── displayClock.ts │ ├── fairlight.ts │ ├── index.ts │ ├── info.ts │ ├── input.ts │ ├── levels.ts │ ├── macro.ts │ ├── media.ts │ ├── recording.ts │ ├── settings.ts │ ├── streaming.ts │ ├── util.ts │ └── video │ ├── downstreamKeyers.ts │ ├── index.ts │ ├── superSource.ts │ └── upstreamKeyers.ts ├── tsconfig.build.json ├── tsconfig.docs.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | 4 | [*.{cs,js,ts,json}] 5 | indent_size = 4 6 | 7 | [*.{yml,yaml}] 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | scratch 3 | docs -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@sofie-automation/code-standard-preset/eslint/main" 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 🐛 2 | description: Use this if you've found a bug 3 | title: "Bug Report: [Short description of the bug]" 4 | labels: 5 | - 🐛bug 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Before you post, be sure to read our Contribution guidelines: 12 | https://sofie-automation.github.io/sofie-core//docs/for-developers/contribution-guidelines 13 | 14 | - type: textarea 15 | attributes: 16 | label: About me 17 | description: Tell us who / which organization you are representing, and how the Sofie team will be able to contact you. 18 | placeholder: Example "This RFC is posted on behalf of the NRK." 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: Observed Behavior 25 | description: What happened? 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: Expected Behavior 32 | description: What did you expect to happen? 33 | validations: 34 | required: true 35 | 36 | - type: input 37 | attributes: 38 | label: Version 39 | description: What version of Sofie Core / Gateways / other components are you using? 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: Severity / Impact 46 | description: How big of an issue is this? How does this limit your operations? 47 | placeholder: | 48 | Examples: 49 | * This is a blocker for us, we cannot use feature X until this is fixed. 50 | * Not a big issue for us, but it would be nice to have this fixed. 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yml: -------------------------------------------------------------------------------- 1 | name: Other Issue 🔧 2 | description: Use this for other issues 3 | title: "Other: [Short description of the issue]" 4 | labels: 5 | - Other 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Before you post, be sure to read our Contribution guidelines: 12 | https://sofie-automation.github.io/sofie-core//docs/for-developers/contribution-guidelines 13 | 14 | - type: textarea 15 | attributes: 16 | label: About me 17 | description: Tell us who / which organization you are representing, and how the Sofie team will be able to contact you. 18 | placeholder: Example "This RFC is posted on behalf of the NRK." 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: Issue 25 | description: Describe the issue below 26 | validations: 27 | required: true 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question ❓ 2 | description: Use this if you have a question to the Sofie team 3 | title: "Question: [Short summary of the question]" 4 | labels: 5 | - ❓ Question 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Before you post, be sure to read our Contribution guidelines: 12 | https://sofie-automation.github.io/sofie-core//docs/for-developers/contribution-guidelines 13 | 14 | - type: textarea 15 | attributes: 16 | label: About me 17 | description: Tell us who / which organization you are representing, and how the Sofie team will be able to contact you. 18 | placeholder: Example "This RFC is posted on behalf of the NRK." 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: My Question 25 | description: Write your question below 26 | validations: 27 | required: true 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rfc.yml: -------------------------------------------------------------------------------- 1 | name: Request for Comments ❗ 2 | description: Use this to initiate a discussion about a new feature or a larger change 3 | title: "RFC: [Short description of the feature/change]" 4 | labels: 5 | - RFC 6 | - Contribution 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Before you post, be sure to read our Contribution guidelines: 13 | https://sofie-automation.github.io/sofie-core//docs/for-developers/contribution-guidelines 14 | 15 | - type: textarea 16 | attributes: 17 | label: About me 18 | description: Tell us who / which organization you are representing, and how the Sofie team will be able to contact you. 19 | placeholder: Example "This RFC is posted on behalf of the NRK." 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Use case 26 | description: "Please write some background information here, such as: What is your use case? What problem are you trying to solve?" 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | attributes: 32 | label: Proposal 33 | description: Please describe your proposal here 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | attributes: 39 | label: Process 40 | description: Please don't touch this section, the Sofie team will update this as the RFC progresses. 41 | value: | 42 | The Sofie Team will evaluate this RFC and open up a discussion about it, usually within a week. 43 | - [x] RFC created 44 | - [ ] Sofie Team has evaluated the RFC 45 | - [ ] A workshop has been planned 46 | - [ ] RFC has been discussed in a workshop 47 | - [ ] A conclusion has been reached, see comments in thread 48 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## About the Contributor 7 | 11 | 12 | 13 | ## Type of Contribution 14 | 15 | This is a: 16 | 17 | Bug fix / Feature / Code improvement / Documentation improvement / Other (please specify) 18 | 19 | 20 | ## Current Behavior 21 | 25 | 26 | 27 | ## New Behavior 28 | 31 | 32 | 33 | ## Testing Instructions 34 | 41 | 42 | 43 | ## Other Information 44 | 45 | 46 | 47 | ## Status 48 | 52 | 53 | - [ ] PR is ready to be reviewed. 54 | - [ ] The functionality has been tested by the author. 55 | - [ ] Relevant unit tests has been added / updated. 56 | - [ ] Relevant documentation (code comments, [system documentation](https://sofie-automation.github.io/sofie-core//)) has been added / updated. 57 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | reviewers: 8 | - "@Sofie-Automation/operations" 9 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | # Trigger analysis when pushing in main or pull requests, and when creating 3 | # a pull request. 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | name: SonarCloud 10 | jobs: 11 | sonarcloud: 12 | name: SonarCloud 13 | runs-on: ubuntu-latest 14 | if: ${{ github.repository_owner == 'Sofie-Automation' }} 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | # Disabling shallow clone is recommended for improving relevancy of reporting 20 | fetch-depth: 0 21 | 22 | - name: Use Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 16 26 | - name: Prepare Environment 27 | run: | 28 | yarn 29 | env: 30 | CI: true 31 | 32 | - name: SonarCloud Scan 33 | uses: sonarsource/sonarcloud-github-action@master 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test 4 | src/**/*.js 5 | scratch 6 | 7 | /coverage 8 | /docs 9 | .nyc_output 10 | *.log 11 | 12 | wallaby.conf.js 13 | 14 | .DS_Store 15 | 16 | # JetBrains IDE project files 17 | /.idea 18 | 19 | testframe.rgba 20 | 21 | .pnp.* 22 | .yarn/* 23 | !.yarn/patches 24 | !.yarn/plugins 25 | !.yarn/releases 26 | !.yarn/sdks 27 | !.yarn/versions 28 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | CHANGELOG.md 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach by Process ID", 11 | "processId": "${command:PickProcess}", 12 | "protocol": "inspector" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Jest watch", 18 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 19 | "args": ["--watch"], 20 | "cwd": "${workspaceRoot}" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Jest test", 26 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 27 | "args": [""], 28 | "cwd": "${workspaceRoot}" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 5 | spec: "@yarnpkg/plugin-version" 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: "@yarnpkg/plugin-interactive-tools" 8 | 9 | yarnPath: .yarn/releases/yarn-3.5.0.cjs 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute to this Repository 2 | 3 | Before contributing to this specific repository, please read the [Contribution Guidelines](https://sofie-automation.github.io/sofie-core//docs/for-developers/contribution-guidelines) for the _Sofie_ project. 4 | 5 | 6 | ## Branches 7 | This repository uses the following branches: 8 | 9 | * **_main_** is our main branch. We consider it stable and it is used in production. 10 | 11 | We encourage you to base your contributions on the latest **_main_** branch of this repository. The [_Sofie Releases_](https://sofie-automation.github.io/sofie-core//releases) page collects the status and timeline of the _Sofie_ releases. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Norsk rikskringkasting AS 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sofie-Automation/sofie-atem-connection/51c9cf8e995f74a63e50809e29db61792444df35/assets/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /examples/testUtils/generate-data-transfer-spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import { Atem } from '../../dist' 3 | import { DataTransferManager } from '../../dist/dataTransfer' 4 | import * as fs from 'fs' 5 | 6 | const frameBuffer = Buffer.alloc(1920 * 1080 * 4, 0) 7 | // const wavBuffer = fs.readFileSync('./src/dataTransfer/__tests__/sampleAudio.wav') 8 | 9 | const nb = new Atem({}) 10 | nb.on('error', () => null) 11 | 12 | nb.on('connected', async () => { 13 | console.log('connected') 14 | const commands: any[] = [] 15 | 16 | const procCmd = (cmd: any, dir: string): any => { 17 | const props = { ...cmd.properties } 18 | Object.keys(props).forEach((k) => { 19 | if (Buffer.isBuffer(props[k])) { 20 | const buf = props[k] as Buffer 21 | props[k] = { bufferLength: buf.length } 22 | } 23 | }) 24 | return { 25 | name: cmd.constructor.name, 26 | properties: props, 27 | direction: dir, 28 | } 29 | } 30 | 31 | const transfer = new DataTransferManager() 32 | transfer.startCommandSending((cmds) => { 33 | return cmds.map(async (cmd) => { 34 | commands.push(procCmd(cmd, 'send')) 35 | return nb.sendCommand(cmd) 36 | }) 37 | }) 38 | nb.on('receivedCommands', (cmds) => { 39 | cmds.forEach(async (cmd): Promise => { 40 | commands.push(procCmd(cmd, 'recv')) 41 | transfer.queueCommand(cmd) 42 | }) 43 | }) 44 | 45 | console.log('uploading') 46 | // await transfer.uploadStill(0, frameBuffer, 'some still', '') 47 | // await transfer.uploadAudio(1, wavBuffer, 'audio file') 48 | await transfer.uploadClip(1, [frameBuffer, frameBuffer, frameBuffer], 'clip file') 49 | 50 | console.log('uploaded') 51 | 52 | await new Promise((resolve) => setTimeout(resolve, 1000)) 53 | 54 | // console.log(JSON.stringify({ 55 | // sent: sentCommands, 56 | // received: receivedCommands 57 | // })) 58 | fs.writeFileSync('upload.json', JSON.stringify(commands, undefined, '\t')) 59 | 60 | process.exit(0) 61 | }) 62 | nb.connect('10.42.13.98', 9910).catch((e) => { 63 | console.error(e) 64 | process.exit(0) 65 | }) 66 | -------------------------------------------------------------------------------- /examples/upload.ts: -------------------------------------------------------------------------------- 1 | import { Atem } from '../dist' 2 | import * as fs from 'fs' 3 | 4 | const file = fs.readFileSync('./testframe.rgba') 5 | 6 | if (process.argv.length < 3) { 7 | console.error('Expected ip address as parameter') 8 | process.exit(1) 9 | } 10 | const IP = process.argv[2] 11 | 12 | let uploadNumber = 0 13 | let uploadStarted = Date.now() 14 | 15 | const conn = new Atem({}) 16 | 17 | let stuckTimeout: any = null 18 | 19 | function uploadNext(): void { 20 | uploadStarted = Date.now() 21 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 22 | conn.uploadStill(uploadNumber % conn.state!.media.stillPool.length, file, 'contemplation..', '').then( 23 | async (_res) => { 24 | console.log(`Uploaded still #${uploadNumber} in ${Date.now() - uploadStarted}ms at 1080p`) 25 | uploadNumber++ 26 | 27 | if (stuckTimeout) { 28 | clearTimeout(stuckTimeout) 29 | } 30 | stuckTimeout = setTimeout(() => { 31 | console.log('') 32 | console.log('UPLOAD GOT STUCK') 33 | console.log('') 34 | }, 20000) 35 | 36 | setTimeout(() => uploadNext(), 0) 37 | }, 38 | (e) => { 39 | console.log('e', e) 40 | setTimeout(() => uploadNext(), 500) 41 | } 42 | ) 43 | } 44 | 45 | conn.on('error', console.log) 46 | conn.on('disconnected', () => { 47 | console.log('Connection lost') 48 | process.exit(0) 49 | }) 50 | conn.connect(IP).catch((e) => { 51 | console.error(e) 52 | process.exit(0) 53 | }) 54 | conn.once('connected', () => { 55 | console.log(`connected in ${Date.now() - uploadStarted}ms`) 56 | uploadNext() 57 | }) 58 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['ts', 'js'], 3 | transform: { 4 | '^.+\\.(ts|tsx)$': [ 5 | 'ts-jest', 6 | { 7 | tsconfig: 'tsconfig.json', 8 | }, 9 | ], 10 | }, 11 | testMatch: ['**/__tests__/**/*.spec.(ts|js)'], 12 | setupFilesAfterEnv: ['jest-extended/all'], 13 | testEnvironment: 'node', 14 | coverageThreshold: { 15 | global: { 16 | branches: 0, 17 | functions: 0, 18 | lines: 0, 19 | statements: 0, 20 | }, 21 | }, 22 | coverageDirectory: './coverage/', 23 | collectCoverage: true, 24 | collectCoverageFrom: [ 25 | 'src/**/*.{js,ts}', 26 | '!**/@types/**', 27 | '!**/__tests__/**', 28 | '!**/__mocks__/**', 29 | '!**/node_modules/**', 30 | '!**/dist/**', 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=nrkno_tv-automation-atem-connection 2 | sonar.organization=nrkno 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=sofie-atem-connection 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 -------------------------------------------------------------------------------- /src/@types/nanotimer.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'nanotimer' { 4 | namespace NanoTimer { 5 | interface TimeoutResults { 6 | waitTime: number 7 | } 8 | } 9 | 10 | class NanoTimer { 11 | constructor(log?: boolean) 12 | setTimeout( 13 | task: (...args: any[]) => void, 14 | args: any[], 15 | timeout: string, 16 | callback?: (results: NanoTimer.TimeoutResults) => void 17 | ): void 18 | clearTimeout(): void 19 | setInterval( 20 | task: (...args: any[]) => void, 21 | args: any[], 22 | interval: string, 23 | callback?: (error: Error) => void 24 | ): void 25 | clearInterval(): void 26 | time( 27 | task: (cb: () => void) => void, 28 | args: string | any[], 29 | interval: string, 30 | callback?: (error: Error) => void 31 | ): number 32 | hasTimeout(): boolean 33 | } 34 | 35 | export = NanoTimer 36 | } 37 | -------------------------------------------------------------------------------- /src/__tests__/connection/dump.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | /* eslint-disable node/no-missing-require */ 3 | const fs = require('fs') 4 | const path = require('path') 5 | const { AtemSocket } = require('../../../dist/lib/atemSocket') 6 | const { DEFAULT_PORT } = require('../../../dist') 7 | 8 | const args = process.argv.slice(2) 9 | if (args.length < 2) { 10 | console.log('Usage: node dump.js ') 11 | console.log('eg: node dump.js 10.42.13.99 case1') 12 | process.exit() 13 | } 14 | 15 | const socket = new AtemSocket({ 16 | debug: false, 17 | log: console.log, 18 | disableMultithreaded: true, 19 | address: args[0], 20 | port: DEFAULT_PORT, 21 | }) 22 | socket.on('disconnect', () => { 23 | console.log('disconnect') 24 | process.exit(1) 25 | }) 26 | 27 | const output = [] 28 | 29 | socket.on('receivedCommands', (cmds) => { 30 | const initComplete = cmds.find((cmd) => cmd.constructor.name === 'InitCompleteCommand') 31 | if (initComplete) { 32 | console.log('complete') 33 | const filePath = path.resolve(__dirname, `./${args[1]}.data`) 34 | fs.writeFileSync(filePath, output.join('\n')) 35 | process.exit(0) 36 | } 37 | }) 38 | 39 | const origParse = socket._parseCommands.bind(socket) 40 | socket._parseCommands = (payload) => { 41 | output.push(payload.toString('hex')) 42 | return origParse(payload) 43 | } 44 | 45 | socket.connect() 46 | console.log('connecting') 47 | -------------------------------------------------------------------------------- /src/__tests__/tally/dump.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | /* eslint-disable node/no-missing-require */ 3 | /** 4 | * A small helper script to generate a test case for the tally tests 5 | */ 6 | 7 | const { Atem } = require('../../../dist') 8 | const fs = require('fs') 9 | const path = require('path') 10 | 11 | const args = process.argv.slice(2) 12 | if (args.length < 2) { 13 | console.log('Usage: node dump.js ') 14 | console.log('eg: node dump.js 10.42.13.99 case1') 15 | process.exit() 16 | } 17 | 18 | const conn = new Atem({ debug: true }) 19 | conn.on('error', console.log) 20 | 21 | function writeJson(fileName, data) { 22 | const filePath = path.resolve(__dirname, fileName) 23 | fs.writeFileSync(filePath, JSON.stringify(data, undefined, '\t')) 24 | } 25 | 26 | conn.once('connected', () => { 27 | writeJson(`./${args[1]}-state.json`, { 28 | video: conn.state.video, 29 | inputs: conn.state.inputs, 30 | }) 31 | console.log('Wrote state file') 32 | 33 | // All done now! 34 | process.exit() 35 | }) 36 | conn.on('receivedCommand', (cmd) => { 37 | if (cmd.rawName === 'TlSr') { 38 | writeJson(`./${args[1]}-tally.json`, cmd.properties) 39 | console.log('Wrote tally file') 40 | } 41 | }) 42 | 43 | console.log(`Connecting to "${args[0]}", for case "${args[1]}"`) 44 | conn.connect(args[0]) 45 | -------------------------------------------------------------------------------- /src/commands/Audio/AudioMixerHeadphonesCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { Util } from '../..' 4 | import { ClassicAudioHeadphoneOutputChannel } from '../../state/audio' 5 | 6 | export class AudioMixerHeadphonesCommand extends WritableCommand { 7 | public static MaskFlags = { 8 | gain: 1 << 0, 9 | programOutGain: 1 << 1, 10 | talkbackGain: 1 << 2, 11 | sidetoneGain: 1 << 3, 12 | } 13 | public static readonly rawName = 'CAMH' 14 | 15 | public serialize(): Buffer { 16 | const buffer = Buffer.alloc(12) 17 | buffer.writeUInt8(this.flag, 0) 18 | buffer.writeUInt16BE(Util.DecibelToUInt16BE(this.properties.gain || 0), 2) 19 | buffer.writeUInt16BE(Util.DecibelToUInt16BE(this.properties.programOutGain || 0), 4) 20 | buffer.writeUInt16BE(Util.DecibelToUInt16BE(this.properties.talkbackGain || 0), 6) 21 | buffer.writeUInt16BE(Util.DecibelToUInt16BE(this.properties.sidetoneGain || 0), 8) 22 | return buffer 23 | } 24 | } 25 | 26 | export class AudioMixerHeadphonesUpdateCommand extends DeserializedCommand { 27 | public static readonly rawName = 'AMHP' 28 | 29 | public static deserialize(rawCommand: Buffer): AudioMixerHeadphonesUpdateCommand { 30 | const properties = { 31 | gain: Util.UInt16BEToDecibel(rawCommand.readUInt16BE(0)), 32 | programOutGain: Util.UInt16BEToDecibel(rawCommand.readUInt16BE(2)), 33 | talkbackGain: Util.UInt16BEToDecibel(rawCommand.readUInt16BE(4)), 34 | sidetoneGain: Util.UInt16BEToDecibel(rawCommand.readUInt16BE(6)), 35 | } 36 | 37 | return new AudioMixerHeadphonesUpdateCommand(properties) 38 | } 39 | 40 | public applyToState(state: AtemState): string { 41 | if (!state.audio) { 42 | throw new InvalidIdError('Classic Audio') 43 | } 44 | 45 | state.audio.headphones = { 46 | ...state.audio.headphones, 47 | ...this.properties, 48 | } 49 | return `audio.headphones` 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/Audio/AudioMixerMasterCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { Util } from '../..' 4 | import { AudioMasterChannel } from '../../state/audio' 5 | 6 | export class AudioMixerMasterCommand extends WritableCommand { 7 | public static MaskFlags = { 8 | gain: 1 << 0, 9 | balance: 1 << 1, 10 | followFadeToBlack: 1 << 2, 11 | } 12 | public static readonly rawName = 'CAMM' 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(8) 16 | buffer.writeUInt8(this.flag, 0) 17 | buffer.writeUInt16BE(Util.DecibelToUInt16BE(this.properties.gain || 0), 2) 18 | buffer.writeInt16BE(Util.BalanceToInt(this.properties.balance || 0), 4) 19 | buffer.writeUInt8(this.properties.followFadeToBlack ? 0x01 : 0x00, 6) // Note: I never got this one to work 20 | return buffer 21 | } 22 | } 23 | 24 | export class AudioMixerMasterUpdateCommand extends DeserializedCommand { 25 | public static readonly rawName = 'AMMO' 26 | 27 | public static deserialize(rawCommand: Buffer): AudioMixerMasterUpdateCommand { 28 | const properties = { 29 | gain: Util.UInt16BEToDecibel(rawCommand.readUInt16BE(0)), 30 | balance: Util.IntToBalance(rawCommand.readInt16BE(2)), 31 | followFadeToBlack: rawCommand.readInt8(4) === 1, 32 | } 33 | 34 | return new AudioMixerMasterUpdateCommand(properties) 35 | } 36 | 37 | public applyToState(state: AtemState): string { 38 | if (!state.audio) { 39 | throw new InvalidIdError('Classic Audio') 40 | } 41 | 42 | state.audio.master = { 43 | ...state.audio.master, 44 | ...this.properties, 45 | } 46 | return `audio.master` 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/Audio/AudioMixerMonitorCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { Util } from '../..' 4 | import { ClassicAudioMonitorChannel } from '../../state/audio' 5 | 6 | export class AudioMixerMonitorCommand extends WritableCommand { 7 | public static MaskFlags = { 8 | enabled: 1 << 0, 9 | gain: 1 << 1, 10 | mute: 1 << 2, 11 | solo: 1 << 3, 12 | soloSource: 1 << 4, 13 | dim: 1 << 5, 14 | dimLevel: 1 << 6, 15 | } 16 | public static readonly rawName = 'CAMm' 17 | 18 | public serialize(): Buffer { 19 | const buffer = Buffer.alloc(12) 20 | buffer.writeUInt8(this.flag, 0) 21 | buffer.writeUInt8(this.properties.enabled ? 1 : 0, 1) 22 | buffer.writeUInt16BE(Util.DecibelToUInt16BE(this.properties.gain || 0), 2) 23 | buffer.writeUInt8(this.properties.mute ? 1 : 0, 4) 24 | buffer.writeUInt8(this.properties.solo ? 1 : 0, 5) 25 | buffer.writeUInt16BE(this.properties.soloSource || 0, 6) 26 | buffer.writeUInt8(this.properties.dim ? 1 : 0, 8) 27 | buffer.writeUInt16BE(this.properties.dimLevel || 0, 10) 28 | return buffer 29 | } 30 | } 31 | 32 | export class AudioMixerMonitorUpdateCommand extends DeserializedCommand { 33 | public static readonly rawName = 'AMmO' 34 | 35 | public static deserialize(rawCommand: Buffer): AudioMixerMonitorUpdateCommand { 36 | const properties = { 37 | enabled: rawCommand.readUInt8(0) > 0, 38 | gain: Util.UInt16BEToDecibel(rawCommand.readUInt16BE(2)), 39 | 40 | mute: rawCommand.readUInt8(4) > 0, 41 | solo: rawCommand.readUInt8(5) > 0, 42 | soloSource: rawCommand.readUInt16BE(6), 43 | 44 | dim: rawCommand.readUInt8(8) > 0, 45 | dimLevel: rawCommand.readUInt16BE(10), 46 | } 47 | 48 | return new AudioMixerMonitorUpdateCommand(properties) 49 | } 50 | 51 | public applyToState(state: AtemState): string { 52 | if (!state.audio) { 53 | throw new InvalidIdError('Classic Audio') 54 | } 55 | 56 | state.audio.monitor = { 57 | ...state.audio.monitor, 58 | ...this.properties, 59 | } 60 | return `audio.monitor` 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/Audio/AudioMixerPropertiesCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | 4 | export class AudioMixerPropertiesCommand extends BasicWritableCommand<{ audioFollowVideo: boolean }> { 5 | public static readonly rawName = 'CAMP' 6 | 7 | public serialize(): Buffer { 8 | const buffer = Buffer.alloc(4) 9 | buffer.writeUInt8(1 << 0, 0) // Mask 10 | buffer.writeUInt8(this.properties.audioFollowVideo ? 1 : 0, 1) 11 | return buffer 12 | } 13 | } 14 | 15 | export class AudioMixerPropertiesUpdateCommand extends DeserializedCommand<{ audioFollowVideo: boolean }> { 16 | public static readonly rawName = 'AMPP' 17 | 18 | public static deserialize(rawCommand: Buffer): AudioMixerPropertiesUpdateCommand { 19 | const audioFollowVideo = rawCommand.readUInt8(0) > 0 20 | 21 | return new AudioMixerPropertiesUpdateCommand({ audioFollowVideo }) 22 | } 23 | 24 | public applyToState(state: AtemState): string { 25 | if (!state.audio) { 26 | throw new InvalidIdError('Classic Audio') 27 | } 28 | 29 | state.audio.audioFollowVideoCrossfadeTransitionEnabled = this.properties.audioFollowVideo 30 | return `audio.audioFollowVideoCrossfadeTransitionEnabled` 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/Audio/AudioMixerResetPeaksCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | 3 | export interface ClassicAudioResetPeaks { 4 | all: boolean 5 | input: number 6 | master: boolean 7 | monitor: boolean 8 | } 9 | 10 | export class AudioMixerResetPeaksCommand extends WritableCommand { 11 | public static MaskFlags = { 12 | all: 1 << 0, 13 | input: 1 << 1, 14 | master: 1 << 2, 15 | monitor: 1 << 3, 16 | } 17 | public static readonly rawName = 'RAMP' 18 | 19 | public serialize(): Buffer { 20 | const buffer = Buffer.alloc(8) 21 | buffer.writeUInt8(this.flag, 0) 22 | buffer.writeUInt8(this.properties.all ? 1 : 0, 1) 23 | buffer.writeUInt16BE(this.properties.input || 0, 2) 24 | buffer.writeUInt8(this.properties.master ? 1 : 0, 4) 25 | buffer.writeUInt8(this.properties.monitor ? 1 : 0, 5) 26 | return buffer 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/Audio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AudioMixerHeadphonesCommand' 2 | export * from './AudioMixerInputCommand' 3 | export * from './AudioMixerMasterCommand' 4 | export * from './AudioMixerMonitorCommand' 5 | export * from './AudioMixerPropertiesCommand' 6 | export * from './AudioMixerResetPeaksCommand' 7 | -------------------------------------------------------------------------------- /src/commands/AuxSourceCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from './CommandBase' 2 | import { AtemState } from '../state' 3 | 4 | export interface AuxSourceProps { 5 | source: number 6 | } 7 | 8 | export class AuxSourceCommand extends BasicWritableCommand { 9 | public static readonly rawName = 'CAuS' 10 | 11 | public readonly auxBus: number 12 | 13 | constructor(auxBus: number, source: number) { 14 | super({ source }) 15 | 16 | this.auxBus = auxBus 17 | } 18 | 19 | public serialize(): Buffer { 20 | const buffer = Buffer.alloc(4) 21 | buffer.writeUInt8(0x01, 0) 22 | buffer.writeUInt8(this.auxBus, 1) 23 | buffer.writeUInt16BE(this.properties.source, 2) 24 | return buffer 25 | } 26 | } 27 | 28 | export class AuxSourceUpdateCommand extends DeserializedCommand { 29 | public static readonly rawName = 'AuxS' 30 | 31 | public readonly auxBus: number 32 | 33 | constructor(auxBus: number, properties: AuxSourceProps) { 34 | super(properties) 35 | 36 | this.auxBus = auxBus 37 | } 38 | 39 | public static deserialize(rawCommand: Buffer): AuxSourceUpdateCommand { 40 | const auxBus = rawCommand.readUInt8(0) 41 | const properties = { 42 | source: rawCommand.readUInt16BE(2), 43 | } 44 | 45 | return new AuxSourceUpdateCommand(auxBus, properties) 46 | } 47 | 48 | public applyToState(state: AtemState): string { 49 | state.video.auxilliaries[this.auxBus] = this.properties.source 50 | return `video.auxilliaries.${this.auxBus}` 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/ColorGeneratorCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from './CommandBase' 2 | import { AtemState, ColorGeneratorState } from '../state' 3 | 4 | export class ColorGeneratorCommand extends WritableCommand { 5 | public static MaskFlags = { 6 | hue: 1 << 0, 7 | saturation: 1 << 1, 8 | luma: 1 << 2, 9 | } 10 | 11 | public static readonly rawName = 'CClV' 12 | 13 | public readonly index: number 14 | 15 | constructor(index: number) { 16 | super() 17 | 18 | this.index = index 19 | } 20 | 21 | public serialize(): Buffer { 22 | const buffer = Buffer.alloc(8) 23 | buffer.writeUInt8(this.flag, 0) 24 | buffer.writeUInt8(this.index, 1) 25 | buffer.writeUInt16BE(this.properties.hue || 0, 2) 26 | buffer.writeUInt16BE(this.properties.saturation || 0, 4) 27 | buffer.writeUInt16BE(this.properties.luma || 0, 6) 28 | return buffer 29 | } 30 | } 31 | 32 | export class ColorGeneratorUpdateCommand extends DeserializedCommand { 33 | public static readonly rawName = 'ColV' 34 | 35 | public readonly index: number 36 | 37 | constructor(index: number, properties: ColorGeneratorState) { 38 | super(properties) 39 | 40 | this.index = index 41 | } 42 | 43 | public static deserialize(rawCommand: Buffer): ColorGeneratorUpdateCommand { 44 | const auxBus = rawCommand.readUInt8(0) 45 | const properties = { 46 | hue: rawCommand.readUInt16BE(2), 47 | saturation: rawCommand.readUInt16BE(4), 48 | luma: rawCommand.readUInt16BE(6), 49 | } 50 | 51 | return new ColorGeneratorUpdateCommand(auxBus, properties) 52 | } 53 | 54 | public applyToState(state: AtemState): string { 55 | if (!state.colorGenerators) state.colorGenerators = {} 56 | 57 | state.colorGenerators[this.index] = this.properties 58 | return `colorGenerators.${this.index}` 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferAckCommand.ts: -------------------------------------------------------------------------------- 1 | import { SymmetricalCommand } from '../CommandBase' 2 | 3 | export interface DataTransferAckProps { 4 | transferId: number 5 | transferIndex: number 6 | } 7 | 8 | export class DataTransferAckCommand extends SymmetricalCommand { 9 | public static readonly rawName = 'FTUA' 10 | 11 | public static deserialize(rawCommand: Buffer): DataTransferAckCommand { 12 | const properties = { 13 | transferId: rawCommand.readUInt16BE(0), 14 | transferIndex: rawCommand.readUInt8(2), 15 | } 16 | 17 | return new DataTransferAckCommand(properties) 18 | } 19 | 20 | public serialize(): Buffer { 21 | const buffer = Buffer.alloc(4) 22 | 23 | buffer.writeUInt16BE(this.properties.transferId) 24 | buffer.writeUInt8(this.properties.transferIndex, 2) 25 | 26 | return buffer 27 | } 28 | 29 | public applyToState(): string[] { 30 | // Nothing to do 31 | return [] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferCompleteCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | 3 | export class DataTransferCompleteCommand extends DeserializedCommand<{ transferId: number }> { 4 | public static readonly rawName = 'FTDC' 5 | 6 | public static deserialize(rawCommand: Buffer): DataTransferCompleteCommand { 7 | const properties = { 8 | transferId: rawCommand.readUInt16BE(0), 9 | } 10 | 11 | return new DataTransferCompleteCommand(properties) 12 | } 13 | 14 | public applyToState(): string[] { 15 | // Nothing to do 16 | return [] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferDataCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, IDeserializedCommand } from '../CommandBase' 2 | 3 | export interface DataTransferDataProps { 4 | transferId: number 5 | body: Buffer 6 | } 7 | 8 | export class DataTransferDataCommand 9 | extends BasicWritableCommand 10 | implements IDeserializedCommand 11 | { 12 | public static readonly rawName = 'FTDa' 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt16BE(this.properties.transferId, 0) 17 | buffer.writeUInt16BE(this.properties.body.length, 2) 18 | 19 | return Buffer.concat([buffer, this.properties.body]) 20 | } 21 | 22 | public static deserialize(rawCommand: Buffer): DataTransferDataCommand { 23 | const properties = { 24 | transferId: rawCommand.readUInt16BE(0), 25 | size: rawCommand.readUInt16BE(2), 26 | body: rawCommand.slice(4, 4 + rawCommand.readUInt16BE(2)), 27 | } 28 | 29 | return new DataTransferDataCommand(properties) 30 | } 31 | 32 | public applyToState(): string[] { 33 | // Nothing to do 34 | return [] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferDownloadRequestCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export interface DataTransferDownloadRequestProps { 4 | transferId: number 5 | transferStoreId: number 6 | transferIndex: number 7 | transferType: number 8 | } 9 | 10 | export class DataTransferDownloadRequestCommand extends BasicWritableCommand { 11 | public static readonly rawName = 'FTSU' 12 | 13 | public serialize(): Buffer { 14 | const buffer = Buffer.alloc(12) 15 | buffer.writeUInt16BE(this.properties.transferId, 0) 16 | buffer.writeUInt16BE(this.properties.transferStoreId, 2) 17 | buffer.writeUInt16BE(this.properties.transferIndex, 6) 18 | 19 | buffer.writeUInt16BE(this.properties.transferType, 8) 20 | 21 | return buffer 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferErrorCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | 3 | /** The known error codes */ 4 | export enum ErrorCode { 5 | Retry = 1, 6 | NotFound = 2, 7 | NotLocked = 5, // Maybe 8 | } 9 | 10 | export interface DataTransferErrorProps { 11 | transferId: number 12 | errorCode: ErrorCode 13 | } 14 | 15 | export class DataTransferErrorCommand extends DeserializedCommand { 16 | public static readonly rawName = 'FTDE' 17 | 18 | public static deserialize(rawCommand: Buffer): DataTransferErrorCommand { 19 | const properties = { 20 | transferId: rawCommand.readUInt16BE(0), 21 | errorCode: rawCommand.readUInt8(2), 22 | } 23 | 24 | return new DataTransferErrorCommand(properties) 25 | } 26 | 27 | public applyToState(): string[] { 28 | // Nothing to do 29 | return [] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferFileDescriptionCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export interface DataTransferFileDescriptionProps { 4 | transferId: number 5 | name: string | undefined 6 | description: string | undefined 7 | fileHash: string 8 | } 9 | 10 | export class DataTransferFileDescriptionCommand extends BasicWritableCommand { 11 | public static readonly rawName = 'FTFD' 12 | 13 | public serialize(): Buffer { 14 | const buffer = Buffer.alloc(212) 15 | buffer.writeUInt16BE(this.properties.transferId, 0) 16 | if (this.properties.name) buffer.write(this.properties.name, 2, 64) 17 | if (this.properties.description) buffer.write(this.properties.description, 66, 128) 18 | Buffer.from(this.properties.fileHash, 'base64').copy(buffer, 194, 0, 16) 19 | 20 | return buffer 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferUploadContinueCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | 3 | export interface DataTransferUploadContinueProps { 4 | transferId: number 5 | chunkSize: number 6 | chunkCount: number 7 | } 8 | 9 | export class DataTransferUploadContinueCommand extends DeserializedCommand { 10 | public static readonly rawName = 'FTCD' 11 | 12 | public static deserialize(rawCommand: Buffer): DataTransferUploadContinueCommand { 13 | const properties = { 14 | transferId: rawCommand.readUInt16BE(0), 15 | chunkSize: rawCommand.readUInt16BE(6), 16 | chunkCount: rawCommand.readUInt16BE(8), 17 | } 18 | 19 | return new DataTransferUploadContinueCommand(properties) 20 | } 21 | 22 | public applyToState(): string[] { 23 | // Nothing to do 24 | return [] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/DataTransferUploadRequestCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export interface DataTransferUploadRequestProps { 4 | transferId: number 5 | transferStoreId: number 6 | transferIndex: number 7 | size: number 8 | mode: number // Note: maybe this should be an enum, but we don't have a good description, and it shouldn't be used externally 9 | } 10 | 11 | export class DataTransferUploadRequestCommand extends BasicWritableCommand { 12 | public static readonly rawName = 'FTSD' 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(16) 16 | buffer.writeUInt16BE(this.properties.transferId, 0) 17 | buffer.writeUInt16BE(this.properties.transferStoreId, 2) 18 | buffer.writeUInt16BE(this.properties.transferIndex, 6) 19 | buffer.writeUInt32BE(this.properties.size, 8) 20 | buffer.writeUInt16BE(this.properties.mode, 12) 21 | return buffer 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/LockObtainedCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | 3 | export class LockObtainedCommand extends DeserializedCommand<{ index: number }> { 4 | public static readonly rawName = 'LKOB' 5 | 6 | constructor(index: number) { 7 | super({ index }) 8 | } 9 | 10 | public static deserialize(rawCommand: Buffer): LockObtainedCommand { 11 | const index = rawCommand.readUInt16BE(0) 12 | 13 | return new LockObtainedCommand(index) 14 | } 15 | 16 | public applyToState(): string[] { 17 | // nothing to do 18 | return [] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/LockStateCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | 3 | export interface LockStateProps { 4 | index: number 5 | locked: boolean 6 | } 7 | 8 | export class LockStateCommand extends BasicWritableCommand { 9 | public static readonly rawName = 'LOCK' 10 | 11 | constructor(index: number, locked: boolean) { 12 | super({ index, locked }) 13 | } 14 | 15 | public serialize(): Buffer { 16 | const buffer = Buffer.alloc(4) 17 | buffer.writeUInt16BE(this.properties.index, 0) 18 | buffer.writeUInt8(this.properties.locked ? 1 : 0, 2) 19 | return buffer 20 | } 21 | } 22 | 23 | export class LockStateUpdateCommand extends DeserializedCommand { 24 | public static readonly rawName = 'LKST' 25 | 26 | public static deserialize(rawCommand: Buffer): LockStateUpdateCommand { 27 | const properties = { 28 | index: rawCommand.readUInt16BE(0), 29 | locked: rawCommand.readUInt8(2) === 1, 30 | } 31 | 32 | return new LockStateUpdateCommand(properties) 33 | } 34 | 35 | public applyToState(): string | string[] { 36 | // Nothing to do 37 | return [] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/DataTransfer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DataTransferAckCommand' 2 | export * from './DataTransferCompleteCommand' 3 | export * from './DataTransferDataCommand' 4 | export * from './DataTransferDownloadRequestCommand' 5 | export * from './DataTransferErrorCommand' 6 | export * from './DataTransferFileDescriptionCommand' 7 | export * from './DataTransferUploadContinueCommand' 8 | export * from './DataTransferUploadRequestCommand' 9 | export * from './LockObtainedCommand' 10 | export * from './LockStateCommand' 11 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/audioMixerConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { AudioMixerInfo } from '../../state/info' 4 | 5 | export class AudioMixerConfigCommand extends DeserializedCommand { 6 | public static readonly rawName = '_AMC' 7 | 8 | constructor(properties: AudioMixerInfo) { 9 | super(properties) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): AudioMixerConfigCommand { 13 | return new AudioMixerConfigCommand({ 14 | inputs: rawCommand.readUInt8(0), 15 | monitors: rawCommand.readUInt8(1), 16 | headphones: rawCommand.readUInt8(2), 17 | }) 18 | } 19 | 20 | public applyToState(state: AtemState): string[] { 21 | state.info.audioMixer = this.properties 22 | state.audio = { 23 | numberOfChannels: this.properties.inputs, 24 | hasMonitor: this.properties.monitors != 0, 25 | channels: [], 26 | } 27 | 28 | return [`info.audioMixer`, `audio`] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/fairlightAudioMixerConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { FairlightAudioMixerInfo } from '../../state/info' 4 | 5 | export class FairlightAudioMixerConfigCommand extends DeserializedCommand { 6 | public static readonly rawName = '_FAC' 7 | 8 | constructor(properties: FairlightAudioMixerInfo) { 9 | super(properties) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): FairlightAudioMixerConfigCommand { 13 | return new FairlightAudioMixerConfigCommand({ 14 | inputs: rawCommand.readUInt8(0), 15 | monitors: rawCommand.readUInt8(1), 16 | }) 17 | } 18 | 19 | public applyToState(state: AtemState): string[] { 20 | state.info.fairlightMixer = this.properties 21 | state.fairlight = { 22 | inputs: {}, 23 | } 24 | 25 | return [`info.fairlightMixer`, `fairlight.inputs`] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './versionCommand' 2 | export * from './topologyCommand' 3 | export * from './productIdentifierCommand' 4 | export * from './superSourceConfigCommand' 5 | export * from './audioMixerConfigCommand' 6 | export * from './macroPoolConfigCommand' 7 | export * from './mediaPoolConfigCommand' 8 | export * from './mixEffectBlockConfigCommand' 9 | export * from './multiviewerConfigCommand' 10 | export * from './fairlightAudioMixerConfigCommand' 11 | export * from './videoMixerConfigCommand' 12 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/macroPoolConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { MacroPoolInfo } from '../../state/info' 4 | 5 | export class MacroPoolConfigCommand extends DeserializedCommand { 6 | public static readonly rawName = '_MAC' 7 | 8 | constructor(properties: MacroPoolInfo) { 9 | super(properties) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): MacroPoolConfigCommand { 13 | return new MacroPoolConfigCommand({ 14 | macroCount: rawCommand.readUInt8(0), 15 | }) 16 | } 17 | 18 | public applyToState(state: AtemState): string { 19 | state.info.macroPool = this.properties 20 | return `info.macroPool` 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/mediaPoolConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { MediaPoolInfo } from '../../state/info' 4 | 5 | export class MediaPoolConfigCommand extends DeserializedCommand { 6 | public static readonly rawName = '_mpl' 7 | 8 | constructor(properties: MediaPoolInfo) { 9 | super(properties) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): MediaPoolConfigCommand { 13 | return new MediaPoolConfigCommand({ 14 | stillCount: rawCommand.readUInt8(0), 15 | clipCount: rawCommand.readUInt8(1), 16 | }) 17 | } 18 | 19 | public applyToState(state: AtemState): string { 20 | state.info.mediaPool = this.properties 21 | return `info.mediaPool` 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/mixEffectBlockConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { MixEffectInfo } from '../../state/info' 4 | 5 | export class MixEffectBlockConfigCommand extends DeserializedCommand { 6 | public static readonly rawName = '_MeC' 7 | 8 | public readonly index: number 9 | 10 | constructor(index: number, properties: MixEffectInfo) { 11 | super(properties) 12 | 13 | this.index = index 14 | } 15 | 16 | public static deserialize(rawCommand: Buffer): MixEffectBlockConfigCommand { 17 | return new MixEffectBlockConfigCommand(rawCommand.readUInt8(0), { keyCount: rawCommand.readUInt8(1) }) 18 | } 19 | 20 | public applyToState(state: AtemState): string { 21 | state.info.mixEffects[this.index] = this.properties 22 | return `info.mixEffects` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/multiviewerConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { MultiviewerInfo } from '../../state/info' 4 | import { ProtocolVersion } from '../../enums' 5 | 6 | export class MultiviewerConfigCommand extends DeserializedCommand { 7 | public static readonly rawName = '_MvC' 8 | 9 | constructor(properties: MultiviewerInfo) { 10 | super(properties) 11 | } 12 | 13 | public static deserialize(rawCommand: Buffer, version: ProtocolVersion): MultiviewerConfigCommand { 14 | if (version >= ProtocolVersion.V8_1_1) { 15 | return new MultiviewerConfigCommand({ 16 | count: -1, 17 | windowCount: rawCommand.readUInt8(1), 18 | }) 19 | } else { 20 | return new MultiviewerConfigCommand({ 21 | count: rawCommand.readUInt8(0), 22 | windowCount: rawCommand.readUInt8(1), 23 | }) 24 | } 25 | } 26 | 27 | public applyToState(state: AtemState): string { 28 | if (this.properties.count === -1) { 29 | state.info.multiviewer = { 30 | count: -1, 31 | ...state.info.multiviewer, 32 | windowCount: this.properties.windowCount, 33 | } 34 | } else { 35 | state.info.multiviewer = this.properties 36 | } 37 | return `info.multiviewer` 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/productIdentifierCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import * as Util from '../../lib/atemUtil' 4 | import { Enums } from '../..' 5 | import { DeviceInfo } from '../../state/info' 6 | 7 | export class ProductIdentifierCommand extends DeserializedCommand> { 8 | public static readonly rawName = '_pin' 9 | 10 | public static deserialize(rawCommand: Buffer): ProductIdentifierCommand { 11 | const properties = { 12 | productIdentifier: Util.bufToNullTerminatedString(rawCommand, 0, 40), 13 | model: rawCommand.readUInt8(40), 14 | } 15 | 16 | return new ProductIdentifierCommand(properties) 17 | } 18 | 19 | public applyToState(state: AtemState): string { 20 | state.info.productIdentifier = this.properties.productIdentifier 21 | state.info.model = this.properties.model 22 | 23 | // Model specific features that aren't specified by the protocol 24 | switch (state.info.model) { 25 | case Enums.Model.TwoME: 26 | case Enums.Model.TwoME4K: 27 | case Enums.Model.TwoMEBS4K: 28 | case Enums.Model.Constellation: 29 | case Enums.Model.Constellation8K: 30 | case Enums.Model.ConstellationHD4ME: 31 | case Enums.Model.Constellation4K4ME: 32 | state.info.power = [false, false] 33 | break 34 | default: 35 | state.info.power = [false] 36 | break 37 | } 38 | 39 | return `info` 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/superSourceConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { ProtocolVersion } from '../../enums' 4 | import { SuperSourceInfo } from '../../state/info' 5 | 6 | export class SuperSourceConfigCommand extends DeserializedCommand { 7 | public static readonly rawName = '_SSC' 8 | 9 | public readonly ssrcId: number 10 | 11 | constructor(ssrcId: number, properties: SuperSourceInfo) { 12 | super(properties) 13 | 14 | this.ssrcId = ssrcId 15 | } 16 | 17 | public static deserialize(rawCommand: Buffer, version: ProtocolVersion): SuperSourceConfigCommand { 18 | if (version >= ProtocolVersion.V8_0) { 19 | return new SuperSourceConfigCommand(rawCommand.readUInt8(0), { boxCount: rawCommand.readUInt8(2) }) 20 | } else { 21 | return new SuperSourceConfigCommand(0, { boxCount: rawCommand.readUInt8(0) }) 22 | } 23 | } 24 | 25 | public applyToState(state: AtemState): string { 26 | state.info.superSources[this.ssrcId] = this.properties 27 | return `info.superSources` 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/topologyCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { AtemCapabilites } from '../../state/info' 4 | import { ProtocolVersion } from '../../enums' 5 | import { Mutable } from '../../lib/types' 6 | 7 | export class TopologyCommand extends DeserializedCommand { 8 | public static readonly rawName = '_top' 9 | 10 | public static deserialize(rawCommand: Buffer, version: ProtocolVersion): TopologyCommand { 11 | const v230offset = version > ProtocolVersion.V8_0_1 ? 1 : 0 12 | const properties: Mutable = { 13 | mixEffects: rawCommand.readUInt8(0), 14 | sources: rawCommand.readUInt8(1), 15 | downstreamKeyers: rawCommand.readUInt8(2), 16 | auxilliaries: rawCommand.readUInt8(3), 17 | mixMinusOutputs: rawCommand.readUInt8(4), 18 | mediaPlayers: rawCommand.readUInt8(5), 19 | multiviewers: v230offset > 0 ? rawCommand.readUInt8(6) : -1, 20 | serialPorts: rawCommand.readUInt8(6 + v230offset), 21 | maxHyperdecks: rawCommand.readUInt8(7 + v230offset), 22 | DVEs: rawCommand.readUInt8(8 + v230offset), 23 | stingers: rawCommand.readUInt8(9 + v230offset), 24 | superSources: rawCommand.readUInt8(10 + v230offset), 25 | talkbackChannels: rawCommand.readUInt8(12 + v230offset), 26 | 27 | cameraControl: rawCommand.readUInt8(17 + v230offset) === 1, 28 | 29 | // Note: these are defined below as they can overflow in older firmwares 30 | advancedChromaKeyers: false, 31 | onlyConfigurableOutputs: false, 32 | } 33 | 34 | // in 7.4? 35 | if (rawCommand.length > 20) { 36 | properties.advancedChromaKeyers = rawCommand.readUInt8(21 + v230offset) === 1 37 | properties.onlyConfigurableOutputs = rawCommand.readUInt8(22 + v230offset) === 1 38 | } 39 | 40 | return new TopologyCommand(properties) 41 | } 42 | 43 | public applyToState(state: AtemState): string { 44 | state.info.capabilities = { 45 | ...state.info.capabilities, 46 | ...this.properties, 47 | } 48 | if (this.properties.multiviewers > 0) { 49 | state.info.multiviewer = { 50 | windowCount: 10, 51 | ...state.info.multiviewer, 52 | count: this.properties.multiviewers, 53 | } 54 | } 55 | return `info.capabilities` 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/versionCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { ProtocolVersion } from '../../enums' 4 | 5 | export class VersionCommand extends DeserializedCommand<{ version: ProtocolVersion }> { 6 | public static readonly rawName = '_ver' 7 | 8 | constructor(version: ProtocolVersion) { 9 | super({ version }) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): VersionCommand { 13 | const version = rawCommand.readUInt32BE(0) 14 | 15 | return new VersionCommand(version) 16 | } 17 | 18 | public applyToState(state: AtemState): string { 19 | state.info.apiVersion = this.properties.version 20 | return `info.apiVersion` 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/DeviceProfile/videoMixerConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { SupportedVideoMode } from '../../state/info' 4 | import { ProtocolVersion, VideoMode } from '../../enums' 5 | 6 | export class VideoMixerConfigCommand extends DeserializedCommand> { 7 | public static readonly rawName = '_VMC' 8 | 9 | constructor(properties: Array) { 10 | super(properties) 11 | } 12 | 13 | public static deserialize(rawCommand: Buffer, version: ProtocolVersion): VideoMixerConfigCommand { 14 | const modes: Array = [] 15 | 16 | const hasRequiresReconfig = version >= ProtocolVersion.V8_0 17 | const size = hasRequiresReconfig ? 13 : 12 18 | 19 | const count = rawCommand.readUInt16BE(0) 20 | for (let i = 0; i < count; i++) { 21 | const baseOffset = 4 + i * size 22 | 23 | modes.push({ 24 | mode: rawCommand.readUInt8(baseOffset), 25 | multiviewerModes: readVideoModeBitmask(rawCommand.readUInt32BE(baseOffset + 4)), 26 | downConvertModes: readVideoModeBitmask(rawCommand.readUInt32BE(baseOffset + 8)), 27 | requiresReconfig: hasRequiresReconfig ? rawCommand.readUInt8(baseOffset + 12) !== 0 : false, 28 | }) 29 | } 30 | 31 | return new VideoMixerConfigCommand(modes) 32 | } 33 | 34 | public applyToState(state: AtemState): string | string[] { 35 | state.info.supportedVideoModes = this.properties 36 | return `info.supportedVideoModes` 37 | } 38 | } 39 | 40 | function readVideoModeBitmask(rawVal: number): Array { 41 | const modes: Array = [] 42 | for (const possibleMode of Object.values(VideoMode)) { 43 | if (typeof possibleMode === 'number' && (rawVal & (1 << possibleMode)) != 0) modes.push(possibleMode) 44 | } 45 | 46 | return modes 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/DisplayClock/DisplayClockCurrentTimeCommand.ts: -------------------------------------------------------------------------------- 1 | import { DisplayClockTime } from '../../state/displayClock' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { DeserializedCommand } from '../CommandBase' 4 | 5 | export class DisplayClockCurrentTimeCommand extends DeserializedCommand<{ time: DisplayClockTime }> { 6 | public static readonly rawName = 'DSTV' 7 | 8 | constructor(time: DisplayClockTime) { 9 | super({ time }) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): DisplayClockCurrentTimeCommand { 13 | // Future: id at byte 0 14 | 15 | const time: DisplayClockTime = { 16 | hours: rawCommand.readUint8(1), 17 | minutes: rawCommand.readUint8(2), 18 | seconds: rawCommand.readUint8(3), 19 | frames: rawCommand.readUint8(4), 20 | } 21 | 22 | return new DisplayClockCurrentTimeCommand(time) 23 | } 24 | 25 | public applyToState(state: AtemState): string { 26 | if (!state.displayClock) { 27 | throw new InvalidIdError('DisplayClock') 28 | } 29 | 30 | state.displayClock.currentTime = this.properties.time 31 | return 'displayClock.currentTime' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/DisplayClock/DisplayClockPropertiesGetCommand.ts: -------------------------------------------------------------------------------- 1 | import { DisplayClockProperties } from '../../state/displayClock' 2 | import { AtemState } from '../../state' 3 | import { DeserializedCommand } from '../CommandBase' 4 | 5 | export class DisplayClockPropertiesGetCommand extends DeserializedCommand { 6 | public static readonly rawName = 'DCPV' 7 | 8 | constructor(props: DisplayClockProperties) { 9 | super(props) 10 | } 11 | 12 | public static deserialize(rawCommand: Buffer): DisplayClockPropertiesGetCommand { 13 | // Future: id at byte 0 14 | 15 | const props: DisplayClockProperties = { 16 | enabled: !!rawCommand.readUint8(1), 17 | size: rawCommand.readUint8(3), 18 | opacity: rawCommand.readUint8(5), 19 | positionX: rawCommand.readInt16BE(6), 20 | positionY: rawCommand.readInt16BE(8), 21 | autoHide: !!rawCommand.readUint8(10), 22 | startFrom: { 23 | hours: rawCommand.readUint8(11), 24 | minutes: rawCommand.readUint8(12), 25 | seconds: rawCommand.readUint8(13), 26 | frames: rawCommand.readUint8(14), 27 | }, 28 | clockMode: rawCommand.readUint8(15), 29 | clockState: rawCommand.readUint8(16), 30 | } 31 | 32 | return new DisplayClockPropertiesGetCommand(props) 33 | } 34 | 35 | public applyToState(state: AtemState): string { 36 | state.displayClock = { 37 | currentTime: { 38 | hours: 0, 39 | minutes: 0, 40 | seconds: 0, 41 | frames: 0, 42 | }, 43 | ...state.displayClock, 44 | properties: this.properties, 45 | } 46 | return 'displayClock.properties' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/DisplayClock/DisplayClockPropertiesSetCommand.ts: -------------------------------------------------------------------------------- 1 | import { DisplayClockProperties } from '../../state/displayClock' 2 | import { WritableCommand } from '../CommandBase' 3 | 4 | export interface DisplayClockPropertiesExt extends DisplayClockProperties { 5 | startFromFrames: number 6 | } 7 | 8 | export class DisplayClockPropertiesSetCommand extends WritableCommand { 9 | public static MaskFlags = { 10 | enabled: 1 << 0, 11 | size: 1 << 1, 12 | opacity: 1 << 2, 13 | positionX: 1 << 3, 14 | positionY: 1 << 4, 15 | autoHide: 1 << 5, 16 | startFrom: 1 << 6, 17 | startFromFrames: 1 << 7, 18 | clockMode: 1 << 8, 19 | } 20 | public static readonly rawName = 'DCPC' 21 | 22 | public serialize(): Buffer { 23 | const buffer = Buffer.alloc(28) 24 | 25 | // Future: id at byte 2 26 | 27 | buffer.writeUint16BE(this.flag, 0) 28 | buffer.writeUint8(this.properties.enabled ? 1 : 0, 3) 29 | buffer.writeUint8(this.properties.size ?? 0, 5) 30 | buffer.writeUint8(this.properties.opacity ?? 0, 7) 31 | buffer.writeInt16BE(this.properties.positionX ?? 0, 8) 32 | buffer.writeInt16BE(this.properties.positionY ?? 0, 10) 33 | buffer.writeUint8(this.properties.autoHide ? 1 : 0, 12) 34 | 35 | buffer.writeUint8(this.properties.startFrom?.hours ?? 0, 13) 36 | buffer.writeUint8(this.properties.startFrom?.minutes ?? 0, 14) 37 | buffer.writeUint8(this.properties.startFrom?.seconds ?? 0, 15) 38 | buffer.writeUint8(this.properties.startFrom?.frames ?? 0, 16) 39 | 40 | buffer.writeUint32BE(this.properties.startFromFrames ?? 0, 20) 41 | 42 | buffer.writeUint8(this.properties.clockMode ?? 0, 24) 43 | 44 | return buffer 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/DisplayClock/DisplayClockRequestTimeCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class DisplayClockRequestTimeCommand extends BasicWritableCommand> { 4 | public static readonly rawName = 'DSTR' 5 | 6 | constructor() { 7 | super({}) 8 | } 9 | 10 | public serialize(): Buffer { 11 | // Future: id at byte 0 12 | return Buffer.alloc(4) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/DisplayClock/DisplayClockStateSetCommand.ts: -------------------------------------------------------------------------------- 1 | import { DisplayClockClockState } from '../../enums' 2 | import { BasicWritableCommand } from '../CommandBase' 3 | 4 | export class DisplayClockStateSetCommand extends BasicWritableCommand<{ state: DisplayClockClockState }> { 5 | public static readonly rawName = 'DCSC' 6 | 7 | constructor(state: DisplayClockClockState) { 8 | super({ state }) 9 | } 10 | 11 | public serialize(): Buffer { 12 | // Future: id at byte 0 13 | const buffer = Buffer.alloc(4) 14 | 15 | buffer.writeUint8(this.properties.state, 1) 16 | 17 | return buffer 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/DisplayClock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DisplayClockCurrentTimeCommand' 2 | export * from './DisplayClockPropertiesGetCommand' 3 | export * from './DisplayClockPropertiesSetCommand' 4 | export * from './DisplayClockRequestTimeCommand' 5 | export * from './DisplayClockStateSetCommand' 6 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyAutoCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | import { ProtocolVersion } from '../../enums' 3 | 4 | export class DownstreamKeyAutoCommand extends WritableCommand<{ isTowardsOnAir: boolean }> { 5 | public static readonly MaskFlags = { 6 | isTowardsOnAir: 1, 7 | } 8 | public static readonly rawName = 'DDsA' 9 | 10 | public readonly downstreamKeyerId: number 11 | 12 | constructor(downstreamKeyerId: number) { 13 | super() 14 | 15 | this.downstreamKeyerId = downstreamKeyerId 16 | } 17 | 18 | public serialize(version: ProtocolVersion): Buffer { 19 | const buffer = Buffer.alloc(4) 20 | 21 | if (version >= ProtocolVersion.V8_0_1) { 22 | buffer.writeUInt8(this.flag, 0) 23 | buffer.writeUInt8(this.downstreamKeyerId, 1) 24 | buffer.writeUInt8(this.properties.isTowardsOnAir ? 1 : 0, 2) 25 | } else { 26 | buffer.writeUInt8(this.downstreamKeyerId, 0) 27 | } 28 | 29 | return buffer 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyCutSourceCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class DownstreamKeyCutSourceCommand extends BasicWritableCommand<{ input: number }> { 4 | public static readonly rawName = 'CDsC' 5 | 6 | public readonly downstreamKeyerId: number 7 | 8 | constructor(downstreamKeyerId: number, input: number) { 9 | super({ input }) 10 | 11 | this.downstreamKeyerId = downstreamKeyerId 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.downstreamKeyerId, 0) 17 | buffer.writeUInt16BE(this.properties.input, 2) 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyFillSourceCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class DownstreamKeyFillSourceCommand extends BasicWritableCommand<{ input: number }> { 4 | public static readonly rawName = 'CDsF' 5 | 6 | public readonly downstreamKeyerId: number 7 | 8 | constructor(downstreamKeyerId: number, input: number) { 9 | super({ input }) 10 | 11 | this.downstreamKeyerId = downstreamKeyerId 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.downstreamKeyerId, 0) 17 | buffer.writeUInt16BE(this.properties.input, 2) 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyGeneralCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | import { DownstreamKeyerGeneral } from '../../state/video/downstreamKeyers' 3 | 4 | export class DownstreamKeyGeneralCommand extends WritableCommand { 5 | public static MaskFlags = { 6 | preMultiply: 1 << 0, 7 | clip: 1 << 1, 8 | gain: 1 << 2, 9 | invert: 1 << 3, 10 | } 11 | 12 | public static readonly rawName = 'CDsG' 13 | 14 | public readonly downstreamKeyerId: number 15 | 16 | constructor(downstreamKeyerId: number) { 17 | super() 18 | 19 | this.downstreamKeyerId = downstreamKeyerId 20 | } 21 | 22 | public serialize(): Buffer { 23 | const buffer = Buffer.alloc(12) 24 | buffer.writeUInt8(this.flag, 0) 25 | buffer.writeUInt8(this.downstreamKeyerId, 1) 26 | buffer.writeUInt8(this.properties.preMultiply ? 1 : 0, 2) 27 | buffer.writeInt16BE(this.properties.clip || 0, 4) 28 | buffer.writeInt16BE(this.properties.gain || 0, 6) 29 | buffer.writeUInt8(this.properties.invert ? 1 : 0, 8) 30 | return buffer 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyMaskCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | import { DownstreamKeyerMask } from '../../state/video/downstreamKeyers' 3 | 4 | export class DownstreamKeyMaskCommand extends WritableCommand { 5 | public static MaskFlags = { 6 | enabled: 1 << 0, 7 | top: 1 << 1, 8 | bottom: 1 << 2, 9 | left: 1 << 3, 10 | right: 1 << 4, 11 | } 12 | 13 | public static readonly rawName = 'CDsM' 14 | 15 | public readonly downstreamKeyerId: number 16 | 17 | constructor(downstreamKeyerId: number) { 18 | super() 19 | 20 | this.downstreamKeyerId = downstreamKeyerId 21 | } 22 | 23 | public serialize(): Buffer { 24 | const buffer = Buffer.alloc(12) 25 | buffer.writeUInt8(this.flag, 0) 26 | buffer.writeUInt8(this.downstreamKeyerId, 1) 27 | buffer.writeUInt8(this.properties.enabled ? 1 : 0, 2) 28 | 29 | buffer.writeInt16BE(this.properties.top || 0, 4) 30 | buffer.writeInt16BE(this.properties.bottom || 0, 6) 31 | buffer.writeInt16BE(this.properties.left || 0, 8) 32 | buffer.writeInt16BE(this.properties.right || 0, 10) 33 | return buffer 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyOnAirCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class DownstreamKeyOnAirCommand extends BasicWritableCommand<{ onAir: boolean }> { 4 | public static readonly rawName = 'CDsL' 5 | 6 | public readonly downstreamKeyerId: number 7 | 8 | constructor(downstreamKeyerId: number, onAir: boolean) { 9 | super({ onAir }) 10 | 11 | this.downstreamKeyerId = downstreamKeyerId 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.downstreamKeyerId, 0) 17 | buffer.writeUInt8(this.properties.onAir ? 1 : 0, 1) 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyPropertiesCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 3 | import { DownstreamKeyerProperties } from '../../state/video/downstreamKeyers' 4 | 5 | export class DownstreamKeyPropertiesCommand extends DeserializedCommand { 6 | public static readonly rawName = 'DskP' 7 | 8 | public readonly downstreamKeyerId: number 9 | 10 | constructor(downstreamKeyerId: number, properties: DownstreamKeyerProperties) { 11 | super(properties) 12 | 13 | this.downstreamKeyerId = downstreamKeyerId 14 | } 15 | 16 | public static deserialize(rawCommand: Buffer): DownstreamKeyPropertiesCommand { 17 | const downstreamKeyerId = rawCommand.readUInt8(0) 18 | const properties = { 19 | tie: rawCommand.readUInt8(1) === 1, 20 | rate: rawCommand.readUInt8(2), 21 | 22 | preMultiply: rawCommand.readUInt8(3) === 1, 23 | clip: rawCommand.readUInt16BE(4), 24 | gain: rawCommand.readUInt16BE(6), 25 | invert: rawCommand.readUInt8(8) === 1, 26 | 27 | mask: { 28 | enabled: rawCommand.readUInt8(9) === 1, 29 | top: rawCommand.readInt16BE(10), 30 | bottom: rawCommand.readInt16BE(12), 31 | left: rawCommand.readInt16BE(14), 32 | right: rawCommand.readInt16BE(16), 33 | }, 34 | } 35 | 36 | return new DownstreamKeyPropertiesCommand(downstreamKeyerId, properties) 37 | } 38 | 39 | public applyToState(state: AtemState): string { 40 | if (!state.info.capabilities || this.downstreamKeyerId >= state.info.capabilities.downstreamKeyers) { 41 | throw new InvalidIdError('DownstreamKeyer', this.downstreamKeyerId) 42 | } 43 | 44 | AtemStateUtil.getDownstreamKeyer(state, this.downstreamKeyerId).properties = this.properties 45 | return `video.downstreamKeyers.${this.downstreamKeyerId}` 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyRateCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class DownstreamKeyRateCommand extends BasicWritableCommand<{ rate: number }> { 4 | public static readonly rawName = 'CDsR' 5 | 6 | public readonly downstreamKeyerId: number 7 | 8 | constructor(downstreamKeyerId: number, rate: number) { 9 | super({ rate }) 10 | 11 | this.downstreamKeyerId = downstreamKeyerId 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.downstreamKeyerId, 0) 17 | buffer.writeUInt8(this.properties.rate, 1) 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeySourcesCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 3 | import { DownstreamKeyer } from '../../state/video/downstreamKeyers' 4 | 5 | export class DownstreamKeySourcesCommand extends DeserializedCommand { 6 | public static readonly rawName = 'DskB' 7 | 8 | public readonly downstreamKeyerId: number 9 | 10 | constructor(downstreamKeyerId: number, properties: DownstreamKeyer['sources']) { 11 | super(properties) 12 | 13 | this.downstreamKeyerId = downstreamKeyerId 14 | } 15 | 16 | public static deserialize(rawCommand: Buffer): DownstreamKeySourcesCommand { 17 | const downstreamKeyerId = rawCommand.readUInt8(0) 18 | const properties = { 19 | fillSource: rawCommand.readUInt16BE(2), 20 | cutSource: rawCommand.readUInt16BE(4), 21 | } 22 | 23 | return new DownstreamKeySourcesCommand(downstreamKeyerId, properties) 24 | } 25 | 26 | public applyToState(state: AtemState): string { 27 | if (!state.info.capabilities || this.downstreamKeyerId >= state.info.capabilities.downstreamKeyers) { 28 | throw new InvalidIdError('DownstreamKeyer', this.downstreamKeyerId) 29 | } 30 | 31 | AtemStateUtil.getDownstreamKeyer(state, this.downstreamKeyerId).sources = this.properties 32 | return `video.downstreamKeyers.${this.downstreamKeyerId}` 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/DownstreamKeyTieCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class DownstreamKeyTieCommand extends BasicWritableCommand<{ tie: boolean }> { 4 | public static readonly rawName = 'CDsT' 5 | 6 | public readonly downstreamKeyerId: number 7 | 8 | constructor(downstreamKeyerId: number, tie: boolean) { 9 | super({ tie }) 10 | 11 | this.downstreamKeyerId = downstreamKeyerId 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.downstreamKeyerId, 0) 17 | buffer.writeUInt8(this.properties.tie ? 1 : 0, 1) 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/DownstreamKey/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DownstreamKeyAutoCommand' 2 | export * from './DownstreamKeyCutSourceCommand' 3 | export * from './DownstreamKeyFillSourceCommand' 4 | export * from './DownstreamKeyGeneralCommand' 5 | export * from './DownstreamKeyMaskCommand' 6 | export * from './DownstreamKeyOnAirCommand' 7 | export * from './DownstreamKeyPropertiesCommand' 8 | export * from './DownstreamKeyRateCommand' 9 | export * from './DownstreamKeySourcesCommand' 10 | export * from './DownstreamKeyStateCommand' 11 | export * from './DownstreamKeyTieCommand' 12 | -------------------------------------------------------------------------------- /src/commands/Fairlight/AudioRouting/AudioRoutingSource.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand, WritableCommand } from '../../CommandBase' 2 | import { FairlightAudioRoutingSource } from '../../../state/fairlight' 3 | import { OmitReadonly } from '../../../lib/types' 4 | import { AtemState, InvalidIdError } from '../../../state' 5 | import * as Util from '../../../lib/atemUtil' 6 | import { AudioChannelPair } from '../../../enums' 7 | 8 | export class AudioRoutingSourceCommand extends WritableCommand> { 9 | public static MaskFlags = { 10 | name: 1 << 0, 11 | } 12 | 13 | public static readonly rawName = 'ARSC' 14 | 15 | public readonly id: number 16 | 17 | constructor(sourceId: number) { 18 | super() 19 | 20 | this.id = sourceId 21 | } 22 | 23 | public serialize(): Buffer { 24 | const buffer = Buffer.alloc(72) 25 | buffer.writeUInt8(this.flag, 0) 26 | buffer.writeUInt32BE(this.id, 4) 27 | 28 | buffer.write(this.properties.name ?? '', 8, 64) 29 | return buffer 30 | } 31 | } 32 | 33 | export class AudioRoutingSourceUpdateCommand extends DeserializedCommand { 34 | public static readonly rawName = 'ARSP' 35 | 36 | public readonly id: number 37 | 38 | constructor(sourceId: number, properties: FairlightAudioRoutingSource) { 39 | super(properties) 40 | 41 | this.id = sourceId 42 | } 43 | 44 | public static deserialize(rawCommand: Buffer): AudioRoutingSourceUpdateCommand { 45 | const sourceId = rawCommand.readUInt32BE(0) 46 | const properties = { 47 | audioSourceId: sourceId >> 16, 48 | audioChannelPair: (sourceId & 0xffff) as AudioChannelPair, 49 | 50 | externalPortType: rawCommand.readUInt16BE(4), 51 | internalPortType: rawCommand.readUInt16BE(6), 52 | 53 | name: Util.bufToNullTerminatedString(rawCommand, 8, 64), 54 | } 55 | 56 | return new AudioRoutingSourceUpdateCommand(sourceId, properties) 57 | } 58 | 59 | public applyToState(state: AtemState): string { 60 | if (!state.fairlight) { 61 | throw new InvalidIdError('Fairlight') 62 | } 63 | 64 | if (!state.fairlight.audioRouting) 65 | state.fairlight.audioRouting = { 66 | outputs: {}, 67 | sources: {}, 68 | } 69 | 70 | state.fairlight.audioRouting.sources[this.id] = this.properties 71 | return `fairlight.audioRouting.sources.${this.id}` 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/Fairlight/AudioRouting/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AudioRoutingOutput' 2 | export * from './AudioRoutingSource' 3 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerAnalogAudioCommand.ts_: -------------------------------------------------------------------------------- 1 | import { AtemState, InvalidIdError } from '../../state' 2 | // import { FairlightAudioInputAnalog } from '../../state/fairlight' 3 | import * as Util from '../../lib/atemUtil' 4 | import { DeserializedCommand, WritableCommand } from '../CommandBase' 5 | import { OmitReadonly } from '../../lib/types' 6 | 7 | /** 8 | * TODO - how does this not clash with the normal update command? 9 | */ 10 | 11 | export class FairlightMixerAnalogAudioCommand extends WritableCommand> { 12 | public static readonly rawName = 'CFAA' 13 | 14 | public readonly index: number 15 | 16 | constructor(index: number) { 17 | super() 18 | 19 | this.index = index 20 | } 21 | 22 | public serialize(): Buffer { 23 | const buffer = Buffer.alloc(4) 24 | buffer.writeUInt16BE(this.index, 0) 25 | 26 | buffer.writeUInt8(this.properties.inputLevel || 0, 2) 27 | 28 | return buffer 29 | } 30 | } 31 | 32 | export class FairlightMixerAnalogAudioUpdateCommand extends DeserializedCommand { 33 | public static readonly rawName = 'FAAI' 34 | 35 | public readonly index: number 36 | 37 | constructor(index: number, properties: FairlightAudioInputAnalog) { 38 | super(properties) 39 | 40 | this.index = index 41 | } 42 | 43 | public static deserialize(rawCommand: Buffer): FairlightMixerAnalogAudioUpdateCommand { 44 | const index = rawCommand.readUInt16BE(0) 45 | 46 | const properties = { 47 | supportedInputLevels: Util.getComponents(rawCommand.readUInt8(3)), 48 | inputLevel: rawCommand.readUInt8(4) 49 | } 50 | 51 | return new FairlightMixerAnalogAudioUpdateCommand(index, properties) 52 | } 53 | 54 | public applyToState(state: AtemState): string { 55 | if (!state.fairlight) { 56 | throw new InvalidIdError('Fairlight') 57 | } 58 | 59 | // TODO 60 | // state.fairlight.inputs[this.index] = { 61 | // sources: {}, 62 | // ...state.fairlight.inputs[this.index], 63 | // properties: this.properties 64 | // } 65 | return `fairlight.inputs.${this.index}.properties` 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMasterDynamicsResetCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | import { FairlightDynamicsResetProps } from './common' 3 | 4 | export class FairlightMixerMasterDynamicsResetCommand extends WritableCommand { 5 | public static readonly rawName = 'RMOD' 6 | 7 | public serialize(): Buffer { 8 | const buffer = Buffer.alloc(4) 9 | 10 | let val = 0 11 | if (this.properties.dynamics) { 12 | val |= 1 << 0 13 | } 14 | if (this.properties.expander) { 15 | val |= 1 << 1 16 | } 17 | if (this.properties.compressor) { 18 | val |= 1 << 2 19 | } 20 | if (this.properties.limiter) { 21 | val |= 1 << 3 22 | } 23 | 24 | buffer.writeUInt8(val, 1) 25 | return buffer 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMasterEqualizerResetCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | 3 | export class FairlightMixerMasterEqualizerResetCommand extends WritableCommand<{ equalizer: boolean; band: number }> { 4 | public static MaskFlags = { 5 | equalizer: 1 << 0, 6 | band: 1 << 1, 7 | } 8 | 9 | public static readonly rawName = 'RMOE' 10 | 11 | public serialize(): Buffer { 12 | const buffer = Buffer.alloc(4) 13 | 14 | buffer.writeUInt8(this.flag, 0) 15 | buffer.writeUInt8(this.properties.equalizer ? 1 : 0, 1) 16 | buffer.writeUInt8(this.properties.band || 0, 2) 17 | 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMasterLevelsCommand.ts: -------------------------------------------------------------------------------- 1 | import { FairlightAudioLevels } from '../../state/levels' 2 | import { DeserializedCommand } from '../CommandBase' 3 | 4 | export class FairlightMixerMasterLevelsUpdateCommand extends DeserializedCommand< 5 | Omit 6 | > { 7 | public static readonly rawName = 'FDLv' 8 | 9 | public static deserialize(rawCommand: Buffer): FairlightMixerMasterLevelsUpdateCommand { 10 | const properties = { 11 | inputLeftLevel: rawCommand.readInt16BE(0), 12 | inputRightLevel: rawCommand.readInt16BE(2), 13 | inputLeftPeak: rawCommand.readInt16BE(4), 14 | inputRightPeak: rawCommand.readInt16BE(6), 15 | 16 | compressorGainReduction: rawCommand.readInt16BE(8), 17 | limiterGainReduction: rawCommand.readInt16BE(10), 18 | 19 | outputLeftLevel: rawCommand.readInt16BE(12), 20 | outputRightLevel: rawCommand.readInt16BE(14), 21 | outputLeftPeak: rawCommand.readInt16BE(16), 22 | outputRightPeak: rawCommand.readInt16BE(18), 23 | 24 | leftLevel: rawCommand.readInt16BE(20), 25 | rightLevel: rawCommand.readInt16BE(22), 26 | leftPeak: rawCommand.readInt16BE(24), 27 | rightPeak: rawCommand.readInt16BE(26), 28 | } 29 | 30 | return new FairlightMixerMasterLevelsUpdateCommand(properties) 31 | } 32 | 33 | public applyToState(): string[] { 34 | // Not stored in the state 35 | return [] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMasterLimiterCommand.ts: -------------------------------------------------------------------------------- 1 | import { FairlightAudioLimiterState } from '../../state/fairlight' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { DeserializedCommand, WritableCommand } from '../CommandBase' 4 | import { OmitReadonly } from '../../lib/types' 5 | 6 | export class FairlightMixerMasterLimiterCommand extends WritableCommand> { 7 | public static MaskFlags = { 8 | limiterEnabled: 1 << 0, 9 | threshold: 1 << 1, 10 | attack: 1 << 2, 11 | hold: 1 << 3, 12 | release: 1 << 4, 13 | } 14 | 15 | public static readonly rawName = 'CMLP' 16 | 17 | public serialize(): Buffer { 18 | const buffer = Buffer.alloc(20) 19 | buffer.writeUInt8(this.flag, 0) 20 | 21 | buffer.writeUInt8(this.properties.limiterEnabled ? 1 : 0, 1) 22 | buffer.writeInt32BE(this.properties.threshold || 0, 4) 23 | buffer.writeInt32BE(this.properties.attack || 0, 8) 24 | buffer.writeInt32BE(this.properties.hold || 0, 12) 25 | buffer.writeInt32BE(this.properties.release || 0, 16) 26 | 27 | return buffer 28 | } 29 | } 30 | 31 | export class FairlightMixerMasterLimiterUpdateCommand extends DeserializedCommand { 32 | public static readonly rawName = 'AMLP' 33 | 34 | public static deserialize(rawCommand: Buffer): FairlightMixerMasterLimiterUpdateCommand { 35 | const properties = { 36 | limiterEnabled: rawCommand.readUInt8(0) > 0, 37 | threshold: rawCommand.readInt32BE(4), 38 | attack: rawCommand.readInt32BE(8), 39 | hold: rawCommand.readInt32BE(12), 40 | release: rawCommand.readInt32BE(16), 41 | } 42 | 43 | return new FairlightMixerMasterLimiterUpdateCommand(properties) 44 | } 45 | 46 | public applyToState(state: AtemState): string { 47 | if (!state.fairlight) { 48 | throw new InvalidIdError('Fairlight') 49 | } 50 | 51 | if (!state.fairlight.master) { 52 | throw new InvalidIdError('Fairlight.Master') 53 | } 54 | 55 | if (!state.fairlight.master.dynamics) { 56 | state.fairlight.master.dynamics = {} 57 | } 58 | 59 | state.fairlight.master.dynamics.limiter = this.properties 60 | 61 | return `fairlight.master.dynamics.limiter` 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMasterPropertiesCommand.ts: -------------------------------------------------------------------------------- 1 | import { AtemState, InvalidIdError } from '../../state' 2 | import { DeserializedCommand, WritableCommand } from '../CommandBase' 3 | import { OmitReadonly } from '../../lib/types' 4 | 5 | export class FairlightMixerMasterPropertiesCommand extends WritableCommand< 6 | OmitReadonly<{ audioFollowVideo: boolean }> 7 | > { 8 | public static MaskFlags = { 9 | audioFollowVideo: 1 << 0, 10 | } 11 | 12 | public static readonly rawName = 'CMPP' 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.flag, 0) 17 | 18 | buffer.writeUInt8(this.properties.audioFollowVideo ? 1 : 0, 1) 19 | return buffer 20 | } 21 | } 22 | 23 | export class FairlightMixerMasterPropertiesUpdateCommand extends DeserializedCommand<{ audioFollowVideo: boolean }> { 24 | public static readonly rawName = 'FMPP' 25 | 26 | public static deserialize(rawCommand: Buffer): FairlightMixerMasterPropertiesUpdateCommand { 27 | const audioFollowVideo = rawCommand.readUInt8(0) > 0 28 | 29 | return new FairlightMixerMasterPropertiesUpdateCommand({ audioFollowVideo }) 30 | } 31 | 32 | public applyToState(state: AtemState): string { 33 | if (!state.fairlight) { 34 | throw new InvalidIdError('Fairlight') 35 | } 36 | 37 | state.fairlight.audioFollowVideoCrossfadeTransitionEnabled = this.properties.audioFollowVideo 38 | 39 | return `fairlight.audioFollowVideoCrossfadeTransitionEnabled` 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMonitorCommand.ts: -------------------------------------------------------------------------------- 1 | import { FairlightAudioMonitorChannel } from '../../state/fairlight' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { DeserializedCommand, WritableCommand } from '../CommandBase' 4 | import { OmitReadonly } from '../../lib/types' 5 | 6 | export class FairlightMixerMonitorCommand extends WritableCommand> { 7 | public static MaskFlags = { 8 | gain: 1 << 0, 9 | inputMasterGain: 1 << 1, 10 | inputMasterMuted: 1 << 2, 11 | inputTalkbackGain: 1 << 3, 12 | inputTalkbackMuted: 1 << 4, 13 | inputSidetoneGain: 1 << 7, 14 | } 15 | 16 | public static readonly rawName = 'CFMH' 17 | 18 | public serialize(): Buffer { 19 | const buffer = Buffer.alloc(36) 20 | buffer.writeUInt8(this.flag, 0) 21 | 22 | buffer.writeInt32BE(this.properties.gain || 0, 4) 23 | buffer.writeInt32BE(this.properties.inputMasterGain || 0, 8) 24 | buffer.writeUInt8(this.properties.inputMasterMuted ? 0 : 1, 12) 25 | buffer.writeInt32BE(this.properties.inputTalkbackGain || 0, 16) 26 | buffer.writeUInt8(this.properties.inputTalkbackMuted ? 0 : 1, 20) 27 | buffer.writeInt32BE(this.properties.inputSidetoneGain || 0, 32) 28 | return buffer 29 | } 30 | } 31 | 32 | export class FairlightMixerMonitorUpdateCommand extends DeserializedCommand { 33 | public static readonly rawName = 'FMHP' 34 | 35 | public static deserialize(rawCommand: Buffer): FairlightMixerMonitorUpdateCommand { 36 | const properties = { 37 | gain: rawCommand.readInt32BE(0), 38 | inputMasterGain: rawCommand.readInt32BE(4), 39 | inputMasterMuted: rawCommand.readUInt8(8) === 0, 40 | inputTalkbackGain: rawCommand.readInt32BE(12), 41 | inputTalkbackMuted: rawCommand.readUInt8(16) === 0, 42 | inputSidetoneGain: rawCommand.readInt32BE(28), 43 | } 44 | 45 | return new FairlightMixerMonitorUpdateCommand(properties) 46 | } 47 | 48 | public applyToState(state: AtemState): string { 49 | if (!state.fairlight) { 50 | throw new InvalidIdError('Fairlight') 51 | } 52 | 53 | state.fairlight.monitor = { 54 | ...this.properties, 55 | } 56 | 57 | return `fairlight.monitor` 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerMonitorSoloCommand.ts: -------------------------------------------------------------------------------- 1 | import { FairlightAudioMonitorSolo } from '../../state/fairlight' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { DeserializedCommand, WritableCommand } from '../CommandBase' 4 | import { OmitReadonly } from '../../lib/types' 5 | 6 | export class FairlightMixerMonitorSoloCommand extends WritableCommand> { 7 | public static MaskFlags = { 8 | solo: 1 << 0, 9 | index: 1 << 1, 10 | source: 1 << 1, // Intentional duplicate 11 | } 12 | 13 | public static readonly rawName = 'CFMS' 14 | 15 | public serialize(): Buffer { 16 | const buffer = Buffer.alloc(24) 17 | buffer.writeUInt8(this.flag, 0) 18 | 19 | buffer.writeUInt8(this.properties.solo ? 1 : 0, 1) 20 | buffer.writeUInt16BE(this.properties.index ?? 0, 8) 21 | if (this.properties.source) buffer.writeBigInt64BE(BigInt(this.properties.source), 16) 22 | return buffer 23 | } 24 | } 25 | 26 | export class FairlightMixerMonitorSoloUpdateCommand extends DeserializedCommand { 27 | public static readonly rawName = 'FAMS' 28 | 29 | public static deserialize(rawCommand: Buffer): FairlightMixerMonitorSoloUpdateCommand { 30 | const properties: FairlightAudioMonitorSolo = { 31 | solo: rawCommand.readUint8(0) === 1, 32 | index: rawCommand.readUInt16BE(8), 33 | source: rawCommand.readBigInt64BE(16).toString(), 34 | } 35 | 36 | return new FairlightMixerMonitorSoloUpdateCommand(properties) 37 | } 38 | 39 | public applyToState(state: AtemState): string { 40 | if (!state.fairlight) { 41 | throw new InvalidIdError('Fairlight') 42 | } 43 | 44 | state.fairlight.solo = { ...this.properties } 45 | 46 | return `fairlight.solo` 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerResetPeakLevelsCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class FairlightMixerResetPeakLevelsCommand extends BasicWritableCommand<{ all: boolean; master: boolean }> { 4 | public static readonly rawName = 'RFLP' 5 | 6 | public serialize(): Buffer { 7 | const buffer = Buffer.alloc(4) 8 | 9 | let val = 0 10 | if (this.properties.all) { 11 | val |= 1 << 0 12 | 13 | // some magic number that is needed for this to work 14 | buffer.writeUInt8(0x01, 1) 15 | } 16 | if (this.properties.master) { 17 | val |= 1 << 1 18 | 19 | // some magic number that is needed for this to work 20 | buffer.writeUInt8(0x04, 3) 21 | } 22 | 23 | buffer.writeUInt8(val, 0) 24 | return buffer 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerSendLevelsCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class FairlightMixerSendLevelsCommand extends BasicWritableCommand<{ sendLevels: boolean }> { 4 | public static readonly rawName = 'SFLN' 5 | 6 | constructor(sendLevels: boolean) { 7 | super({ sendLevels }) 8 | } 9 | 10 | public serialize(): Buffer { 11 | const buffer = Buffer.alloc(4) 12 | buffer.writeUInt8(this.properties.sendLevels ? 1 : 0, 0) 13 | return buffer 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerSourceDynamicsResetCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | import { FairlightDynamicsResetProps } from './common' 3 | 4 | export class FairlightMixerSourceDynamicsResetCommand extends WritableCommand { 5 | public static readonly rawName = 'RICD' 6 | 7 | public readonly index: number 8 | public readonly source: bigint 9 | 10 | constructor(index: number, source: bigint) { 11 | super() 12 | 13 | this.index = index 14 | this.source = source 15 | } 16 | 17 | public serialize(): Buffer { 18 | const buffer = Buffer.alloc(20) 19 | buffer.writeUInt16BE(this.index, 0) 20 | buffer.writeBigInt64BE(this.source, 8) 21 | 22 | let val = 0 23 | if (this.properties.dynamics) { 24 | val |= 1 << 0 25 | } 26 | if (this.properties.expander) { 27 | val |= 1 << 1 28 | } 29 | if (this.properties.compressor) { 30 | val |= 1 << 2 31 | } 32 | if (this.properties.limiter) { 33 | val |= 1 << 3 34 | } 35 | 36 | buffer.writeUInt8(val, 17) 37 | return buffer 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerSourceEqualizerResetCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../CommandBase' 2 | 3 | export class FairlightMixerSourceEqualizerResetCommand extends WritableCommand<{ equalizer: boolean; band: number }> { 4 | public static MaskFlags = { 5 | equalizer: 1 << 0, 6 | band: 1 << 1, 7 | } 8 | 9 | public static readonly rawName = 'RICE' 10 | 11 | public readonly index: number 12 | public readonly source: bigint 13 | 14 | constructor(index: number, source: bigint) { 15 | super() 16 | 17 | this.index = index 18 | this.source = source 19 | } 20 | 21 | public serialize(): Buffer { 22 | const buffer = Buffer.alloc(20) 23 | buffer.writeUInt8(this.flag, 0) 24 | buffer.writeUInt16BE(this.index, 2) 25 | buffer.writeBigInt64BE(this.source, 8) 26 | 27 | buffer.writeUInt8(this.properties.equalizer ? 1 : 0, 16) 28 | buffer.writeUInt8(this.properties.band || 0, 17) 29 | 30 | return buffer 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerSourceLevelsCommand.ts: -------------------------------------------------------------------------------- 1 | import { FairlightAudioLevels } from '../../state/levels' 2 | import { DeserializedCommand } from '../CommandBase' 3 | 4 | export class FairlightMixerSourceLevelsUpdateCommand extends DeserializedCommand { 5 | public static readonly rawName = 'FMLv' 6 | 7 | public readonly index: number 8 | public readonly source: bigint 9 | 10 | constructor(index: number, source: bigint, props: FairlightMixerSourceLevelsUpdateCommand['properties']) { 11 | super(props) 12 | 13 | this.index = index 14 | this.source = source 15 | } 16 | 17 | public static deserialize(rawCommand: Buffer): FairlightMixerSourceLevelsUpdateCommand { 18 | const source = rawCommand.readBigInt64BE(0) 19 | const index = rawCommand.readUInt16BE(8) 20 | const properties = { 21 | inputLeftLevel: rawCommand.readInt16BE(10), 22 | inputRightLevel: rawCommand.readInt16BE(12), 23 | inputLeftPeak: rawCommand.readInt16BE(14), 24 | inputRightPeak: rawCommand.readInt16BE(16), 25 | 26 | expanderGainReduction: rawCommand.readInt16BE(18), 27 | compressorGainReduction: rawCommand.readInt16BE(20), 28 | limiterGainReduction: rawCommand.readInt16BE(22), 29 | 30 | outputLeftLevel: rawCommand.readInt16BE(24), 31 | outputRightLevel: rawCommand.readInt16BE(26), 32 | outputLeftPeak: rawCommand.readInt16BE(28), 33 | outputRightPeak: rawCommand.readInt16BE(30), 34 | 35 | leftLevel: rawCommand.readInt16BE(32), 36 | rightLevel: rawCommand.readInt16BE(34), 37 | leftPeak: rawCommand.readInt16BE(36), 38 | rightPeak: rawCommand.readInt16BE(38), 39 | } 40 | 41 | return new FairlightMixerSourceLevelsUpdateCommand(index, source, properties) 42 | } 43 | 44 | public applyToState(): string[] { 45 | // Not stored in the state 46 | return [] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/Fairlight/FairlightMixerSourceResetPeakLevelsCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class FairlightMixerSourceResetPeakLevelsCommand extends BasicWritableCommand<{ 4 | output: boolean 5 | dynamicsInput: boolean 6 | dynamicsOutput: boolean 7 | }> { 8 | public static readonly rawName = 'RFIP' 9 | 10 | public readonly index: number 11 | public readonly source: bigint 12 | 13 | constructor(index: number, source: bigint, properties: FairlightMixerSourceResetPeakLevelsCommand['properties']) { 14 | super(properties) 15 | 16 | this.index = index 17 | this.source = source 18 | } 19 | 20 | public serialize(): Buffer { 21 | const buffer = Buffer.alloc(20) 22 | buffer.writeUInt16BE(this.index, 0) 23 | buffer.writeBigInt64BE(this.source, 8) 24 | 25 | let val = 0 26 | if (this.properties.dynamicsInput) { 27 | val |= 1 << 0 28 | } 29 | if (this.properties.dynamicsOutput) { 30 | val |= 1 << 1 31 | } 32 | if (this.properties.output) { 33 | val |= 1 << 2 34 | } 35 | 36 | buffer.writeUInt8(val, 17) 37 | return buffer 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/Fairlight/common.ts: -------------------------------------------------------------------------------- 1 | export interface FairlightDynamicsResetProps { 2 | dynamics: boolean 3 | expander: boolean 4 | compressor: boolean 5 | limiter: boolean 6 | } 7 | -------------------------------------------------------------------------------- /src/commands/Fairlight/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AudioRouting' 2 | 3 | // export * from './FairlightMixerAnalogAudioCommand' 4 | export * from './FairlightMixerInputCommand' 5 | export * from './FairlightMixerMasterCommand' 6 | export * from './FairlightMixerMasterCompressorCommand' 7 | export * from './FairlightMixerMasterDynamicsResetCommand' 8 | export * from './FairlightMixerMasterEqualizerBandCommand' 9 | export * from './FairlightMixerMasterEqualizerResetCommand' 10 | export * from './FairlightMixerMasterLevelsCommand' 11 | export * from './FairlightMixerMasterLimiterCommand' 12 | export * from './FairlightMixerMasterPropertiesCommand' 13 | export * from './FairlightMixerMonitorCommand' 14 | export * from './FairlightMixerMonitorSoloCommand' 15 | export * from './FairlightMixerResetPeakLevelsCommand' 16 | export * from './FairlightMixerSendLevelsCommand' 17 | export * from './FairlightMixerSourceCommand' 18 | export * from './FairlightMixerSourceCompressorCommand' 19 | export * from './FairlightMixerSourceDynamicsResetCommand' 20 | export * from './FairlightMixerSourceEqualizerBandCommand' 21 | export * from './FairlightMixerSourceEqualizerResetCommand' 22 | export * from './FairlightMixerSourceExpanderCommand' 23 | export * from './FairlightMixerSourceLevelsCommand' 24 | export * from './FairlightMixerSourceLimiterCommand' 25 | export * from './FairlightMixerSourceResetPeakLevelsCommand' 26 | -------------------------------------------------------------------------------- /src/commands/InitCompleteCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from './CommandBase' 2 | 3 | export class InitCompleteCommand extends DeserializedCommand { 4 | public static readonly rawName = 'InCm' 5 | 6 | constructor() { 7 | super(null) 8 | } 9 | 10 | public static deserialize(): InitCompleteCommand { 11 | return new InitCompleteCommand() 12 | } 13 | 14 | public applyToState(): string { 15 | return `info` 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/Inputs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InputPropertiesCommand' 2 | -------------------------------------------------------------------------------- /src/commands/Macro/MacroActionCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | import { MacroAction } from '../../enums' 3 | 4 | export class MacroActionCommand extends BasicWritableCommand<{ action: MacroAction }> { 5 | public static readonly rawName = 'MAct' 6 | 7 | public readonly index: number 8 | 9 | constructor(index: number, action: MacroAction) { 10 | super({ action }) 11 | 12 | this.index = index 13 | } 14 | 15 | public serialize(): Buffer { 16 | const buffer = Buffer.alloc(4) 17 | buffer.writeUInt8(this.properties.action, 2) 18 | switch (this.properties.action) { 19 | case MacroAction.Run: 20 | case MacroAction.Delete: 21 | buffer.writeUInt16BE(this.index, 0) 22 | break 23 | case MacroAction.Stop: 24 | case MacroAction.StopRecord: 25 | case MacroAction.InsertUserWait: 26 | case MacroAction.Continue: 27 | buffer.writeUInt16BE(0xffff, 0) 28 | break 29 | default: 30 | break 31 | } 32 | return buffer 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/Macro/MacroAddTimedPauseCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class MacroAddTimedPauseCommand extends BasicWritableCommand<{ frames: number }> { 4 | public static readonly rawName = 'MSlp' 5 | 6 | constructor(frames: number) { 7 | super({ frames }) 8 | } 9 | 10 | public serialize(): Buffer { 11 | const buffer = Buffer.alloc(4) 12 | buffer.writeUInt16BE(this.properties.frames, 2) 13 | return buffer 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/Macro/MacroRecordCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | import { Util } from '../..' 3 | 4 | export class MacroRecordCommand extends BasicWritableCommand<{ name: string; description: string }> { 5 | public static readonly rawName = 'MSRc' 6 | public readonly index: number 7 | 8 | constructor(index: number, name: string, description: string) { 9 | super({ name, description }) 10 | 11 | this.index = index 12 | } 13 | 14 | public serialize(): Buffer { 15 | const name = this.properties.name || '' 16 | const description = this.properties.description || '' 17 | 18 | const buffer = Buffer.alloc(Util.padToMultiple(8 + name.length + description.length, 4)) 19 | buffer.writeUInt16BE(this.index, 0) 20 | buffer.writeUInt16BE(name.length, 2) 21 | buffer.writeUInt16BE(description.length, 4) 22 | buffer.write(name, 6, 'utf8') 23 | buffer.write(description, 6 + name.length, 'utf8') 24 | return buffer 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/Macro/MacroRecordingStatusCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { MacroRecorderState } from '../../state/macro' 4 | 5 | export class MacroRecordingStatusCommand extends DeserializedCommand { 6 | public static readonly rawName = 'MRcS' 7 | 8 | public static deserialize(rawCommand: Buffer): MacroRecordingStatusCommand { 9 | const properties = { 10 | isRecording: rawCommand.readUInt8(0) != 0, 11 | macroIndex: rawCommand.readUInt16BE(2), 12 | } 13 | 14 | return new MacroRecordingStatusCommand(properties) 15 | } 16 | 17 | public applyToState(state: AtemState): string { 18 | state.macro.macroRecorder = { 19 | ...state.macro.macroRecorder, 20 | ...this.properties, 21 | } 22 | return `macro.macroRecorder` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/Macro/MacroRunStatusCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand, WritableCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { MacroPlayerState } from '../../state/macro' 4 | 5 | export class MacroRunStatusUpdateCommand extends DeserializedCommand { 6 | public static readonly rawName = 'MRPr' 7 | 8 | public static deserialize(rawCommand: Buffer): MacroRunStatusUpdateCommand { 9 | const properties: MacroPlayerState = { 10 | isRunning: Boolean(rawCommand.readUInt8(0) & (1 << 0)), 11 | isWaiting: Boolean(rawCommand.readUInt8(0) & (1 << 1)), 12 | loop: rawCommand.readUInt8(1) != 0, 13 | macroIndex: rawCommand.readUInt16BE(2), 14 | } 15 | 16 | return new MacroRunStatusUpdateCommand(properties) 17 | } 18 | 19 | public applyToState(state: AtemState): string { 20 | state.macro.macroPlayer = this.properties 21 | return `macro.macroPlayer` 22 | } 23 | } 24 | 25 | export class MacroRunStatusCommand extends WritableCommand<{ loop: boolean }> { 26 | public static MaskFlags = { 27 | loop: 1 << 0, 28 | } 29 | 30 | public static readonly rawName = 'MRCP' 31 | 32 | public serialize(): Buffer { 33 | const buffer = Buffer.alloc(4) 34 | buffer.writeUInt8(this.flag, 0) 35 | buffer.writeUInt8(this.properties.loop ? 1 : 0, 1) 36 | return buffer 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/Macro/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MacroActionCommand' 2 | export * from './MacroAddTimedPauseCommand' 3 | export * from './MacroPropertiesCommand' 4 | export * from './MacroRecordCommand' 5 | export * from './MacroRecordingStatusCommand' 6 | export * from './MacroRunStatusCommand' 7 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPlayerSourceCommand.ts: -------------------------------------------------------------------------------- 1 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 2 | import { MediaPlayerSource } from '../../state/media' 3 | import { WritableCommand, DeserializedCommand } from '../CommandBase' 4 | 5 | export class MediaPlayerSourceCommand extends WritableCommand { 6 | public static MaskFlags = { 7 | sourceType: 1 << 0, 8 | stillIndex: 1 << 1, 9 | clipIndex: 1 << 2, 10 | } 11 | 12 | public static readonly rawName = 'MPSS' 13 | 14 | public readonly mediaPlayerId: number 15 | 16 | constructor(mediaPlayerId: number) { 17 | super() 18 | 19 | this.mediaPlayerId = mediaPlayerId 20 | } 21 | 22 | public serialize(): Buffer { 23 | const buffer = Buffer.alloc(8) 24 | buffer.writeUInt8(this.flag, 0) 25 | buffer.writeUInt8(this.mediaPlayerId, 1) 26 | buffer.writeUInt8(this.properties.sourceType || 0, 2) 27 | buffer.writeUInt8(this.properties.stillIndex || 0, 3) 28 | buffer.writeUInt8(this.properties.clipIndex || 0, 4) 29 | return buffer 30 | } 31 | } 32 | 33 | export class MediaPlayerSourceUpdateCommand extends DeserializedCommand { 34 | public static readonly rawName = 'MPCE' 35 | 36 | public readonly mediaPlayerId: number 37 | 38 | constructor(mediaPlayerId: number, properties: MediaPlayerSource) { 39 | super(properties) 40 | 41 | this.mediaPlayerId = mediaPlayerId 42 | } 43 | 44 | public static deserialize(rawCommand: Buffer): MediaPlayerSourceUpdateCommand { 45 | const mediaPlayerId = rawCommand.readUInt8(0) 46 | const properties = { 47 | sourceType: rawCommand.readUInt8(1), 48 | stillIndex: rawCommand.readUInt8(2), 49 | clipIndex: rawCommand.readUInt8(3), 50 | } 51 | 52 | return new MediaPlayerSourceUpdateCommand(mediaPlayerId, properties) 53 | } 54 | 55 | public applyToState(state: AtemState): string { 56 | if (!state.info.capabilities || this.mediaPlayerId >= state.info.capabilities.mediaPlayers) { 57 | throw new InvalidIdError('MediaPlayer', this.mediaPlayerId) 58 | } 59 | 60 | state.media.players[this.mediaPlayerId] = { 61 | ...AtemStateUtil.getMediaPlayer(state, this.mediaPlayerId), 62 | ...this.properties, 63 | } 64 | return `media.players.${this.mediaPlayerId}` 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPoolCaptureStillCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class MediaPoolCaptureStillCommand extends BasicWritableCommand> { 4 | public static readonly rawName = 'Capt' 5 | 6 | constructor() { 7 | super({}) 8 | } 9 | 10 | public serialize(): Buffer { 11 | return Buffer.alloc(0) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPoolClearClipCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class MediaPoolClearClipCommand extends BasicWritableCommand<{ index: number }> { 4 | public static readonly rawName = 'CMPC' 5 | 6 | constructor(index: number) { 7 | super({ index }) 8 | } 9 | 10 | public serialize(): Buffer { 11 | const buffer = Buffer.alloc(4) 12 | buffer.writeUInt8(this.properties.index, 0) 13 | return buffer 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPoolClearStillCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class MediaPoolClearStillCommand extends BasicWritableCommand<{ index: number }> { 4 | public static readonly rawName = 'CSTL' 5 | 6 | constructor(index: number) { 7 | super({ index }) 8 | } 9 | 10 | public serialize(): Buffer { 11 | const buffer = Buffer.alloc(4) 12 | buffer.writeUInt8(this.properties.index, 0) 13 | return buffer 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPoolClipDescription.ts: -------------------------------------------------------------------------------- 1 | import { AtemState, AtemStateUtil } from '../../state' 2 | import { ClipBank } from '../../state/media' 3 | import { DeserializedCommand } from '../CommandBase' 4 | import * as Util from '../../lib/atemUtil' 5 | 6 | export class MediaPoolClipDescriptionCommand extends DeserializedCommand> { 7 | public static readonly rawName = 'MPCS' 8 | 9 | public readonly clipId: number 10 | 11 | constructor(mediaPool: number, properties: Omit) { 12 | super(properties) 13 | 14 | this.clipId = mediaPool 15 | } 16 | 17 | public static deserialize(rawCommand: Buffer): MediaPoolClipDescriptionCommand { 18 | const mediaPool = rawCommand.readUInt8(0) 19 | const properties = { 20 | isUsed: rawCommand.readUInt8(1) === 1, 21 | name: Util.bufToNullTerminatedString(rawCommand, 2, 64), 22 | frameCount: rawCommand.readUInt16BE(66), 23 | } 24 | 25 | return new MediaPoolClipDescriptionCommand(mediaPool, properties) 26 | } 27 | 28 | public applyToState(state: AtemState): string { 29 | // TODO - validate ids 30 | 31 | state.media.clipPool[this.clipId] = { 32 | ...this.properties, 33 | frames: AtemStateUtil.getClip(state, this.clipId).frames, // TODO - lengthen/shorten array of frames? 34 | } 35 | return `media.clipPool.${this.clipId}` 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPoolFrameDescription.ts: -------------------------------------------------------------------------------- 1 | import { AtemState, AtemStateUtil } from '../../state' 2 | import { StillFrame } from '../../state/media' 3 | import { DeserializedCommand } from '../CommandBase' 4 | import * as Util from '../../lib/atemUtil' 5 | 6 | export class MediaPoolFrameDescriptionCommand extends DeserializedCommand { 7 | public static readonly rawName = 'MPfe' 8 | 9 | public readonly mediaPool: number 10 | public readonly frameIndex: number 11 | 12 | constructor(mediaPool: number, frameIndex: number, properties: StillFrame) { 13 | super(properties) 14 | 15 | this.mediaPool = mediaPool 16 | this.frameIndex = frameIndex 17 | } 18 | 19 | public static deserialize(rawCommand: Buffer): MediaPoolFrameDescriptionCommand { 20 | const mediaPool = rawCommand.readUInt8(0) 21 | const frameIndex = rawCommand.readUInt16BE(2) 22 | const properties = { 23 | isUsed: rawCommand.readUInt8(4) === 1, 24 | hash: Util.bufToBase64String(rawCommand, 5, 16), 25 | fileName: Util.bufToNullTerminatedString(rawCommand, 24, rawCommand.readUInt8(23)), 26 | } 27 | 28 | return new MediaPoolFrameDescriptionCommand(mediaPool, frameIndex, properties) 29 | } 30 | 31 | public applyToState(state: AtemState): string | string[] { 32 | // TODO - validate ids 33 | 34 | if (this.mediaPool === 0) { 35 | // This is a still 36 | state.media.stillPool[this.frameIndex] = this.properties 37 | return `media.stillPool.${this.frameIndex}` 38 | } else if (this.mediaPool < 3) { 39 | const clipId = this.mediaPool - 1 40 | // This is a clip 41 | AtemStateUtil.getClip(state, clipId).frames[this.frameIndex] = this.properties 42 | return `media.clipPool.${clipId}.frames.${this.frameIndex}` 43 | } 44 | return [] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/Media/MediaPoolSetClipCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export interface MediaPoolSetClipProps { 4 | index: number 5 | name: string 6 | frames: number 7 | } 8 | 9 | export class MediaPoolSetClipCommand extends BasicWritableCommand { 10 | public static readonly rawName = 'SMPC' 11 | 12 | public serialize(): Buffer { 13 | const buffer = Buffer.alloc(68) 14 | buffer.writeUInt8(3, 0) 15 | buffer.writeUInt8(this.properties.index, 1) 16 | buffer.write(this.properties.name, 2, 44, 'utf8') 17 | buffer.writeUInt16BE(this.properties.frames, 66) 18 | return buffer 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/Media/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MediaPlayerSourceCommand' 2 | export * from './MediaPlayerStatusCommand' 3 | export * from './MediaPoolCaptureStillCommand' 4 | export * from './MediaPoolClearClipCommand' 5 | export * from './MediaPoolClearStillCommand' 6 | export * from './MediaPoolClipDescription' 7 | export * from './MediaPoolFrameDescription' 8 | export * from './MediaPoolSetClipCommand' 9 | -------------------------------------------------------------------------------- /src/commands/MixEffects/AutoTransitionCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class AutoTransitionCommand extends BasicWritableCommand { 4 | public static readonly rawName = 'DAut' 5 | 6 | public readonly mixEffect: number 7 | 8 | constructor(mixEffect: number) { 9 | super(null) 10 | 11 | this.mixEffect = mixEffect 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.mixEffect, 0) 17 | return buffer 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/MixEffects/CutCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../CommandBase' 2 | 3 | export class CutCommand extends BasicWritableCommand { 4 | public static readonly rawName = 'DCut' 5 | 6 | public readonly mixEffect: number 7 | 8 | constructor(mixEffect: number) { 9 | super(null) 10 | 11 | this.mixEffect = mixEffect 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.mixEffect, 0) 17 | return buffer 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/MixEffects/FadeToBlack/FadeToBlackAutoCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../../CommandBase' 2 | 3 | export class FadeToBlackAutoCommand extends BasicWritableCommand { 4 | public static readonly rawName = 'FtbA' 5 | 6 | public readonly mixEffect: number 7 | 8 | constructor(mixEffect: number) { 9 | super(null) 10 | 11 | this.mixEffect = mixEffect 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.mixEffect, 0) 17 | return buffer 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/MixEffects/FadeToBlack/FadeToBlackRateCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | 4 | export class FadeToBlackRateCommand extends BasicWritableCommand<{ rate: number }> { 5 | public static readonly rawName = 'FtbC' 6 | 7 | public readonly mixEffect: number 8 | 9 | constructor(mixEffect: number, rate: number) { 10 | super({ rate }) 11 | 12 | this.mixEffect = mixEffect 13 | } 14 | 15 | public serialize(): Buffer { 16 | const buffer = Buffer.alloc(4) 17 | buffer.writeUInt8(1, 0) 18 | buffer.writeUInt8(this.mixEffect, 1) 19 | buffer.writeUInt8(this.properties.rate, 2) 20 | return buffer 21 | } 22 | } 23 | 24 | export class FadeToBlackRateUpdateCommand extends DeserializedCommand<{ rate: number }> { 25 | public static readonly rawName = 'FtbP' 26 | 27 | public readonly mixEffect: number 28 | 29 | constructor(mixEffect: number, rate: number) { 30 | super({ rate }) 31 | 32 | this.mixEffect = mixEffect 33 | } 34 | 35 | public static deserialize(rawCommand: Buffer): FadeToBlackRateUpdateCommand { 36 | const mixEffect = rawCommand.readUInt8(0) 37 | const rate = rawCommand.readUInt8(1) 38 | 39 | return new FadeToBlackRateUpdateCommand(mixEffect, rate) 40 | } 41 | 42 | public applyToState(state: AtemState): string { 43 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 44 | throw new InvalidIdError('MixEffect', this.mixEffect) 45 | } 46 | 47 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 48 | mixEffect.fadeToBlack = { 49 | isFullyBlack: false, 50 | inTransition: false, 51 | remainingFrames: 0, 52 | ...mixEffect.fadeToBlack, 53 | rate: this.properties.rate, 54 | } 55 | return `video.mixEffects.${this.mixEffect}.fadeToBlack` 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/MixEffects/FadeToBlack/FadeToBlackStateCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | 4 | export interface FadeToBlackProps { 5 | isFullyBlack: boolean 6 | inTransition: boolean 7 | remainingFrames: number 8 | } 9 | 10 | export class FadeToBlackStateCommand extends DeserializedCommand { 11 | public static readonly rawName = 'FtbS' 12 | 13 | public readonly mixEffect: number 14 | 15 | constructor(mixEffect: number, properties: FadeToBlackProps) { 16 | super(properties) 17 | 18 | this.mixEffect = mixEffect 19 | } 20 | 21 | public static deserialize(rawCommand: Buffer): FadeToBlackStateCommand { 22 | const mixEffect = rawCommand.readUInt8(0) 23 | const properties = { 24 | isFullyBlack: rawCommand.readUInt8(1) === 1, 25 | inTransition: rawCommand.readUInt8(2) === 1, 26 | remainingFrames: rawCommand.readUInt8(3), 27 | } 28 | 29 | return new FadeToBlackStateCommand(mixEffect, properties) 30 | } 31 | 32 | public applyToState(state: AtemState): string { 33 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 34 | throw new InvalidIdError('MixEffect', this.mixEffect) 35 | } 36 | 37 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 38 | mixEffect.fadeToBlack = { 39 | rate: 0, 40 | ...mixEffect.fadeToBlack, 41 | ...this.properties, 42 | } 43 | return `video.mixEffects.${this.mixEffect}.fadeToBlack` 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/MixEffects/FadeToBlack/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FadeToBlackAutoCommand' 2 | export * from './FadeToBlackRateCommand' 3 | export * from './FadeToBlackStateCommand' 4 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyAdvancedChromaSampleResetCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../../CommandBase' 2 | 3 | export interface AdvancedChromaSampleResetProps { 4 | keyAdjustments?: boolean 5 | chromaCorrection?: boolean 6 | colorAdjustments?: boolean 7 | } 8 | 9 | export class MixEffectKeyAdvancedChromaSampleResetCommand extends BasicWritableCommand { 10 | public static readonly rawName = 'RACK' 11 | 12 | public readonly mixEffect: number 13 | public readonly upstreamKeyerId: number 14 | 15 | constructor(mixEffect: number, upstreamKeyerId: number, props: AdvancedChromaSampleResetProps) { 16 | super(props) 17 | 18 | this.mixEffect = mixEffect 19 | this.upstreamKeyerId = upstreamKeyerId 20 | } 21 | 22 | public serialize(): Buffer { 23 | const buffer = Buffer.alloc(4) 24 | buffer.writeUInt8(this.mixEffect, 0) 25 | buffer.writeUInt8(this.upstreamKeyerId, 1) 26 | 27 | let val = 0 28 | if (this.properties.keyAdjustments) { 29 | val |= 1 << 0 30 | } 31 | if (this.properties.chromaCorrection) { 32 | val |= 1 << 1 33 | } 34 | if (this.properties.colorAdjustments) { 35 | val |= 1 << 2 36 | } 37 | 38 | buffer.writeUInt8(val, 3) 39 | 40 | return buffer 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyCutSourceSetCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../../CommandBase' 2 | 3 | export class MixEffectKeyCutSourceSetCommand extends BasicWritableCommand<{ cutSource: number }> { 4 | public static readonly rawName = 'CKeC' 5 | 6 | public readonly mixEffect: number 7 | public readonly upstreamKeyerId: number 8 | 9 | constructor(mixEffect: number, upstreamKeyerId: number, cutSource: number) { 10 | super({ cutSource }) 11 | 12 | this.mixEffect = mixEffect 13 | this.upstreamKeyerId = upstreamKeyerId 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(4) 18 | buffer.writeUInt8(this.mixEffect, 0) 19 | buffer.writeUInt8(this.upstreamKeyerId, 1) 20 | buffer.writeUInt16BE(this.properties.cutSource, 2) 21 | return buffer 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyFillSourceSetCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../../CommandBase' 2 | 3 | export class MixEffectKeyFillSourceSetCommand extends BasicWritableCommand<{ fillSource: number }> { 4 | public static readonly rawName = 'CKeF' 5 | 6 | public readonly mixEffect: number 7 | public readonly upstreamKeyerId: number 8 | 9 | constructor(mixEffect: number, upstreamKeyerId: number, fillSource: number) { 10 | super({ fillSource }) 11 | 12 | this.mixEffect = mixEffect 13 | this.upstreamKeyerId = upstreamKeyerId 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(4) 18 | buffer.writeUInt8(this.mixEffect, 0) 19 | buffer.writeUInt8(this.upstreamKeyerId, 1) 20 | buffer.writeUInt16BE(this.properties.fillSource, 2) 21 | return buffer 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyFlyPropertiesGetCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | import { UpstreamKeyerFlySettings } from '../../../state/video/upstreamKeyers' 4 | 5 | export class MixEffectKeyFlyPropertiesGetCommand extends DeserializedCommand { 6 | public static readonly rawName = 'KeFS' 7 | 8 | public readonly mixEffect: number 9 | public readonly upstreamKeyerId: number 10 | 11 | constructor(mixEffect: number, upstreamKeyerId: number, properties: UpstreamKeyerFlySettings) { 12 | super(properties) 13 | 14 | this.mixEffect = mixEffect 15 | this.upstreamKeyerId = upstreamKeyerId 16 | } 17 | 18 | public static deserialize(rawCommand: Buffer): MixEffectKeyFlyPropertiesGetCommand { 19 | const mixEffect = rawCommand.readUInt8(0) 20 | const upstreamKeyerId = rawCommand.readUInt8(1) 21 | const properties = { 22 | isASet: rawCommand.readUInt8(2) === 1, 23 | isBSet: rawCommand.readUInt8(3) === 1, 24 | isAtKeyFrame: rawCommand.readUInt8(6), 25 | runToInfiniteIndex: rawCommand.readUInt8(7), 26 | } 27 | return new MixEffectKeyFlyPropertiesGetCommand(mixEffect, upstreamKeyerId, properties) 28 | } 29 | 30 | public applyToState(state: AtemState): string { 31 | const meInfo = state.info.mixEffects[this.mixEffect] 32 | if (!meInfo || this.upstreamKeyerId >= meInfo.keyCount) { 33 | throw new InvalidIdError('UpstreamKeyer', this.mixEffect, this.upstreamKeyerId) 34 | } 35 | 36 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 37 | const upstreamKeyer = AtemStateUtil.getUpstreamKeyer(mixEffect, this.upstreamKeyerId) 38 | upstreamKeyer.flyProperties = { 39 | ...this.properties, 40 | } 41 | return `video.mixEffects.${this.mixEffect}.upstreamKeyers.${this.upstreamKeyerId}.flyProperties` 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyMaskSetCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../../CommandBase' 2 | import { UpstreamKeyerMaskSettings } from '../../../state/video/upstreamKeyers' 3 | 4 | export class MixEffectKeyMaskSetCommand extends WritableCommand { 5 | public static MaskFlags = { 6 | maskEnabled: 1 << 0, 7 | maskTop: 1 << 1, 8 | maskBottom: 1 << 2, 9 | maskLeft: 1 << 3, 10 | maskRight: 1 << 4, 11 | } 12 | 13 | public static readonly rawName = 'CKMs' 14 | 15 | public readonly mixEffect: number 16 | public readonly upstreamKeyerId: number 17 | 18 | constructor(mixEffect: number, upstreamKeyerId: number) { 19 | super() 20 | 21 | this.mixEffect = mixEffect 22 | this.upstreamKeyerId = upstreamKeyerId 23 | } 24 | 25 | public serialize(): Buffer { 26 | const buffer = Buffer.alloc(12) 27 | buffer.writeUInt8(this.flag, 0) 28 | buffer.writeUInt8(this.mixEffect, 1) 29 | buffer.writeUInt8(this.upstreamKeyerId, 2) 30 | 31 | buffer.writeUInt8(this.properties.maskEnabled ? 1 : 0, 3) 32 | buffer.writeInt16BE(this.properties.maskTop || 0, 4) 33 | buffer.writeInt16BE(this.properties.maskBottom || 0, 6) 34 | buffer.writeInt16BE(this.properties.maskLeft || 0, 8) 35 | buffer.writeInt16BE(this.properties.maskRight || 0, 10) 36 | 37 | return buffer 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyOnAirCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | 4 | export class MixEffectKeyOnAirCommand extends BasicWritableCommand<{ onAir: boolean }> { 5 | public static readonly rawName = 'CKOn' 6 | 7 | public readonly mixEffect: number 8 | public readonly upstreamKeyerId: number 9 | 10 | constructor(mixEffect: number, upstreamKeyerId: number, onAir: boolean) { 11 | super({ onAir }) 12 | 13 | this.mixEffect = mixEffect 14 | this.upstreamKeyerId = upstreamKeyerId 15 | } 16 | 17 | public serialize(): Buffer { 18 | const buffer = Buffer.alloc(4) 19 | buffer.writeUInt8(this.mixEffect, 0) 20 | buffer.writeUInt8(this.upstreamKeyerId, 1) 21 | buffer.writeUInt8(this.properties.onAir ? 1 : 0, 2) 22 | return buffer 23 | } 24 | } 25 | 26 | export class MixEffectKeyOnAirUpdateCommand extends DeserializedCommand<{ onAir: boolean }> { 27 | public static readonly rawName = 'KeOn' 28 | 29 | public readonly mixEffect: number 30 | public readonly upstreamKeyerId: number 31 | 32 | constructor(mixEffect: number, upstreamKeyerId: number, properties: MixEffectKeyOnAirUpdateCommand['properties']) { 33 | super(properties) 34 | 35 | this.mixEffect = mixEffect 36 | this.upstreamKeyerId = upstreamKeyerId 37 | } 38 | 39 | public static deserialize(rawCommand: Buffer): MixEffectKeyOnAirUpdateCommand { 40 | const mixEffect = rawCommand.readUInt8(0) 41 | const upstreamKeyerId = rawCommand.readUInt8(1) 42 | const properties = { 43 | onAir: rawCommand.readUInt8(2) === 1, 44 | } 45 | return new MixEffectKeyOnAirUpdateCommand(mixEffect, upstreamKeyerId, properties) 46 | } 47 | 48 | public applyToState(state: AtemState): string { 49 | const meInfo = state.info.mixEffects[this.mixEffect] 50 | if (!meInfo || this.upstreamKeyerId >= meInfo.keyCount) { 51 | throw new InvalidIdError('UpstreamKeyer', this.mixEffect, this.upstreamKeyerId) 52 | } 53 | 54 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 55 | const upstreamKeyer = AtemStateUtil.getUpstreamKeyer(mixEffect, this.upstreamKeyerId) 56 | upstreamKeyer.onAir = this.properties.onAir 57 | return `video.mixEffects.${this.mixEffect}.upstreamKeyers.${this.upstreamKeyerId}.onAir` 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyPropertiesGetCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | import { UpstreamKeyerBase } from '../../../state/video/upstreamKeyers' 4 | 5 | export class MixEffectKeyPropertiesGetCommand extends DeserializedCommand { 6 | public static readonly rawName = 'KeBP' 7 | 8 | public readonly mixEffect: number 9 | public readonly upstreamKeyerId: number 10 | 11 | constructor(mixEffect: number, keyer: number, properties: UpstreamKeyerBase) { 12 | super(properties) 13 | 14 | this.mixEffect = mixEffect 15 | this.upstreamKeyerId = keyer 16 | } 17 | 18 | public static deserialize(rawCommand: Buffer): MixEffectKeyPropertiesGetCommand { 19 | const mixEffect = rawCommand.readUInt8(0) 20 | const keyer = rawCommand.readUInt8(1) 21 | const properties = { 22 | upstreamKeyerId: keyer, 23 | mixEffectKeyType: rawCommand.readUInt8(2), 24 | canFlyKey: rawCommand.readUInt8(4) === 1, 25 | flyEnabled: rawCommand.readUInt8(5) === 1, 26 | fillSource: rawCommand.readUInt16BE(6), 27 | cutSource: rawCommand.readUInt16BE(8), 28 | maskSettings: { 29 | maskEnabled: rawCommand.readUInt8(10) === 1, 30 | maskTop: rawCommand.readInt16BE(12), 31 | maskBottom: rawCommand.readInt16BE(14), 32 | maskLeft: rawCommand.readInt16BE(16), 33 | maskRight: rawCommand.readInt16BE(18), 34 | }, 35 | } 36 | 37 | return new MixEffectKeyPropertiesGetCommand(mixEffect, keyer, properties) 38 | } 39 | 40 | public applyToState(state: AtemState): string { 41 | const meInfo = state.info.mixEffects[this.mixEffect] 42 | if (!meInfo || this.upstreamKeyerId >= meInfo.keyCount) { 43 | throw new InvalidIdError('UpstreamKeyer', this.mixEffect, this.upstreamKeyerId) 44 | } 45 | 46 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 47 | mixEffect.upstreamKeyers[this.properties.upstreamKeyerId] = { 48 | ...AtemStateUtil.getUpstreamKeyer(mixEffect, this.properties.upstreamKeyerId), 49 | ...this.properties, 50 | } 51 | return `video.mixEffects.${this.mixEffect}.upstreamKeyers.${this.properties.upstreamKeyerId}` 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyRunToCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from '../../CommandBase' 2 | import { FlyKeyKeyFrame, FlyKeyDirection } from '../../../enums' 3 | 4 | export class MixEffectKeyRunToCommand extends BasicWritableCommand<{ 5 | keyFrameId: FlyKeyKeyFrame 6 | direction: FlyKeyDirection 7 | }> { 8 | public static readonly rawName = 'RFlK' 9 | 10 | public readonly mixEffect: number 11 | public readonly upstreamKeyerId: number 12 | 13 | constructor(mixEffect: number, upstreamKeyerId: number, keyFrameId: FlyKeyKeyFrame, direction: FlyKeyDirection) { 14 | super({ keyFrameId, direction }) 15 | 16 | this.mixEffect = mixEffect 17 | this.upstreamKeyerId = upstreamKeyerId 18 | } 19 | 20 | public serialize(): Buffer { 21 | const buffer = Buffer.alloc(8) 22 | buffer.writeUInt8(this.properties.keyFrameId === FlyKeyKeyFrame.RunToInfinite ? 2 : 0, 0) 23 | buffer.writeUInt8(this.mixEffect, 1) 24 | buffer.writeUInt8(this.upstreamKeyerId, 2) 25 | buffer.writeUInt8(this.properties.keyFrameId, 4) 26 | buffer.writeUInt8(this.properties.direction, 5) 27 | 28 | return buffer 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/MixEffectKeyTypeSetCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand } from '../../CommandBase' 2 | import { UpstreamKeyerTypeSettings } from '../../../state/video/upstreamKeyers' 3 | 4 | export class MixEffectKeyTypeSetCommand extends WritableCommand { 5 | public static MaskFlags = { 6 | mixEffectKeyType: 1 << 0, 7 | flyEnabled: 1 << 1, 8 | } 9 | 10 | public static readonly rawName = 'CKTp' 11 | 12 | public readonly mixEffect: number 13 | public readonly upstreamKeyerId: number 14 | 15 | constructor(mixEffect: number, upstreamKeyerId: number) { 16 | super() 17 | 18 | this.mixEffect = mixEffect 19 | this.upstreamKeyerId = upstreamKeyerId 20 | } 21 | 22 | public serialize(): Buffer { 23 | const buffer = Buffer.alloc(8) 24 | buffer.writeUInt8(this.flag, 0) 25 | buffer.writeUInt8(this.mixEffect, 1) 26 | buffer.writeUInt8(this.upstreamKeyerId, 2) 27 | 28 | buffer.writeUInt8(this.properties.mixEffectKeyType || 0, 3) 29 | buffer.writeUInt8(this.properties.flyEnabled ? 1 : 0, 4) 30 | 31 | return buffer 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Key/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MixEffectKeyAdvancedChromaPropertiesCommand' 2 | export * from './MixEffectKeyAdvancedChromaSampleCommand' 3 | export * from './MixEffectKeyAdvancedChromaSampleResetCommand' 4 | export * from './MixEffectKeyChromaCommand' 5 | export * from './MixEffectKeyCutSourceSetCommand' 6 | export * from './MixEffectKeyDVECommand' 7 | export * from './MixEffectKeyFillSourceSetCommand' 8 | export * from './MixEffectKeyFlyKeyframeCommand' 9 | export * from './MixEffectKeyFlyPropertiesGetCommand' 10 | export * from './MixEffectKeyLumaCommand' 11 | export * from './MixEffectKeyMaskSetCommand' 12 | export * from './MixEffectKeyRunToCommand' 13 | export * from './MixEffectKeyOnAirCommand' 14 | export * from './MixEffectKeyPatternCommand' 15 | export * from './MixEffectKeyPropertiesGetCommand' 16 | export * from './MixEffectKeyTypeSetCommand' 17 | -------------------------------------------------------------------------------- /src/commands/MixEffects/PreviewInputCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 3 | 4 | export interface InputSource { 5 | source: number 6 | } 7 | 8 | export class PreviewInputCommand extends BasicWritableCommand { 9 | public static readonly rawName = 'CPvI' 10 | 11 | public readonly mixEffect: number 12 | 13 | constructor(mixEffect: number, source: number) { 14 | super({ source }) 15 | 16 | this.mixEffect = mixEffect 17 | } 18 | 19 | public serialize(): Buffer { 20 | const buffer = Buffer.alloc(4) 21 | buffer.writeUInt8(this.mixEffect, 0) 22 | buffer.writeUInt16BE(this.properties.source, 2) 23 | return buffer 24 | } 25 | } 26 | 27 | export class PreviewInputUpdateCommand extends DeserializedCommand { 28 | public static readonly rawName = 'PrvI' 29 | 30 | public readonly mixEffect: number 31 | 32 | constructor(mixEffect: number, properties: InputSource) { 33 | super(properties) 34 | 35 | this.mixEffect = mixEffect 36 | } 37 | 38 | public static deserialize(rawCommand: Buffer): PreviewInputUpdateCommand { 39 | const mixEffect = rawCommand.readUInt8(0) 40 | const properties = { 41 | source: rawCommand.readUInt16BE(2), 42 | } 43 | 44 | return new PreviewInputUpdateCommand(mixEffect, properties) 45 | } 46 | 47 | public applyToState(state: AtemState): string { 48 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 49 | throw new InvalidIdError('MixEffect', this.mixEffect) 50 | } 51 | 52 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 53 | mixEffect.previewInput = this.properties.source 54 | return `video.mixEffects.${this.mixEffect}.previewInput` 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/MixEffects/ProgramInputCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 3 | import { InputSource } from './PreviewInputCommand' 4 | 5 | export class ProgramInputCommand extends BasicWritableCommand { 6 | public static readonly rawName = 'CPgI' 7 | 8 | public readonly mixEffect: number 9 | 10 | constructor(mixEffect: number, source: number) { 11 | super({ source }) 12 | 13 | this.mixEffect = mixEffect 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(4) 18 | buffer.writeUInt8(this.mixEffect, 0) 19 | buffer.writeUInt16BE(this.properties.source, 2) 20 | return buffer 21 | } 22 | } 23 | 24 | export class ProgramInputUpdateCommand extends DeserializedCommand { 25 | public static readonly rawName = 'PrgI' 26 | 27 | public readonly mixEffect: number 28 | 29 | constructor(mixEffect: number, properties: InputSource) { 30 | super(properties) 31 | 32 | this.mixEffect = mixEffect 33 | } 34 | 35 | public static deserialize(rawCommand: Buffer): ProgramInputUpdateCommand { 36 | const mixEffect = rawCommand.readUInt8(0) 37 | const properties = { 38 | source: rawCommand.readUInt16BE(2), 39 | } 40 | 41 | return new ProgramInputUpdateCommand(mixEffect, properties) 42 | } 43 | 44 | public applyToState(state: AtemState): string { 45 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 46 | throw new InvalidIdError('MixEffect', this.mixEffect) 47 | } 48 | 49 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 50 | mixEffect.programInput = this.properties.source 51 | return `video.mixEffects.${this.mixEffect}.programInput` 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Transition/TransitionDipCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | import { DipTransitionSettings } from '../../../state/video' 4 | 5 | export class TransitionDipCommand extends WritableCommand { 6 | public static MaskFlags = { 7 | rate: 1 << 0, 8 | input: 1 << 1, 9 | } 10 | public static readonly rawName = 'CTDp' 11 | 12 | public readonly mixEffect: number 13 | 14 | constructor(mixEffect: number) { 15 | super() 16 | 17 | this.mixEffect = mixEffect 18 | } 19 | 20 | public serialize(): Buffer { 21 | const buffer = Buffer.alloc(8) 22 | buffer.writeUInt8(this.flag, 0) 23 | buffer.writeUInt8(this.mixEffect, 1) 24 | buffer.writeUInt8(this.properties.rate || 0, 2) 25 | buffer.writeUInt16BE(this.properties.input || 0, 4) 26 | return buffer 27 | } 28 | } 29 | 30 | export class TransitionDipUpdateCommand extends DeserializedCommand { 31 | public static readonly rawName = 'TDpP' 32 | 33 | public readonly mixEffect: number 34 | 35 | constructor(mixEffect: number, properties: DipTransitionSettings) { 36 | super(properties) 37 | 38 | this.mixEffect = mixEffect 39 | } 40 | 41 | public static deserialize(rawCommand: Buffer): TransitionDipUpdateCommand { 42 | const mixEffect = rawCommand.readUInt8(0) 43 | const properties = { 44 | rate: rawCommand.readUInt8(1), 45 | input: (rawCommand.readUInt8(2) << 8) | (rawCommand.readUInt8(3) & 0xff), 46 | } 47 | 48 | return new TransitionDipUpdateCommand(mixEffect, properties) 49 | } 50 | 51 | public applyToState(state: AtemState): string { 52 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 53 | throw new InvalidIdError('MixEffect', this.mixEffect) 54 | } 55 | 56 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 57 | mixEffect.transitionSettings.dip = { 58 | ...this.properties, 59 | } 60 | return `video.mixEffects.${this.mixEffect}.transitionSettings.dip` 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Transition/TransitionMixCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand, BasicWritableCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | import { MixTransitionSettings } from '../../../state/video' 4 | 5 | export class TransitionMixCommand extends BasicWritableCommand { 6 | public static readonly rawName = 'CTMx' 7 | 8 | public readonly mixEffect: number 9 | 10 | constructor(mixEffect: number, rate: number) { 11 | super({ rate }) 12 | 13 | this.mixEffect = mixEffect 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(4) 18 | buffer.writeUInt8(this.mixEffect, 0) 19 | buffer.writeUInt8(this.properties.rate || 0, 1) 20 | return buffer 21 | } 22 | } 23 | 24 | export class TransitionMixUpdateCommand extends DeserializedCommand { 25 | public static readonly rawName = 'TMxP' 26 | 27 | public readonly mixEffect: number 28 | 29 | constructor(mixEffect: number, properties: MixTransitionSettings) { 30 | super(properties) 31 | 32 | this.mixEffect = mixEffect 33 | } 34 | 35 | public static deserialize(rawCommand: Buffer): TransitionMixUpdateCommand { 36 | const mixEffect = rawCommand.readUInt8(0) 37 | const properties = { 38 | rate: rawCommand.readUInt8(1), 39 | } 40 | 41 | return new TransitionMixUpdateCommand(mixEffect, properties) 42 | } 43 | 44 | public applyToState(state: AtemState): string { 45 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 46 | throw new InvalidIdError('MixEffect', this.mixEffect) 47 | } 48 | 49 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 50 | mixEffect.transitionSettings.mix = { 51 | ...this.properties, 52 | } 53 | return `video.mixEffects.${this.mixEffect}.transitionSettings.mix` 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Transition/TransitionPositionCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | import { TransitionPosition } from '../../../state/video' 4 | 5 | export class TransitionPositionCommand extends BasicWritableCommand> { 6 | public static readonly rawName = 'CTPs' 7 | 8 | public readonly mixEffect: number 9 | 10 | constructor(mixEffect: number, handlePosition: number) { 11 | super({ handlePosition }) 12 | 13 | this.mixEffect = mixEffect 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(4) 18 | buffer.writeUInt8(this.mixEffect, 0) 19 | buffer.writeUInt16BE(this.properties.handlePosition, 2) 20 | return buffer 21 | } 22 | } 23 | 24 | export class TransitionPositionUpdateCommand extends DeserializedCommand { 25 | public static readonly rawName = 'TrPs' 26 | 27 | public readonly mixEffect: number 28 | 29 | constructor(mixEffect: number, properties: TransitionPosition) { 30 | super(properties) 31 | 32 | this.mixEffect = mixEffect 33 | } 34 | 35 | public static deserialize(rawCommand: Buffer): TransitionPositionUpdateCommand { 36 | const mixEffect = rawCommand.readUInt8(0) 37 | const properties = { 38 | inTransition: rawCommand.readUInt8(1) === 1, 39 | remainingFrames: rawCommand.readUInt8(2), 40 | handlePosition: rawCommand.readUInt16BE(4), 41 | } 42 | 43 | return new TransitionPositionUpdateCommand(mixEffect, properties) 44 | } 45 | 46 | public applyToState(state: AtemState): string { 47 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 48 | throw new InvalidIdError('MixEffect', this.mixEffect) 49 | } 50 | 51 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 52 | mixEffect.transitionPosition = this.properties 53 | return `video.mixEffects.${this.mixEffect}.transitionPosition` 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Transition/TransitionPreviewCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../../state' 3 | 4 | export interface PreviewProps { 5 | preview: boolean 6 | } 7 | 8 | export class PreviewTransitionCommand extends BasicWritableCommand { 9 | public static readonly rawName = 'CTPr' 10 | 11 | public readonly mixEffect: number 12 | 13 | constructor(mixEffect: number, preview: boolean) { 14 | super({ preview }) 15 | 16 | this.mixEffect = mixEffect 17 | } 18 | 19 | public serialize(): Buffer { 20 | const buffer = Buffer.alloc(4) 21 | buffer.writeUInt8(this.mixEffect, 0) 22 | buffer.writeUInt8(this.properties.preview ? 1 : 0, 1) 23 | return buffer 24 | } 25 | } 26 | 27 | export class PreviewTransitionUpdateCommand extends DeserializedCommand { 28 | public static readonly rawName = 'TrPr' 29 | 30 | public readonly mixEffect: number 31 | 32 | constructor(mixEffect: number, properties: PreviewProps) { 33 | super(properties) 34 | 35 | this.mixEffect = mixEffect 36 | } 37 | 38 | public static deserialize(rawCommand: Buffer): PreviewTransitionUpdateCommand { 39 | const mixEffect = rawCommand.readUInt8(0) 40 | const properties = { 41 | preview: rawCommand.readUInt8(1) === 1, 42 | } 43 | 44 | return new PreviewTransitionUpdateCommand(mixEffect, properties) 45 | } 46 | 47 | public applyToState(state: AtemState): string { 48 | if (!state.info.capabilities || this.mixEffect >= state.info.capabilities.mixEffects) { 49 | throw new InvalidIdError('MixEffect', this.mixEffect) 50 | } 51 | 52 | const mixEffect = AtemStateUtil.getMixEffect(state, this.mixEffect) 53 | mixEffect.transitionPreview = this.properties.preview 54 | return `video.mixEffects.${this.mixEffect}.transitionPreview` 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/MixEffects/Transition/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TransitionDipCommand' 2 | export * from './TransitionDVECommand' 3 | export * from './TransitionMixCommand' 4 | export * from './TransitionPositionCommand' 5 | export * from './TransitionPreviewCommand' 6 | export * from './TransitionPropertiesCommand' 7 | export * from './TransitionStingerCommand' 8 | export * from './TransitionWipeCommand' 9 | -------------------------------------------------------------------------------- /src/commands/MixEffects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FadeToBlack' 2 | export * from './Key' 3 | export * from './Transition' 4 | 5 | export * from './AutoTransitionCommand' 6 | export * from './CutCommand' 7 | export * from './PreviewInputCommand' 8 | export * from './ProgramInputCommand' 9 | -------------------------------------------------------------------------------- /src/commands/PowerStatusCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from './CommandBase' 2 | import { AtemState } from '../state' 3 | 4 | /** 5 | * This command gets the power status from the Atem. As defined in 6 | * DeviceProfile/productIdentifierCommand.ts the 2ME, 2ME 4K and the 7 | * Broadcast Studio have 2 power supplies. All other models have 1. 8 | */ 9 | export class PowerStatusCommand extends DeserializedCommand { 10 | public static readonly rawName = 'Powr' 11 | 12 | public static deserialize(rawCommand: Buffer): PowerStatusCommand { 13 | const properties = [Boolean(rawCommand.readUInt8(0) & (1 << 0)), Boolean(rawCommand.readUInt8(0) & (1 << 1))] 14 | 15 | return new PowerStatusCommand(properties) 16 | } 17 | 18 | public applyToState(state: AtemState): string { 19 | const count = state.info.power.length 20 | state.info.power = this.properties.slice(0, count) 21 | return `info.power` 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/Recording/RecordingDiskCommand.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolVersion } from '../../enums' 2 | import { InvalidIdError, AtemState } from '../../state' 3 | import { DeserializedCommand, BasicWritableCommand } from '../CommandBase' 4 | import { RecordingDiskProperties } from '../../state/recording' 5 | import { bufToNullTerminatedString } from '../../lib/atemUtil' 6 | 7 | export class RecordingRequestSwitchDiskCommand extends BasicWritableCommand> { 8 | public static readonly rawName = 'RMSp' 9 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 10 | 11 | constructor() { 12 | super({}) 13 | } 14 | 15 | public serialize(): Buffer { 16 | return Buffer.alloc(0) 17 | } 18 | } 19 | 20 | export interface DeletableRecordingDiskProperties extends RecordingDiskProperties { 21 | isDelete: boolean 22 | } 23 | 24 | export class RecordingDiskInfoUpdateCommand extends DeserializedCommand { 25 | public static readonly rawName = 'RTMD' 26 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 27 | 28 | public static readonly DeleteStatusFlag = 1 << 5 29 | 30 | public readonly diskId: number 31 | 32 | constructor(diskId: number, properties: DeletableRecordingDiskProperties) { 33 | super(properties) 34 | 35 | this.diskId = diskId 36 | } 37 | 38 | public static deserialize(rawCommand: Buffer): RecordingDiskInfoUpdateCommand { 39 | const diskId = rawCommand.readUInt32BE(0) 40 | const rawStatus = rawCommand.readUInt16BE(8) 41 | 42 | const props: DeletableRecordingDiskProperties = { 43 | diskId, 44 | recordingTimeAvailable: rawCommand.readUInt32BE(4), 45 | status: rawStatus & ~this.DeleteStatusFlag, 46 | isDelete: (rawStatus & this.DeleteStatusFlag) === this.DeleteStatusFlag, 47 | volumeName: bufToNullTerminatedString(rawCommand, 10, 64), 48 | } 49 | 50 | return new RecordingDiskInfoUpdateCommand(diskId, props) 51 | } 52 | 53 | public applyToState(state: AtemState): string { 54 | if (!state.recording) { 55 | throw new InvalidIdError('Recording') 56 | } 57 | 58 | if (this.properties.isDelete) { 59 | delete state.recording.disks[this.diskId] 60 | } else { 61 | state.recording.disks[this.diskId] = this.properties 62 | } 63 | 64 | return `recording.duration` 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/Recording/RecordingDurationCommand.ts: -------------------------------------------------------------------------------- 1 | import { Timecode } from '../../state/common' 2 | import { ProtocolVersion } from '../../enums' 3 | import { InvalidIdError, AtemState } from '../../state' 4 | import { DeserializedCommand, BasicWritableCommand } from '../CommandBase' 5 | 6 | export class RecordingRequestDurationCommand extends BasicWritableCommand> { 7 | public static readonly rawName = 'RMDR' 8 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 9 | 10 | constructor() { 11 | super({}) 12 | } 13 | 14 | public serialize(): Buffer { 15 | return Buffer.alloc(0) 16 | } 17 | } 18 | 19 | export class RecordingDurationUpdateCommand extends DeserializedCommand { 20 | public static readonly rawName = 'RTMR' 21 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 22 | 23 | constructor(properties: Timecode) { 24 | super(properties) 25 | } 26 | 27 | public static deserialize(rawCommand: Buffer): RecordingDurationUpdateCommand { 28 | const props: Timecode = { 29 | hours: rawCommand.readUInt8(0), 30 | minutes: rawCommand.readUInt8(1), 31 | seconds: rawCommand.readUInt8(2), 32 | frames: rawCommand.readUInt8(3), 33 | isDropFrame: rawCommand.readUInt8(4) != 0, 34 | } 35 | 36 | return new RecordingDurationUpdateCommand(props) 37 | } 38 | 39 | public applyToState(state: AtemState): string { 40 | if (!state.recording) { 41 | throw new InvalidIdError('Recording') 42 | } 43 | 44 | state.recording.duration = this.properties 45 | 46 | return `recording.duration` 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/Recording/RecordingISOCommand.ts: -------------------------------------------------------------------------------- 1 | import { SymmetricalCommand } from '../CommandBase' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { ProtocolVersion } from '../../enums' 4 | 5 | export class RecordingISOCommand extends SymmetricalCommand<{ recordAllInputs: boolean }> { 6 | public static readonly rawName = 'ISOi' 7 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 8 | 9 | constructor(recordAllInputs: boolean) { 10 | super({ recordAllInputs }) 11 | } 12 | 13 | public serialize(): Buffer { 14 | const buffer = Buffer.alloc(4) 15 | buffer.writeUInt8(this.properties.recordAllInputs ? 1 : 0, 0) 16 | return buffer 17 | } 18 | 19 | public static deserialize(rawCommand: Buffer): RecordingISOCommand { 20 | const recordAllInputs = rawCommand.readUInt8(0) > 0 21 | 22 | return new RecordingISOCommand(recordAllInputs) 23 | } 24 | 25 | public applyToState(state: AtemState): string { 26 | if (!state.recording) { 27 | throw new InvalidIdError('Recording') 28 | } 29 | 30 | state.recording.recordAllInputs = this.properties.recordAllInputs 31 | 32 | return `recording.recordAllInputs` 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/Recording/RecordingSettingsCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { RecordingStateProperties } from '../../state/recording' 4 | import { ProtocolVersion } from '../../enums' 5 | import { bufToNullTerminatedString } from '../../lib/atemUtil' 6 | 7 | export class RecordingSettingsCommand extends WritableCommand { 8 | public static readonly rawName = 'CRMS' 9 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 10 | 11 | public static MaskFlags = { 12 | filename: 1 << 0, 13 | workingSet1DiskId: 1 << 1, 14 | workingSet2DiskId: 1 << 2, 15 | recordInAllCameras: 1 << 3, 16 | } 17 | 18 | public serialize(): Buffer { 19 | const buffer = Buffer.alloc(144) 20 | buffer.writeUInt8(this.flag, 0) 21 | buffer.write(this.properties.filename || '', 1, 128, 'utf8') 22 | buffer.writeUInt32BE(this.properties.workingSet1DiskId || 0, 132) 23 | buffer.writeUInt32BE(this.properties.workingSet2DiskId || 0, 136) 24 | buffer.writeUInt8(this.properties.recordInAllCameras ? 1 : 0, 140) 25 | return buffer 26 | } 27 | } 28 | 29 | export class RecordingSettingsUpdateCommand extends DeserializedCommand { 30 | public static readonly rawName = 'RMSu' 31 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 32 | 33 | constructor(properties: RecordingStateProperties) { 34 | super(properties) 35 | } 36 | 37 | public static deserialize(rawCommand: Buffer): RecordingSettingsUpdateCommand { 38 | const props: RecordingStateProperties = { 39 | filename: bufToNullTerminatedString(rawCommand, 0, 128), 40 | workingSet1DiskId: rawCommand.readUInt32BE(128), 41 | workingSet2DiskId: rawCommand.readUInt32BE(132), 42 | recordInAllCameras: rawCommand.readUInt8(136) != 0, 43 | } 44 | 45 | return new RecordingSettingsUpdateCommand(props) 46 | } 47 | 48 | public applyToState(state: AtemState): string { 49 | if (!state.recording) { 50 | state.recording = { 51 | properties: this.properties, 52 | disks: {}, 53 | } 54 | } else { 55 | state.recording.properties = this.properties 56 | } 57 | 58 | return `recording.properties` 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/Recording/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RecordingDiskCommand' 2 | export * from './RecordingDurationCommand' 3 | export * from './RecordingISOCommand' 4 | export * from './RecordingSettingsCommand' 5 | export * from './RecordingStatusCommand' 6 | -------------------------------------------------------------------------------- /src/commands/Settings/MediaPool.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { ProtocolVersion } from '../../enums' 4 | 5 | export interface MediaPoolProps { 6 | maxFrames: number[] 7 | } 8 | 9 | export class MediaPoolSettingsSetCommand extends BasicWritableCommand { 10 | public static readonly rawName = 'CMPS' 11 | public static readonly minimumVersion = ProtocolVersion.V8_0 12 | 13 | constructor(maxFrames: number[]) { 14 | super({ maxFrames }) 15 | } 16 | 17 | public serialize(): Buffer { 18 | const buffer = Buffer.alloc(8) 19 | buffer.writeUInt16BE(this.properties.maxFrames[0] || 0, 0) 20 | buffer.writeUInt16BE(this.properties.maxFrames[1] || 0, 2) 21 | buffer.writeUInt16BE(this.properties.maxFrames[2] || 0, 4) 22 | buffer.writeUInt16BE(this.properties.maxFrames[3] || 0, 6) 23 | return buffer 24 | } 25 | } 26 | 27 | export class MediaPoolSettingsGetCommand extends DeserializedCommand { 28 | public static readonly rawName = 'MPSp' 29 | public static readonly minimumVersion = ProtocolVersion.V8_0 30 | 31 | constructor(maxFrames: number[], unassignedFrames: number) { 32 | super({ maxFrames, unassignedFrames }) 33 | } 34 | 35 | public static deserialize(rawCommand: Buffer): MediaPoolSettingsGetCommand { 36 | return new MediaPoolSettingsGetCommand( 37 | [ 38 | rawCommand.readUInt16BE(0), 39 | rawCommand.readUInt16BE(2), 40 | rawCommand.readUInt16BE(4), 41 | rawCommand.readUInt16BE(6), 42 | ], 43 | rawCommand.readUInt16BE(8) 44 | ) 45 | } 46 | 47 | public applyToState(state: AtemState): string { 48 | state.settings.mediaPool = { 49 | maxFrames: this.properties.maxFrames, 50 | unassignedFrames: this.properties.unassignedFrames, 51 | } 52 | return `settings.mediaPool` 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/Settings/MultiViewerVuOpacityCommand.ts: -------------------------------------------------------------------------------- 1 | import { SymmetricalCommand } from '../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 3 | 4 | export interface MultiViewerVuOpacityState { 5 | opacity: number 6 | } 7 | 8 | export class MultiViewerVuOpacityCommand extends SymmetricalCommand { 9 | public static readonly rawName = 'VuMo' 10 | 11 | public readonly multiViewerId: number 12 | 13 | constructor(multiviewerId: number, opacity: number) { 14 | super({ opacity }) 15 | 16 | this.multiViewerId = multiviewerId 17 | } 18 | 19 | public serialize(): Buffer { 20 | const buffer = Buffer.alloc(4) 21 | buffer.writeUInt8(this.multiViewerId, 0) 22 | buffer.writeUInt8(this.properties.opacity, 1) 23 | return buffer 24 | } 25 | 26 | public static deserialize(rawCommand: Buffer): MultiViewerVuOpacityCommand { 27 | const multiViewerId = rawCommand.readUInt8(0) 28 | const opacity = rawCommand.readUInt8(1) 29 | 30 | return new MultiViewerVuOpacityCommand(multiViewerId, opacity) 31 | } 32 | 33 | public applyToState(state: AtemState): string { 34 | if (!state.info.multiviewer || this.multiViewerId >= state.info.multiviewer.count) { 35 | throw new InvalidIdError('MultiViewer', this.multiViewerId) 36 | } 37 | 38 | const multiviewer = AtemStateUtil.getMultiViewer(state, this.multiViewerId) 39 | multiviewer.vuOpacity = this.properties.opacity 40 | 41 | return `settings.multiViewers.${this.multiViewerId}.vuOpacity` 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/Settings/MultiViewerWindowSafeAreaCommand.ts: -------------------------------------------------------------------------------- 1 | import { SymmetricalCommand } from '../CommandBase' 2 | import { AtemState, AtemStateUtil, InvalidIdError } from '../../state' 3 | import { ProtocolVersion } from '../../enums' 4 | 5 | export class MultiViewerWindowSafeAreaCommand extends SymmetricalCommand<{ safeAreaEnabled: boolean }> { 6 | public static readonly rawName = 'SaMw' 7 | public static readonly minimumVersion = ProtocolVersion.V8_0 8 | 9 | public readonly multiViewerId: number 10 | public readonly windowIndex: number 11 | 12 | constructor(multiviewerId: number, windowIndex: number, safeAreaEnabled: boolean) { 13 | super({ safeAreaEnabled }) 14 | 15 | this.multiViewerId = multiviewerId 16 | this.windowIndex = windowIndex 17 | } 18 | 19 | public serialize(): Buffer { 20 | const buffer = Buffer.alloc(4) 21 | buffer.writeUInt8(this.multiViewerId, 0) 22 | buffer.writeUInt8(this.windowIndex, 1) 23 | buffer.writeUInt8(this.properties.safeAreaEnabled ? 1 : 0, 2) 24 | return buffer 25 | } 26 | 27 | public static deserialize(rawCommand: Buffer): MultiViewerWindowSafeAreaCommand { 28 | const multiViewerId = rawCommand.readUInt8(0) 29 | const windowIndex = rawCommand.readUInt8(1) 30 | const safeAreaEnabled = rawCommand.readUInt8(2) > 0 31 | 32 | return new MultiViewerWindowSafeAreaCommand(multiViewerId, windowIndex, safeAreaEnabled) 33 | } 34 | 35 | public applyToState(state: AtemState): string { 36 | if (!state.info.multiviewer || this.multiViewerId >= state.info.multiviewer.count) { 37 | throw new InvalidIdError('MultiViewer', this.multiViewerId) 38 | } 39 | 40 | const multiviewer = AtemStateUtil.getMultiViewer(state, this.multiViewerId) 41 | const window = multiviewer.windows[this.windowIndex] 42 | if (!window) { 43 | throw new InvalidIdError('MultiViewer Window', this.multiViewerId, this.windowIndex) 44 | } 45 | window.safeTitle = this.properties.safeAreaEnabled 46 | 47 | return `settings.multiViewers.${this.multiViewerId}.windows.${this.windowIndex}.safeTitle` 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/Settings/VideoMode.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { Enums } from '../..' 4 | 5 | export interface VideoModeProps { 6 | mode: Enums.VideoMode 7 | } 8 | 9 | export class VideoModeCommand extends BasicWritableCommand { 10 | public static readonly rawName = 'CVdM' 11 | 12 | constructor(mode: Enums.VideoMode) { 13 | super({ mode }) 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(4) 18 | buffer.writeUInt8(this.properties.mode, 0) 19 | return buffer 20 | } 21 | } 22 | 23 | export class VideoModeUpdateCommand extends DeserializedCommand { 24 | public static readonly rawName = 'VidM' 25 | 26 | constructor(mode: Enums.VideoMode) { 27 | super({ mode }) 28 | } 29 | 30 | public static deserialize(rawCommand: Buffer): VideoModeUpdateCommand { 31 | return new VideoModeUpdateCommand(rawCommand.readUInt8(0)) 32 | } 33 | 34 | public applyToState(state: AtemState): string { 35 | state.settings.videoMode = this.properties.mode 36 | return `settings.videoMode` 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/Settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MediaPool' 2 | export * from './MultiViewerSourceCommand' 3 | export * from './MultiViewerPropertiesCommand' 4 | export * from './MultiViewerVuOpacityCommand' 5 | export * from './MultiViewerWindowVuMeterCommand' 6 | export * from './MultiViewerWindowSafeAreaCommand' 7 | export * from './VideoMode' 8 | -------------------------------------------------------------------------------- /src/commands/StartupStateCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand } from './CommandBase' 2 | 3 | export class StartupStateSaveCommand extends BasicWritableCommand { 4 | public static readonly rawName = 'SRsv' 5 | 6 | constructor() { 7 | super({}) 8 | } 9 | 10 | public serialize(): Buffer { 11 | const buffer = Buffer.alloc(4) 12 | // 0 is the 'mode' parameter, which is always 0 for now 13 | return buffer 14 | } 15 | } 16 | 17 | export class StartupStateClearCommand extends BasicWritableCommand { 18 | public static readonly rawName = 'SRcl' 19 | 20 | constructor() { 21 | super({}) 22 | } 23 | 24 | public serialize(): Buffer { 25 | const buffer = Buffer.alloc(4) 26 | // 0 is the 'mode' parameter, which is always 0 for now 27 | return buffer 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/Streaming/StreamingAudioBitratesCommand.ts: -------------------------------------------------------------------------------- 1 | import { SymmetricalCommand } from '../CommandBase' 2 | import { InvalidIdError, AtemState } from '../../state' 3 | import { ProtocolVersion } from '../../enums' 4 | import { StreamingAudioBitrates } from '../../state/streaming' 5 | 6 | export class StreamingAudioBitratesCommand extends SymmetricalCommand { 7 | public static readonly rawName = 'STAB' 8 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 9 | 10 | constructor(lowBitrate = 128000, highBitrate = 192000) { 11 | super({ lowBitrate, highBitrate }) 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(8) 16 | const lowBitrate = this.properties.lowBitrate || 128000 17 | const highBitrate = this.properties.highBitrate || 192000 18 | buffer.writeUInt32BE(lowBitrate, 0) 19 | buffer.writeUInt32BE(highBitrate, 4) 20 | return buffer 21 | } 22 | 23 | public static deserialize(rawCommand: Buffer): StreamingAudioBitratesCommand { 24 | const lowBitrate = rawCommand.readUInt32BE(0) 25 | const highBitrate = rawCommand.readUInt32BE(4) 26 | 27 | return new StreamingAudioBitratesCommand(lowBitrate, highBitrate) 28 | } 29 | 30 | public applyToState(state: AtemState): string { 31 | const audioBitrates = { 32 | lowBitrate: this.properties.lowBitrate, 33 | highBitrate: this.properties.highBitrate, 34 | } 35 | if (!state.streaming) { 36 | throw new InvalidIdError('Streaming') 37 | } else { 38 | state.streaming.audioBitrates = audioBitrates 39 | } 40 | 41 | return `streaming.audioBitrates` 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/Streaming/StreamingDurationCommand.ts: -------------------------------------------------------------------------------- 1 | import { Timecode } from '../../state/common' 2 | import { ProtocolVersion } from '../../enums' 3 | import { InvalidIdError, AtemState } from '../../state' 4 | import { DeserializedCommand, BasicWritableCommand } from '../CommandBase' 5 | 6 | export class StreamingRequestDurationCommand extends BasicWritableCommand> { 7 | public static readonly rawName = 'SRDR' 8 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 9 | 10 | constructor() { 11 | super({}) 12 | } 13 | 14 | public serialize(): Buffer { 15 | return Buffer.alloc(0) 16 | } 17 | } 18 | 19 | export class StreamingDurationUpdateCommand extends DeserializedCommand { 20 | public static readonly rawName = 'SRST' 21 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 22 | 23 | constructor(properties: Timecode) { 24 | super(properties) 25 | } 26 | 27 | public static deserialize(rawCommand: Buffer): StreamingDurationUpdateCommand { 28 | const props: Timecode = { 29 | hours: rawCommand.readUInt8(0), 30 | minutes: rawCommand.readUInt8(1), 31 | seconds: rawCommand.readUInt8(2), 32 | frames: rawCommand.readUInt8(3), 33 | isDropFrame: rawCommand.readUInt8(4) != 0, 34 | } 35 | 36 | return new StreamingDurationUpdateCommand(props) 37 | } 38 | 39 | public applyToState(state: AtemState): string { 40 | if (!state.streaming) { 41 | throw new InvalidIdError('Streaming') 42 | } 43 | 44 | state.streaming.duration = this.properties 45 | 46 | return `streaming.duration` 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/Streaming/StreamingServiceCommand.ts: -------------------------------------------------------------------------------- 1 | import { WritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState } from '../../state' 3 | import { StreamingServiceProperties } from '../../state/streaming' 4 | import { ProtocolVersion } from '../../enums' 5 | import { bufToNullTerminatedString } from '../../lib/atemUtil' 6 | 7 | export class StreamingServiceCommand extends WritableCommand { 8 | public static readonly rawName = 'CRSS' 9 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 10 | 11 | public static MaskFlags = { 12 | serviceName: 1 << 0, 13 | url: 1 << 1, 14 | key: 1 << 2, 15 | bitrates: 1 << 3, 16 | } 17 | 18 | public serialize(): Buffer { 19 | const buffer = Buffer.alloc(1100) 20 | buffer.writeUInt8(this.flag, 0) 21 | buffer.write(this.properties.serviceName || '', 1, 64, 'utf8') 22 | buffer.write(this.properties.url || '', 65, 512, 'utf8') 23 | buffer.write(this.properties.key || '', 577, 512, 'utf8') 24 | 25 | const bitrates = this.properties.bitrates || [0, 0] 26 | buffer.writeUInt32BE(bitrates[0], 1092) 27 | buffer.writeUInt32BE(bitrates[1], 1096) 28 | return buffer 29 | } 30 | } 31 | 32 | export class StreamingServiceUpdateCommand extends DeserializedCommand { 33 | public static readonly rawName = 'SRSU' 34 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 35 | 36 | constructor(properties: StreamingServiceProperties) { 37 | super(properties) 38 | } 39 | 40 | public static deserialize(rawCommand: Buffer): StreamingServiceUpdateCommand { 41 | const props: StreamingServiceProperties = { 42 | serviceName: bufToNullTerminatedString(rawCommand, 0, 64), 43 | url: bufToNullTerminatedString(rawCommand, 64, 512), 44 | key: bufToNullTerminatedString(rawCommand, 576, 512), 45 | bitrates: [rawCommand.readUInt32BE(1088), rawCommand.readUInt32BE(1092)], 46 | } 47 | 48 | return new StreamingServiceUpdateCommand(props) 49 | } 50 | 51 | public applyToState(state: AtemState): string { 52 | if (!state.streaming) { 53 | state.streaming = { 54 | service: this.properties, 55 | } 56 | } else { 57 | state.streaming.service = this.properties 58 | } 59 | 60 | return `streaming.service` 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/Streaming/StreamingStatsCommand.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolVersion } from '../../enums' 2 | import { InvalidIdError, AtemState } from '../../state' 3 | import { DeserializedCommand } from '../CommandBase' 4 | import { StreamingStateStats } from '../../state/streaming' 5 | 6 | export class StreamingStatsUpdateCommand extends DeserializedCommand { 7 | public static readonly rawName = 'SRSS' 8 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 9 | 10 | constructor(properties: StreamingStateStats) { 11 | super(properties) 12 | } 13 | 14 | public static deserialize(rawCommand: Buffer): StreamingStatsUpdateCommand { 15 | const props: StreamingStateStats = { 16 | encodingBitrate: rawCommand.readUInt32BE(0), 17 | cacheUsed: rawCommand.readUInt16BE(4), 18 | } 19 | 20 | return new StreamingStatsUpdateCommand(props) 21 | } 22 | 23 | public applyToState(state: AtemState): string { 24 | if (!state.streaming) { 25 | throw new InvalidIdError('Streaming') 26 | } 27 | 28 | state.streaming.stats = this.properties 29 | 30 | return `streaming.stats` 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/Streaming/StreamingStatusCommand.ts: -------------------------------------------------------------------------------- 1 | import { BasicWritableCommand, DeserializedCommand } from '../CommandBase' 2 | import { AtemState, InvalidIdError } from '../../state' 3 | import { StreamingStateStatus } from '../../state/streaming' 4 | import { StreamingError, StreamingStatus, ProtocolVersion } from '../../enums' 5 | 6 | export class StreamingStatusCommand extends BasicWritableCommand<{ streaming: boolean }> { 7 | public static readonly rawName = 'StrR' 8 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 9 | 10 | constructor(streaming: boolean) { 11 | super({ streaming }) 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.properties.streaming ? 1 : 0, 0) 17 | return buffer 18 | } 19 | } 20 | 21 | const errorEnumValues = Object.values(StreamingError).filter( 22 | (e) => typeof e === 'number' 23 | ) as unknown as number[] 24 | const statusEnumValues = Object.values(StreamingStatus).filter( 25 | (e) => typeof e === 'number' 26 | ) as unknown as number[] 27 | 28 | export class StreamingStatusUpdateCommand extends DeserializedCommand { 29 | public static readonly rawName = 'StRS' 30 | public static readonly minimumVersion = ProtocolVersion.V8_1_1 31 | 32 | constructor(properties: StreamingStateStatus) { 33 | super(properties) 34 | } 35 | 36 | public static deserialize(rawCommand: Buffer): StreamingStatusUpdateCommand { 37 | const rawStatus = rawCommand.readUInt16BE(0) 38 | 39 | let error = StreamingError.None 40 | let state = StreamingStatus.Idle 41 | 42 | for (const e of errorEnumValues) { 43 | if (e !== 0 && (rawStatus & e) === e) { 44 | error = e 45 | break 46 | } 47 | } 48 | 49 | for (const e of statusEnumValues) { 50 | if ((rawStatus & e) === e) { 51 | state = e 52 | if (e !== StreamingStatus.Streaming) break 53 | } 54 | } 55 | 56 | return new StreamingStatusUpdateCommand({ state, error }) 57 | } 58 | 59 | public applyToState(state: AtemState): string { 60 | if (!state.streaming) { 61 | throw new InvalidIdError('Streaming') 62 | } 63 | 64 | state.streaming.status = this.properties 65 | 66 | return `streaming.status` 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/Streaming/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StreamingDurationCommand' 2 | export * from './StreamingServiceCommand' 3 | export * from './StreamingStatsCommand' 4 | export * from './StreamingStatusCommand' 5 | export * from './StreamingAudioBitratesCommand' 6 | -------------------------------------------------------------------------------- /src/commands/SuperSource/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SuperSourceBoxParametersCommand' 2 | export * from './SuperSourcePropertiesCommand' 3 | -------------------------------------------------------------------------------- /src/commands/TallyBySourceCommand.ts: -------------------------------------------------------------------------------- 1 | import { DeserializedCommand } from './CommandBase' 2 | import { AtemState } from '../state' 3 | 4 | export interface TallyBySourceProps { 5 | [source: number]: { program: boolean; preview: boolean } | undefined 6 | } 7 | 8 | export class TallyBySourceCommand extends DeserializedCommand { 9 | public static readonly rawName = 'TlSr' 10 | 11 | public static deserialize(rawCommand: Buffer): TallyBySourceCommand { 12 | const sourceCount = rawCommand.readUInt16BE(0) 13 | 14 | const sources: TallyBySourceProps = {} 15 | for (let i = 0; i < sourceCount; i++) { 16 | const source = rawCommand.readUInt16BE(2 + i * 3) 17 | const value = rawCommand.readUInt8(4 + i * 3) 18 | sources[source] = { 19 | program: (value & 0x01) > 0, 20 | preview: (value & 0x02) > 0, 21 | } 22 | } 23 | 24 | return new TallyBySourceCommand(sources) 25 | } 26 | 27 | public applyToState(_state: AtemState): string[] { 28 | return [] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/TimeCommand.ts: -------------------------------------------------------------------------------- 1 | import { TimeInfo } from '../state/info' 2 | import * as Enums from '../enums' 3 | import { BasicWritableCommand } from '.' 4 | import { SymmetricalCommand } from './CommandBase' 5 | 6 | export class TimeCommand extends SymmetricalCommand { 7 | public static readonly rawName = 'Time' 8 | 9 | constructor(properties: TimeInfo | Omit) { 10 | super({ 11 | dropFrame: false, 12 | ...properties, 13 | }) 14 | } 15 | 16 | public serialize(): Buffer { 17 | const buffer = Buffer.alloc(8) 18 | buffer.writeUInt8(this.properties.hour, 0) 19 | buffer.writeUInt8(this.properties.minute, 1) 20 | buffer.writeUInt8(this.properties.second, 2) 21 | buffer.writeUInt8(this.properties.frame, 3) 22 | 23 | buffer.writeUInt8(this.properties.dropFrame ? 1 : 0, 5) 24 | 25 | return buffer 26 | } 27 | 28 | public static deserialize(rawCommand: Buffer): TimeCommand { 29 | const properties = { 30 | hour: rawCommand.readUInt8(0), 31 | minute: rawCommand.readUInt8(1), 32 | second: rawCommand.readUInt8(2), 33 | frame: rawCommand.readUInt8(3), 34 | // Byte 4 looks to be a field marker 35 | dropFrame: rawCommand.readUInt8(5) === 1, 36 | } 37 | 38 | return new TimeCommand(properties) 39 | } 40 | 41 | public applyToState(): string[] { 42 | // Not stored in the state 43 | return [] 44 | } 45 | } 46 | 47 | export class TimeRequestCommand extends BasicWritableCommand { 48 | public static readonly rawName = 'TiRq' 49 | public static readonly minimumVersion = Enums.ProtocolVersion.V8_0 50 | 51 | constructor() { 52 | super(null) 53 | } 54 | 55 | public serialize(): Buffer { 56 | const buffer = Buffer.alloc(0) 57 | return buffer 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/TimeConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import * as Enums from '../enums' 2 | import { BasicWritableCommand } from '.' 3 | import { DeserializedCommand } from './CommandBase' 4 | import { AtemState } from '../state' 5 | 6 | export class TimeConfigCommand extends BasicWritableCommand<{ mode: Enums.TimeMode }> { 7 | public static readonly rawName = 'CTCC' 8 | public static readonly minimumVersion = Enums.ProtocolVersion.V8_1_1 9 | 10 | constructor(mode: Enums.TimeMode) { 11 | super({ mode }) 12 | } 13 | 14 | public serialize(): Buffer { 15 | const buffer = Buffer.alloc(4) 16 | buffer.writeUInt8(this.properties.mode, 0) 17 | 18 | return buffer 19 | } 20 | } 21 | 22 | export class TimeConfigUpdateCommand extends DeserializedCommand<{ mode: Enums.TimeMode }> { 23 | public static readonly rawName = 'TCCc' 24 | public static readonly minimumVersion = Enums.ProtocolVersion.V8_1_1 25 | 26 | constructor(mode: Enums.TimeMode) { 27 | super({ mode }) 28 | } 29 | 30 | public static deserialize(rawCommand: Buffer): TimeConfigUpdateCommand { 31 | const mode = rawCommand.readUInt8(0) 32 | 33 | return new TimeConfigUpdateCommand(mode) 34 | } 35 | 36 | public applyToState(state: AtemState): string { 37 | state.settings.timeMode = this.properties.mode 38 | return 'settings.timeMode' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Audio' 2 | export * from './DataTransfer' 3 | export * from './DeviceProfile' 4 | export * from './DisplayClock' 5 | export * from './DownstreamKey' 6 | export * from './Fairlight' 7 | export * from './Inputs' 8 | export * from './Macro' 9 | export * from './Media' 10 | export * from './MixEffects' 11 | export * from './Recording' 12 | export * from './Settings' 13 | export * from './Streaming' 14 | export * from './SuperSource' 15 | 16 | export * from './CommandBase' 17 | export * from './AuxSourceCommand' 18 | export * from './CameraControlCommand' 19 | export * from './ColorGeneratorCommand' 20 | export * from './InitCompleteCommand' 21 | export * from './PowerStatusCommand' 22 | export * from './StartupStateCommand' 23 | export * from './TallyBySourceCommand' 24 | export * from './TimeCommand' 25 | export * from './TimeConfigCommand' 26 | -------------------------------------------------------------------------------- /src/dataTransfer/__tests__/download-macro-sequence.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "DataTransferDownloadRequestCommand", 4 | "properties": { 5 | "transferId": 0, 6 | "transferStoreId": 65535, 7 | "transferIndex": 4, 8 | "transferType": 3 9 | }, 10 | "direction": "send" 11 | }, 12 | { 13 | "name": "DataTransferDataCommand", 14 | "properties": { 15 | "transferId": 0, 16 | "body": { 17 | "bufferLength": 24 18 | } 19 | }, 20 | "direction": "recv" 21 | }, 22 | { 23 | "name": "DataTransferAckCommand", 24 | "properties": { 25 | "transferId": 0, 26 | "transferIndex": 4 27 | }, 28 | "direction": "send" 29 | }, 30 | { 31 | "name": "DataTransferCompleteCommand", 32 | "properties": { 33 | "transferId": 0 34 | }, 35 | "direction": "recv" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /src/dataTransfer/__tests__/sampleAudio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sofie-Automation/sofie-atem-connection/51c9cf8e995f74a63e50809e29db61792444df35/src/dataTransfer/__tests__/sampleAudio.wav -------------------------------------------------------------------------------- /src/dataTransfer/__tests__/upload-macro-sequence.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "DataTransferUploadRequestCommand", 4 | "properties": { 5 | "transferId": 0, 6 | "transferStoreId": 65535, 7 | "transferIndex": 4, 8 | "mode": 768, 9 | "size": 400 10 | }, 11 | "direction": "send" 12 | }, 13 | { 14 | "name": "DataTransferUploadContinueCommand", 15 | "properties": { 16 | "transferId": 0, 17 | "chunkSize": 1396, 18 | "chunkCount": 180 19 | }, 20 | "direction": "recv" 21 | }, 22 | { 23 | "name": "DataTransferFileDescriptionCommand", 24 | "properties": { 25 | "name": "test macro", 26 | "fileHash": "", 27 | "transferId": 0 28 | }, 29 | "direction": "send" 30 | }, 31 | { 32 | "name": "DataTransferDataCommand", 33 | "properties": { 34 | "transferId": 0, 35 | "body": { 36 | "bufferLength": 400 37 | } 38 | }, 39 | "direction": "send" 40 | }, 41 | { 42 | "name": "DataTransferCompleteCommand", 43 | "properties": { 44 | "transferId": 0 45 | }, 46 | "direction": "recv" 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /src/dataTransfer/dataTransfer.ts: -------------------------------------------------------------------------------- 1 | import { IDeserializedCommand, ISerializableCommand } from '../commands/CommandBase' 2 | 3 | export enum DataTransferState { 4 | /** Waiting for strt */ 5 | Pending, 6 | /** Started, waiting for first response */ 7 | Ready, 8 | /** In progress */ 9 | Transferring, 10 | /** Finished */ 11 | Finished, 12 | } 13 | 14 | export abstract class DataTransfer { 15 | readonly #completionPromise: Promise 16 | protected resolvePromise: (value: T | PromiseLike) => void 17 | protected rejectPromise: (reason?: any) => void 18 | 19 | constructor() { 20 | // Make typescript happy 21 | this.resolvePromise = (): void => { 22 | // Ignore 23 | } 24 | this.rejectPromise = (): void => { 25 | // Ignore 26 | } 27 | 28 | this.#completionPromise = new Promise((resolve, reject) => { 29 | this.resolvePromise = resolve 30 | this.rejectPromise = reject 31 | }) 32 | } 33 | 34 | /** Get the promise that will resolve upon completion/failure of the transfer */ 35 | get promise(): Promise { 36 | return this.#completionPromise 37 | } 38 | 39 | /** Start the transfer */ 40 | public abstract startTransfer(transferId: number): Promise 41 | 42 | /** Restart the current transfer */ 43 | public async restartTransfer(transferId: number): Promise { 44 | return this.startTransfer(transferId) 45 | } 46 | 47 | /** Handle a received command that is for the transfer */ 48 | public abstract handleCommand( 49 | command: IDeserializedCommand, 50 | oldState: DataTransferState 51 | ): Promise 52 | 53 | /** The current transfer has been aborted and should report failure */ 54 | public abort(reason: Error): void { 55 | this.rejectPromise(reason) 56 | } 57 | } 58 | 59 | export interface ProgressTransferResult { 60 | newState: DataTransferState 61 | commands: ISerializableCommand[] 62 | newId?: number 63 | } 64 | -------------------------------------------------------------------------------- /src/dataTransfer/dataTransferUploadAudio.ts: -------------------------------------------------------------------------------- 1 | import { ISerializableCommand } from '../commands/CommandBase' 2 | import { DataTransferUploadBuffer } from './dataTransferUploadBuffer' 3 | import { DataTransferFileDescriptionCommand, DataTransferUploadRequestCommand } from '../commands/DataTransfer' 4 | import { ProgressTransferResult, DataTransferState } from './dataTransfer' 5 | 6 | export default class DataTransferUploadAudio extends DataTransferUploadBuffer { 7 | readonly #clipIndex: number 8 | readonly #name: string 9 | 10 | constructor(clipIndex: number, data: Buffer, name: string) { 11 | super({ 12 | encodedData: data, 13 | rawDataLength: data.length, 14 | isRleEncoded: false, 15 | hash: null, 16 | }) 17 | 18 | this.#clipIndex = clipIndex 19 | this.#name = name 20 | } 21 | 22 | public async startTransfer(transferId: number): Promise { 23 | const command = new DataTransferUploadRequestCommand({ 24 | transferId: transferId, 25 | transferStoreId: this.#clipIndex + 1, 26 | transferIndex: 0, 27 | size: this.data.length, 28 | mode: 256, 29 | }) 30 | 31 | return { 32 | newState: DataTransferState.Ready, 33 | commands: [command], 34 | } 35 | } 36 | 37 | protected generateDescriptionCommand(transferId: number): ISerializableCommand { 38 | return new DataTransferFileDescriptionCommand({ 39 | name: this.#name, 40 | description: undefined, 41 | fileHash: this.hash, 42 | transferId: transferId, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/dataTransfer/dataTransferUploadMacro.ts: -------------------------------------------------------------------------------- 1 | import { DataTransferFileDescriptionCommand, DataTransferUploadRequestCommand } from '../commands/DataTransfer' 2 | import { ISerializableCommand } from '../commands/CommandBase' 3 | import { ProgressTransferResult, DataTransferState } from './dataTransfer' 4 | import { DataTransferUploadBuffer } from './dataTransferUploadBuffer' 5 | 6 | export class DataTransferUploadMacro extends DataTransferUploadBuffer { 7 | constructor(public readonly macroIndex: number, public readonly data: Buffer, private name: string) { 8 | super({ 9 | encodedData: data, 10 | rawDataLength: data.length, 11 | isRleEncoded: false, 12 | hash: null, 13 | }) 14 | } 15 | 16 | public async startTransfer(transferId: number): Promise { 17 | const command = new DataTransferUploadRequestCommand({ 18 | transferId: transferId, 19 | transferStoreId: 0xffff, 20 | transferIndex: this.macroIndex, 21 | size: this.data.length, 22 | mode: 768, 23 | }) 24 | 25 | return { 26 | newState: DataTransferState.Ready, 27 | commands: [command], 28 | } 29 | } 30 | 31 | protected generateDescriptionCommand(transferId: number): ISerializableCommand { 32 | return new DataTransferFileDescriptionCommand({ 33 | name: this.name, 34 | description: undefined, 35 | fileHash: '', 36 | transferId: transferId, 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/dataTransfer/dataTransferUploadMultiViewerLabel.ts: -------------------------------------------------------------------------------- 1 | import { ISerializableCommand } from '../commands/CommandBase' 2 | import { DataTransferUploadBuffer } from './dataTransferUploadBuffer' 3 | import { DataTransferFileDescriptionCommand, DataTransferUploadRequestCommand } from '../commands/DataTransfer' 4 | import { ProgressTransferResult, DataTransferState } from './dataTransfer' 5 | 6 | export default class DataTransferUploadMultiViewerLabel extends DataTransferUploadBuffer { 7 | readonly #sourceId: number 8 | 9 | constructor(sourceId: number, data: Buffer) { 10 | super({ 11 | encodedData: data, 12 | rawDataLength: data.length, 13 | isRleEncoded: false, 14 | hash: null, 15 | }) 16 | 17 | this.#sourceId = sourceId 18 | } 19 | 20 | public async startTransfer(transferId: number): Promise { 21 | const command = new DataTransferUploadRequestCommand({ 22 | transferId: transferId, 23 | transferStoreId: 0xffff, 24 | transferIndex: this.#sourceId, 25 | size: this.data.length, 26 | mode: 0x0201, 27 | }) 28 | 29 | return { 30 | newState: DataTransferState.Ready, 31 | commands: [command], 32 | } 33 | } 34 | 35 | protected generateDescriptionCommand(transferId: number): ISerializableCommand { 36 | return new DataTransferFileDescriptionCommand({ 37 | description: '', 38 | name: 'Label', 39 | fileHash: this.hash, 40 | transferId: transferId, 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/dataTransfer/dataTransferUploadStill.ts: -------------------------------------------------------------------------------- 1 | import { ISerializableCommand } from '../commands/CommandBase' 2 | import { DataTransferFileDescriptionCommand, DataTransferUploadRequestCommand } from '../commands/DataTransfer' 3 | import { ProgressTransferResult, DataTransferState } from './dataTransfer' 4 | import { DataTransferUploadBuffer, UploadBufferInfo } from './dataTransferUploadBuffer' 5 | 6 | export default class DataTransferUploadStill extends DataTransferUploadBuffer { 7 | readonly #stillIndex: number 8 | readonly #name: string 9 | readonly #description: string 10 | readonly #dataLength: number 11 | 12 | constructor(stillIndex: number, buffer: UploadBufferInfo, name: string, description: string) { 13 | super(buffer) 14 | 15 | this.#stillIndex = stillIndex 16 | this.#name = name 17 | this.#description = description 18 | this.#dataLength = buffer.rawDataLength 19 | } 20 | 21 | public async startTransfer(transferId: number): Promise { 22 | const command = new DataTransferUploadRequestCommand({ 23 | transferId: transferId, 24 | transferStoreId: 0, 25 | transferIndex: this.#stillIndex, 26 | size: this.#dataLength, 27 | mode: 1, 28 | }) 29 | 30 | return { 31 | newState: DataTransferState.Ready, 32 | commands: [command], 33 | } 34 | } 35 | 36 | protected generateDescriptionCommand(transferId: number): ISerializableCommand { 37 | return new DataTransferFileDescriptionCommand({ 38 | description: this.#description, 39 | name: this.#name, 40 | fileHash: this.hash, 41 | transferId: transferId, 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './atem' 2 | export * from './state' 3 | 4 | import * as Enums from './enums' 5 | import * as Commands from './commands' 6 | import * as Util from './lib/atemUtil' 7 | export { Enums, Commands, Util } 8 | export { listVisibleInputs } from './lib/tally' 9 | 10 | import * as VideoState from './state/video' 11 | import * as AudioState from './state/audio' 12 | import * as MediaState from './state/media' 13 | import * as InfoState from './state/info' 14 | import * as InputState from './state/input' 15 | import * as MacroState from './state/macro' 16 | import * as SettingsState from './state/settings' 17 | export { VideoState, AudioState, MediaState, InfoState, InputState, MacroState, SettingsState } 18 | export type { UploadStillEncodingOptions } from './dataTransfer' 19 | export type { UploadBufferInfo } from './dataTransfer/dataTransferUploadBuffer' 20 | -------------------------------------------------------------------------------- /src/lib/__mocks__/dgram.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/no-standalone-expect */ 2 | import { EventEmitter } from 'events' 3 | import { SocketType, RemoteInfo } from 'dgram' 4 | import 'jest-extended' 5 | import { DEFAULT_PORT } from '../../atem' 6 | import * as fakeTimers from '@sinonjs/fake-timers' 7 | 8 | export class Socket extends EventEmitter { 9 | public isOpen = false 10 | 11 | public expectedAddress?: string 12 | public expectedPort?: number 13 | 14 | constructor() { 15 | super() 16 | } 17 | 18 | public sendImpl?: (msg: Buffer) => void 19 | 20 | public async emitMessage(clock: fakeTimers.Clock, msg: Buffer): Promise { 21 | expect(Buffer.isBuffer(msg)).toBeTruthy() 22 | 23 | const rinfo: RemoteInfo = { 24 | address: this.expectedAddress || '127.0.0.1', 25 | port: this.expectedPort || DEFAULT_PORT, 26 | family: 'IPv4', 27 | size: msg.length, 28 | } 29 | this.emit('message', msg, rinfo) 30 | 31 | await clock.tickAsync(0) 32 | } 33 | 34 | public bind(port?: number, address?: string, callback?: () => void): void { 35 | expect(port).toBeUndefined() 36 | expect(address).toBeUndefined() 37 | expect(callback).toBeUndefined() 38 | 39 | this.isOpen = true 40 | } 41 | 42 | public send( 43 | msg: string | Uint8Array, 44 | offset: number, 45 | length: number, 46 | port: number, 47 | address?: string, 48 | callback?: (error: Error | null, bytes: number) => void 49 | ): void { 50 | expect(Buffer.isBuffer(msg)).toBeTruthy() 51 | expect(offset).toBeNumber() 52 | expect(length).toBeNumber() 53 | expect(port).toBeNumber() 54 | expect(address).toBeString() 55 | expect(callback).toBeUndefined() 56 | 57 | if (this.expectedAddress) { 58 | expect(address).toEqual(this.expectedAddress) 59 | } 60 | if (this.expectedPort) { 61 | expect(port).toEqual(this.expectedPort) 62 | } 63 | 64 | if (this.sendImpl) { 65 | this.sendImpl(msg as Buffer) 66 | } 67 | } 68 | 69 | public close(cb?: () => void): void { 70 | this.isOpen = false 71 | if (cb) cb() 72 | } 73 | } 74 | 75 | export function createSocket(type: SocketType, callback?: (msg: Buffer, rinfo: RemoteInfo) => void): Socket { 76 | expect(type).toEqual('udp4') 77 | expect(callback).toBeUndefined() 78 | 79 | return new Socket() 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/atemCommandParser.ts: -------------------------------------------------------------------------------- 1 | import * as Commands from '../commands' 2 | import { ProtocolVersion } from '../enums' 3 | 4 | type CommandConstructor = any 5 | export class CommandParser { 6 | public readonly commands: { [key: string]: Array } = {} 7 | public version: ProtocolVersion = ProtocolVersion.V7_2 // Default to the minimum supported 8 | 9 | constructor() { 10 | for (const cmd in Commands) { 11 | try { 12 | const cmdConstructor = (Commands as any)[cmd] 13 | const rawName = cmdConstructor.rawName 14 | if (rawName) { 15 | if (!this.commands[rawName]) this.commands[rawName] = [] 16 | this.commands[rawName].push(cmdConstructor) 17 | } 18 | } catch (e) { 19 | // probably not a valid command 20 | } 21 | } 22 | } 23 | 24 | public commandFromRawName(name: string): CommandConstructor | undefined { 25 | const commands = this.commands[name] 26 | if (commands) { 27 | if (!this.version) { 28 | // edge case for the version command itself: 29 | return commands[0] 30 | } else { 31 | // now we should have a version defined 32 | const baseline = commands.find((cmd) => !cmd.minimumVersion) 33 | const overrides = commands.filter((cmd) => cmd.minimumVersion && cmd.minimumVersion <= this.version) 34 | 35 | if (overrides.length === 0) return baseline 36 | 37 | let highestProtoCommand = overrides[0] 38 | 39 | for (const cmd of overrides) { 40 | // find highest version in overrides 41 | if ( 42 | highestProtoCommand.minimumVersion && 43 | cmd.minimumVersion && 44 | cmd.minimumVersion > highestProtoCommand.minimumVersion 45 | ) { 46 | highestProtoCommand = cmd 47 | } 48 | } 49 | 50 | return highestProtoCommand 51 | } 52 | } 53 | return undefined 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/converters/colorConstants.ts: -------------------------------------------------------------------------------- 1 | export interface ColorConvertConstants { 2 | readonly KR: number 3 | readonly KB: number 4 | readonly KG: number 5 | 6 | readonly KRi: number 7 | readonly KBi: number 8 | 9 | readonly YRange: number 10 | readonly CbCrRange: number 11 | readonly HalfCbCrRange: number 12 | 13 | readonly YOffset: number 14 | readonly CbCrOffset: number 15 | 16 | readonly KRKRioKG: number 17 | readonly KBKBioKG: number 18 | 19 | readonly KRoKBi: number 20 | readonly KGoKBi: number 21 | readonly KBoKRi: number 22 | readonly KGoKRi: number 23 | } 24 | 25 | function createColorConvertConstants(KR: number, KB: number): ColorConvertConstants { 26 | const KG = 1 - KR - KB 27 | 28 | const KRi = 1 - KR 29 | const KBi = 1 - KB 30 | 31 | const YRange = 219 32 | const CbCrRange = 224 33 | const HalfCbCrRange = CbCrRange / 2 34 | 35 | return { 36 | KR, 37 | KB, 38 | KG, 39 | 40 | KRi, 41 | KBi, 42 | 43 | YRange, 44 | CbCrRange, 45 | HalfCbCrRange, 46 | 47 | YOffset: 16 << 8, 48 | CbCrOffset: 128 << 8, 49 | 50 | KRKRioKG: (KR * KRi * 2) / KG, 51 | KBKBioKG: (KB * KBi * 2) / KG, 52 | 53 | KRoKBi: (KR / KBi) * HalfCbCrRange, 54 | KGoKBi: (KG / KBi) * HalfCbCrRange, 55 | KBoKRi: (KB / KRi) * HalfCbCrRange, 56 | KGoKRi: (KG / KRi) * HalfCbCrRange, 57 | } 58 | } 59 | 60 | export const ColorConvertConstantsBT709 = createColorConvertConstants(0.2126, 0.0722) 61 | export const ColorConvertConstantsBT601 = createColorConvertConstants(0.299, 0.114) 62 | -------------------------------------------------------------------------------- /src/lib/converters/rgbaToYuv422.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @todo: MINT - 2018-5-24: 3 | * Create util functions that handle proper colour spaces in UHD. 4 | */ 5 | 6 | import { ColorConvertConstantsBT709, ColorConvertConstantsBT601 } from './colorConstants' 7 | 8 | export function convertRGBAToYUV422(width: number, height: number, data: Buffer): Buffer { 9 | const constants = height >= 720 ? ColorConvertConstantsBT709 : ColorConvertConstantsBT601 10 | 11 | const genColor = (rawA: number, uv16: number, y16: number): number => { 12 | const a = ((rawA << 2) * 219) / 255 + (16 << 2) 13 | const y = Math.round(y16) >> 6 14 | const uv = Math.round(uv16) >> 6 15 | 16 | return (a << 20) + (uv << 10) + y 17 | } 18 | 19 | const buffer = Buffer.alloc(width * height * 4) 20 | for (let i = 0; i < width * height * 4; i += 8) { 21 | const r1 = data[i + 0] 22 | const g1 = data[i + 1] 23 | const b1 = data[i + 2] 24 | 25 | const r2 = data[i + 4] 26 | const g2 = data[i + 5] 27 | const b2 = data[i + 6] 28 | 29 | const a1 = data[i + 3] 30 | const a2 = data[i + 7] 31 | 32 | const y16a = 33 | constants.YOffset + 34 | constants.KR * constants.YRange * r1 + 35 | constants.KG * constants.YRange * g1 + 36 | constants.KB * constants.YRange * b1 37 | const cb16 = 38 | constants.CbCrOffset + (-constants.KRoKBi * r1 - constants.KGoKBi * g1 + constants.HalfCbCrRange * b1) 39 | const y16b = 40 | constants.YOffset + 41 | constants.KR * constants.YRange * r2 + 42 | constants.KG * constants.YRange * g2 + 43 | constants.KB * constants.YRange * b2 44 | const cr16 = 45 | constants.CbCrOffset + (constants.HalfCbCrRange * r1 - constants.KGoKRi * g1 - constants.KBoKRi * b1) 46 | 47 | buffer.writeUInt32BE(genColor(a1, cb16, y16a), i) 48 | buffer.writeUInt32BE(genColor(a2, cr16, y16b), i + 4) 49 | } 50 | return buffer 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/converters/wavAudio.ts: -------------------------------------------------------------------------------- 1 | import * as Enums from '../../enums' 2 | import WaveFile = require('wavefile') 3 | 4 | export function convertWAVToRaw(inputBuffer: Buffer, model: Enums.Model | undefined): Buffer { 5 | const wav = new (WaveFile as any)(inputBuffer) 6 | 7 | if (wav.fmt.bitsPerSample !== 24) { 8 | throw new Error(`Invalid wav bit bits per sample: ${wav.fmt.bitsPerSample}`) 9 | } 10 | 11 | if (wav.fmt.numChannels !== 2) { 12 | throw new Error(`Invalid number of wav channels: ${wav.fmt.numChannel}`) 13 | } 14 | 15 | const buffer = Buffer.from(wav.data.samples) 16 | const buffer2 = Buffer.alloc(buffer.length) 17 | for (let i = 0; i < buffer.length; i += 3) { 18 | // 24bit samples, change endian from wavfile to atem requirements 19 | buffer2.writeUIntBE(buffer.readUIntLE(i, 3), i, 3) 20 | } 21 | 22 | if (model === undefined || model >= Enums.Model.PS4K) { 23 | // If we don't know the model, assume we want the newer mode as that is more likely 24 | // Newer models want a weird byte order 25 | const buffer3 = Buffer.alloc(buffer2.length) 26 | for (let i = 0; i < buffer.length; i += 4) { 27 | buffer3.writeUIntBE(buffer2.readUIntLE(i, 4), i, 4) 28 | } 29 | 30 | return buffer3 31 | } else { 32 | return buffer2 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/converters/yuv422ToRgba.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @todo: MINT - 2018-5-24: 3 | * Create util functions that handle proper colour spaces in UHD. 4 | */ 5 | 6 | import { ColorConvertConstantsBT709, ColorConvertConstantsBT601 } from './colorConstants' 7 | 8 | function clamp(v: number) { 9 | if (v <= 0) return 0 10 | if (v >= 255) return 255 11 | return v 12 | } 13 | 14 | export function convertYUV422ToRGBA(width: number, height: number, data: Buffer): Buffer { 15 | const constants = height >= 720 ? ColorConvertConstantsBT709 : ColorConvertConstantsBT601 16 | 17 | const splitSample = (raw: number): [y: number, uv: number, a: number] => { 18 | const y = (raw & 0x000003ff) << 6 19 | const uv = (raw & 0x000ffc00) >> 4 // same as << 6 >> 10 20 | const a = (raw & 0x3ff00000) >> 20 21 | 22 | const y_full = (y - constants.YOffset) / constants.YRange 23 | const uv_full = (uv - constants.CbCrOffset) / constants.HalfCbCrRange 24 | const a_full = (((a - (16 << 2)) / 219) * 255) >> 2 // TODO - confirm correct range 25 | 26 | return [y_full, uv_full, a_full] 27 | } 28 | 29 | const genColor = ( 30 | y8: number, 31 | cb8: number, 32 | cr8: number, 33 | a10: number 34 | ): [r: number, g: number, b: number, a: number] => { 35 | const r = clamp(Math.round(y8 + constants.KRi * cr8)) 36 | const g = clamp(Math.round(y8 - constants.KRKRioKG * cr8 - constants.KBKBioKG * cb8)) 37 | const b = clamp(Math.round(y8 + constants.KBi * cb8)) 38 | const a = Math.round(a10) 39 | 40 | return [r, g, b, a] 41 | } 42 | 43 | const buffer = Buffer.alloc(width * height * 4) 44 | for (let i = 0; i < width * height * 4; i += 8) { 45 | const sample1 = data.readUint32BE(i) 46 | const sample2 = data.readUint32BE(i + 4) 47 | 48 | const [y8a, cb8, a10a] = splitSample(sample1) 49 | const [y8b, cr8, a10b] = splitSample(sample2) 50 | 51 | const [r1, g1, b1, a1] = genColor(y8a, cb8, cr8, a10a) 52 | const [r2, g2, b2, a2] = genColor(y8b, cb8, cr8, a10b) 53 | 54 | buffer.writeUint8(r1, i) 55 | buffer.writeUint8(g1, i + 1) 56 | buffer.writeUint8(b1, i + 2) 57 | buffer.writeUint8(a1, i + 3) 58 | buffer.writeUint8(r2, i + 4) 59 | buffer.writeUint8(g2, i + 5) 60 | buffer.writeUint8(b2, i + 6) 61 | buffer.writeUint8(a2, i + 7) 62 | } 63 | return buffer 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | 3 | /** Copied from https://github.com/piotrwitek/utility-types/blob/2ae7412a9edf12f34fedbf594facf43cf04f7e32/src/mapped-types.ts#L112 */ 4 | 5 | /** 6 | * MutableKeys 7 | * @desc Get union type of keys that are mutable in object type `T` 8 | * Credit: Matt McCutchen 9 | * https://stackoverflow.com/questions/52443276/how-to-exclude-getter-only-properties-from-type-in-typescript 10 | * @example 11 | * type Props = { readonly foo: string; bar: number }; 12 | * 13 | * // Expect: "bar" 14 | * type Keys = MutableKeys; 15 | */ 16 | type MutableKeys = { 17 | [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P> 18 | }[keyof T] 19 | 20 | type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B 21 | 22 | export type OmitReadonly = Pick> 23 | 24 | export type Mutable = { 25 | -readonly [P in keyof T]: T[P] 26 | } 27 | -------------------------------------------------------------------------------- /src/state/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioSourceType, ExternalPortType, AudioMixOption } from '../enums' 2 | 3 | export type AudioChannel = ClassicAudioChannel 4 | export type AudioMasterChannel = ClassicAudioMasterChannel 5 | export type AtemAudioState = AtemClassicAudioState 6 | 7 | export interface ClassicAudioChannel { 8 | readonly sourceType: AudioSourceType 9 | portType: ExternalPortType 10 | mixOption: AudioMixOption 11 | /** Gain in decibel, -Infinity to +6dB */ 12 | gain: number 13 | /** Balance, -50 to +50 */ 14 | balance: number 15 | 16 | readonly supportsRcaToXlrEnabled: boolean 17 | rcaToXlrEnabled: boolean 18 | } 19 | 20 | export interface ClassicAudioMasterChannel { 21 | /** Gain in decibel, -Infinity to +6dB */ 22 | gain: number 23 | /** Balance, -50 to +50 */ 24 | balance: number 25 | followFadeToBlack: boolean 26 | } 27 | 28 | export interface ClassicAudioMonitorChannel { 29 | enabled: boolean 30 | /** Gain in decibel, -Infinity to +6dB */ 31 | gain: number 32 | 33 | mute: boolean 34 | 35 | solo: boolean 36 | soloSource: number 37 | 38 | dim: boolean 39 | dimLevel: number 40 | } 41 | 42 | export interface ClassicAudioHeadphoneOutputChannel { 43 | /** Gain in decibel, -Infinity to ?dB */ 44 | gain: number 45 | /** Gain in decibel, -Infinity to ?dB */ 46 | programOutGain: number 47 | /** Gain in decibel, -Infinity to ?dB */ 48 | sidetoneGain: number 49 | /** Gain in decibel, -Infinity to ?dB */ 50 | talkbackGain: number 51 | } 52 | 53 | export interface AtemClassicAudioState { 54 | readonly numberOfChannels?: number 55 | readonly hasMonitor?: boolean 56 | channels: { [channelId: number]: ClassicAudioChannel | undefined } 57 | monitor?: ClassicAudioMonitorChannel 58 | headphones?: ClassicAudioHeadphoneOutputChannel 59 | master?: ClassicAudioMasterChannel 60 | 61 | audioFollowVideoCrossfadeTransitionEnabled?: boolean 62 | } 63 | -------------------------------------------------------------------------------- /src/state/color.ts: -------------------------------------------------------------------------------- 1 | export interface ColorGeneratorState { 2 | hue: number 3 | saturation: number 4 | luma: number 5 | } 6 | -------------------------------------------------------------------------------- /src/state/common.ts: -------------------------------------------------------------------------------- 1 | export interface Timecode { 2 | hours: number 3 | minutes: number 4 | seconds: number 5 | frames: number 6 | isDropFrame: boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/state/displayClock.ts: -------------------------------------------------------------------------------- 1 | import { DisplayClockClockMode, DisplayClockClockState } from '../enums' 2 | 3 | export interface DisplayClockTime { 4 | hours: number 5 | minutes: number 6 | seconds: number 7 | frames: number 8 | } 9 | 10 | export interface DisplayClockProperties { 11 | enabled: boolean 12 | size: number 13 | opacity: number 14 | positionX: number 15 | positionY: number 16 | autoHide: boolean 17 | startFrom: DisplayClockTime 18 | clockMode: DisplayClockClockMode 19 | clockState: DisplayClockClockState 20 | } 21 | 22 | export interface DisplayClockState { 23 | properties: DisplayClockProperties 24 | 25 | /** 26 | * Note: this is only updated following a call to DisplayClockRequestTime 27 | */ 28 | currentTime: DisplayClockTime 29 | } 30 | -------------------------------------------------------------------------------- /src/state/index.ts: -------------------------------------------------------------------------------- 1 | import * as Info from './info' 2 | import * as Video from './video' 3 | import * as ClassicAudio from './audio' 4 | import * as Media from './media' 5 | import * as Input from './input' 6 | import * as Macro from './macro' 7 | import * as Settings from './settings' 8 | import * as Recording from './recording' 9 | import * as Streaming from './streaming' 10 | import * as Fairlight from './fairlight' 11 | import * as DisplayClock from './displayClock' 12 | import * as AtemStateUtil from './util' 13 | import { ColorGeneratorState } from './color' 14 | 15 | export { 16 | AtemStateUtil, 17 | Info, 18 | Video, 19 | ClassicAudio, 20 | Media, 21 | Input, 22 | Macro, 23 | Settings, 24 | Recording, 25 | Streaming, 26 | Fairlight, 27 | DisplayClock, 28 | ColorGeneratorState, 29 | } 30 | 31 | export interface AtemState { 32 | info: Info.DeviceInfo 33 | video: Video.AtemVideoState 34 | audio?: ClassicAudio.AtemClassicAudioState 35 | fairlight?: Fairlight.AtemFairlightAudioState 36 | media: Media.MediaState 37 | inputs: { [inputId: number]: Input.InputChannel | undefined } 38 | macro: Macro.MacroState 39 | settings: Settings.SettingsState 40 | recording?: Recording.RecordingState 41 | streaming?: Streaming.StreamingState 42 | colorGenerators?: { [index: number]: ColorGeneratorState | undefined } 43 | displayClock?: DisplayClock.DisplayClockState 44 | } 45 | 46 | export class InvalidIdError extends Error { 47 | constructor(message: string, ...ids: Array) { 48 | super(InvalidIdError.BuildErrorString(message, ids)) 49 | Object.setPrototypeOf(this, new.target.prototype) 50 | } 51 | 52 | private static BuildErrorString(message: string, ids: Array): string { 53 | if (ids && ids.length > 0) { 54 | return `${message} ${ids.join('-')} is not valid` 55 | } else { 56 | return message 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/state/info.ts: -------------------------------------------------------------------------------- 1 | import { Model, ProtocolVersion, VideoMode } from '../enums' 2 | 3 | export interface AtemCapabilites { 4 | readonly mixEffects: number 5 | readonly sources: number 6 | readonly auxilliaries: number 7 | readonly mixMinusOutputs: number 8 | readonly mediaPlayers: number 9 | readonly serialPorts: number 10 | readonly maxHyperdecks: number 11 | readonly DVEs: number 12 | readonly stingers: number 13 | readonly superSources: number 14 | readonly talkbackChannels: number 15 | readonly downstreamKeyers: number 16 | readonly cameraControl: boolean 17 | readonly advancedChromaKeyers: boolean 18 | readonly onlyConfigurableOutputs: boolean 19 | } 20 | 21 | export interface MixEffectInfo { 22 | readonly keyCount: number 23 | } 24 | 25 | export interface SuperSourceInfo { 26 | readonly boxCount: number 27 | } 28 | 29 | export interface AudioMixerInfo { 30 | readonly inputs: number 31 | readonly monitors: number 32 | readonly headphones: number 33 | } 34 | 35 | export interface FairlightAudioMixerInfo { 36 | readonly inputs: number 37 | readonly monitors: number 38 | } 39 | 40 | export interface MacroPoolInfo { 41 | readonly macroCount: number 42 | } 43 | 44 | export interface MediaPoolInfo { 45 | readonly stillCount: number 46 | readonly clipCount: number 47 | } 48 | 49 | export interface MultiviewerInfo { 50 | readonly count: number 51 | readonly windowCount: number 52 | } 53 | 54 | export interface TimeInfo { 55 | hour: number 56 | minute: number 57 | second: number 58 | frame: number 59 | dropFrame: boolean 60 | } 61 | 62 | export interface DeviceInfo { 63 | apiVersion: ProtocolVersion 64 | capabilities?: AtemCapabilites 65 | model: Model 66 | productIdentifier?: string 67 | superSources: Array 68 | mixEffects: Array 69 | power: boolean[] 70 | audioMixer?: AudioMixerInfo 71 | fairlightMixer?: FairlightAudioMixerInfo 72 | macroPool?: MacroPoolInfo 73 | mediaPool?: MediaPoolInfo 74 | multiviewer?: MultiviewerInfo 75 | // lastTime?: TimeInfo 76 | supportedVideoModes?: Readonly> 77 | } 78 | 79 | export interface SupportedVideoMode { 80 | mode: VideoMode 81 | 82 | requiresReconfig: boolean 83 | 84 | multiviewerModes: Array 85 | downConvertModes: Array 86 | } 87 | -------------------------------------------------------------------------------- /src/state/input.ts: -------------------------------------------------------------------------------- 1 | import { ExternalPortType, InternalPortType, MeAvailability, SourceAvailability } from '../enums' 2 | 3 | export interface InputChannel { 4 | readonly inputId: number 5 | longName: string 6 | shortName: string 7 | readonly areNamesDefault: boolean 8 | readonly externalPorts: Array | null 9 | externalPortType: ExternalPortType 10 | readonly internalPortType: InternalPortType 11 | readonly sourceAvailability: SourceAvailability 12 | readonly meAvailability: MeAvailability 13 | } 14 | -------------------------------------------------------------------------------- /src/state/levels.ts: -------------------------------------------------------------------------------- 1 | export type SomeAtemAudioLevels = FairlightAudioSourceLevels | FairlightAudioMasterLevels 2 | 3 | export interface AtemAudioLevelsBase { 4 | system: 'fairlight' 5 | type: T 6 | } 7 | export interface FairlightAudioSourceLevels extends AtemAudioLevelsBase<'source'> { 8 | index: number 9 | source: bigint 10 | 11 | levels: FairlightAudioLevels 12 | } 13 | 14 | export interface FairlightAudioMasterLevels extends AtemAudioLevelsBase<'master'> { 15 | levels: Omit 16 | } 17 | 18 | export interface FairlightAudioLevels { 19 | inputLeftLevel: number 20 | inputRightLevel: number 21 | inputLeftPeak: number 22 | inputRightPeak: number 23 | 24 | expanderGainReduction: number 25 | compressorGainReduction: number 26 | limiterGainReduction: number 27 | 28 | outputLeftLevel: number 29 | outputRightLevel: number 30 | outputLeftPeak: number 31 | outputRightPeak: number 32 | 33 | leftLevel: number 34 | rightLevel: number 35 | leftPeak: number 36 | rightPeak: number 37 | } 38 | -------------------------------------------------------------------------------- /src/state/macro.ts: -------------------------------------------------------------------------------- 1 | export interface MacroPlayerState { 2 | isRunning: boolean 3 | isWaiting: boolean 4 | loop: boolean 5 | macroIndex: number 6 | } 7 | 8 | export interface MacroRecorderState { 9 | isRecording: boolean 10 | macroIndex: number 11 | } 12 | 13 | export interface MacroPropertiesState { 14 | readonly isUsed: boolean 15 | readonly hasUnsupportedOps: boolean 16 | name: string 17 | description: string 18 | } 19 | 20 | export interface MacroState { 21 | macroPlayer: MacroPlayerState 22 | macroRecorder: MacroRecorderState 23 | readonly macroProperties: Array 24 | } 25 | -------------------------------------------------------------------------------- /src/state/media.ts: -------------------------------------------------------------------------------- 1 | import * as Enums from '../enums' 2 | 3 | export interface MediaPlayer { 4 | playing: boolean 5 | loop: boolean 6 | atBeginning: boolean 7 | clipFrame: number 8 | } 9 | 10 | export interface MediaPlayerSource { 11 | sourceType: Enums.MediaSourceType 12 | clipIndex: number 13 | stillIndex: number 14 | } 15 | 16 | export type MediaPlayerState = MediaPlayer & MediaPlayerSource 17 | 18 | export interface MediaState { 19 | readonly stillPool: Array 20 | readonly clipPool: Array 21 | readonly players: Array 22 | } 23 | 24 | export interface StillFrame { 25 | isUsed: boolean 26 | hash: string 27 | fileName: string 28 | } 29 | 30 | export interface ClipBank { 31 | isUsed: boolean 32 | name: string 33 | frameCount: number 34 | frames: Array 35 | } 36 | -------------------------------------------------------------------------------- /src/state/recording.ts: -------------------------------------------------------------------------------- 1 | import { RecordingError, RecordingStatus, RecordingDiskStatus } from '../enums' 2 | import { Timecode } from './common' 3 | 4 | export interface RecordingState { 5 | status?: RecordingStateStatus 6 | properties: RecordingStateProperties 7 | 8 | recordAllInputs?: boolean 9 | 10 | duration?: Timecode 11 | 12 | disks: { [id: number]: RecordingDiskProperties | undefined } 13 | } 14 | 15 | export interface RecordingDiskProperties { 16 | diskId: number 17 | volumeName: string 18 | recordingTimeAvailable: number 19 | status: RecordingDiskStatus 20 | } 21 | 22 | export interface RecordingStateStatus { 23 | state: RecordingStatus 24 | error: RecordingError 25 | 26 | recordingTimeAvailable: number 27 | } 28 | 29 | export interface RecordingStateProperties { 30 | filename: string 31 | 32 | workingSet1DiskId: number 33 | workingSet2DiskId: number 34 | 35 | recordInAllCameras: boolean 36 | } 37 | -------------------------------------------------------------------------------- /src/state/settings.ts: -------------------------------------------------------------------------------- 1 | import { VideoMode, MultiViewerLayout, TimeMode } from '../enums' 2 | 3 | export interface MultiViewerSourceState { 4 | source: number 5 | readonly windowIndex: number 6 | readonly supportsVuMeter: boolean 7 | readonly supportsSafeArea: boolean 8 | } 9 | 10 | export interface MultiViewerWindowState extends MultiViewerSourceState { 11 | safeTitle?: boolean 12 | audioMeter?: boolean 13 | } 14 | 15 | export interface MultiViewerPropertiesState { 16 | layout: MultiViewerLayout 17 | programPreviewSwapped: boolean 18 | } 19 | 20 | export interface MultiViewer { 21 | readonly index: number 22 | readonly windows: Array 23 | properties?: MultiViewerPropertiesState 24 | vuOpacity?: number 25 | } 26 | 27 | export interface MediaPool { 28 | maxFrames: number[] 29 | unassignedFrames: number 30 | } 31 | 32 | export interface SettingsState { 33 | readonly multiViewers: Array 34 | videoMode: VideoMode 35 | mediaPool?: MediaPool 36 | timeMode?: TimeMode 37 | } 38 | -------------------------------------------------------------------------------- /src/state/streaming.ts: -------------------------------------------------------------------------------- 1 | import { StreamingError, StreamingStatus } from '../enums' 2 | import { Timecode } from './common' 3 | 4 | export interface StreamingState { 5 | status?: StreamingStateStatus 6 | stats?: StreamingStateStats 7 | service: StreamingServiceProperties 8 | 9 | duration?: Timecode 10 | 11 | audioBitrates?: StreamingAudioBitrates 12 | } 13 | 14 | export interface StreamingStateStatus { 15 | readonly state: StreamingStatus 16 | readonly error: StreamingError 17 | } 18 | 19 | export interface StreamingStateStats { 20 | readonly cacheUsed: number 21 | readonly encodingBitrate: number 22 | } 23 | 24 | export interface StreamingServiceProperties { 25 | serviceName: string 26 | url: string 27 | key: string 28 | 29 | bitrates: [number, number] 30 | } 31 | 32 | export interface StreamingAudioBitrates { 33 | lowBitrate: number 34 | highBitrate: number 35 | } 36 | -------------------------------------------------------------------------------- /src/state/video/downstreamKeyers.ts: -------------------------------------------------------------------------------- 1 | export interface DownstreamKeyerBase { 2 | readonly inTransition: boolean 3 | readonly remainingFrames: number 4 | readonly isAuto: boolean 5 | onAir: boolean 6 | isTowardsOnAir?: boolean 7 | } 8 | 9 | export interface DownstreamKeyer extends DownstreamKeyerBase { 10 | sources?: DownstreamKeyerSources 11 | properties?: DownstreamKeyerProperties 12 | } 13 | 14 | export interface DownstreamKeyerGeneral { 15 | preMultiply: boolean 16 | clip: number 17 | gain: number 18 | invert: boolean 19 | } 20 | 21 | export interface DownstreamKeyerMask { 22 | enabled: boolean 23 | top: number 24 | bottom: number 25 | left: number 26 | right: number 27 | } 28 | 29 | export interface DownstreamKeyerProperties extends DownstreamKeyerGeneral { 30 | tie: boolean 31 | rate: number 32 | mask: DownstreamKeyerMask 33 | } 34 | 35 | export interface DownstreamKeyerSources { 36 | fillSource: number 37 | cutSource: number 38 | } 39 | -------------------------------------------------------------------------------- /src/state/video/superSource.ts: -------------------------------------------------------------------------------- 1 | import * as Enum from '../../enums' 2 | 3 | export interface SuperSourceBox { 4 | enabled: boolean 5 | source: number 6 | x: number 7 | y: number 8 | size: number 9 | cropped: boolean 10 | cropTop: number 11 | cropBottom: number 12 | cropLeft: number 13 | cropRight: number 14 | } 15 | 16 | export interface SuperSourceProperties { 17 | artFillSource: number 18 | artCutSource: number 19 | artOption: Enum.SuperSourceArtOption 20 | artPreMultiplied: boolean 21 | artClip: number 22 | artGain: number 23 | artInvertKey: boolean 24 | } 25 | 26 | export interface SuperSourceBorder { 27 | borderEnabled: boolean 28 | borderBevel: Enum.BorderBevel 29 | borderOuterWidth: number 30 | borderInnerWidth: number 31 | borderOuterSoftness: number 32 | borderInnerSoftness: number 33 | borderBevelSoftness: number 34 | borderBevelPosition: number 35 | borderHue: number 36 | borderSaturation: number 37 | borderLuma: number 38 | borderLightSourceDirection: number 39 | borderLightSourceAltitude: number 40 | } 41 | 42 | export interface SuperSource { 43 | readonly index: number 44 | readonly boxes: [ 45 | SuperSourceBox | undefined, 46 | SuperSourceBox | undefined, 47 | SuperSourceBox | undefined, 48 | SuperSourceBox | undefined 49 | ] 50 | properties?: SuperSourceProperties 51 | border?: SuperSourceBorder 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.lib", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*", "scratch/**"], 5 | "compilerOptions": { 6 | "outDir": "./dist", 7 | "baseUrl": "./", 8 | "paths": { 9 | "*": ["./node_modules/*"], 10 | "atem-connection": ["./src/index.ts"] 11 | }, 12 | "types": ["node"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "exclude": ["examples/**"], 4 | "compilerOptions": { 5 | "types": ["node", "jest"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "include": ["src/**/*.ts", "examples/**/*.ts"], 4 | "exclude": ["node_modules/**", "scratch/**"], 5 | "compilerOptions": { 6 | "types": ["jest", "node"] 7 | } 8 | } 9 | --------------------------------------------------------------------------------