diff --git a/package.json b/package.json index b32ee3296..6a3ce17b1 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "symlink-dir": "^5.1.1", "ts-jest": "^29.1.0", "ts-node": "^8.10.2", - "type-fest": "^3.10.0", "typedoc": "^0.23.28", "typescript": "~4.9.5" }, diff --git a/packages/timeline-state-resolver/package.json b/packages/timeline-state-resolver/package.json index 989aba5ec..6d82ce0c0 100644 --- a/packages/timeline-state-resolver/package.json +++ b/packages/timeline-state-resolver/package.json @@ -89,8 +89,8 @@ ], "dependencies": { "@tv2media/v-connection": "^7.3.0", - "atem-connection": "2.5.0", - "atem-state": "0.13.0", + "atem-connection": "3.3.2", + "atem-state": "1.1.0", "cacheable-lookup": "^5.0.3", "casparcg-connection": "^6.0.6", "casparcg-state": "^3.0.2", @@ -113,8 +113,8 @@ "timeline-state-resolver-types": "9.1.0-release51", "tslib": "^2.5.1", "tv-automation-quantel-gateway-client": "^3.1.7", + "type-fest": "^3.10.0", "underscore": "^1.13.6", - "underscore-deep-extend": "^1.1.5", "utf-8-validate": "^5.0.10", "ws": "^7.5.9", "xml-js": "^1.6.11" diff --git a/packages/timeline-state-resolver/src/__mocks__/_setup-all-mocks.ts b/packages/timeline-state-resolver/src/__mocks__/_setup-all-mocks.ts index 5d922dacc..750fba652 100644 --- a/packages/timeline-state-resolver/src/__mocks__/_setup-all-mocks.ts +++ b/packages/timeline-state-resolver/src/__mocks__/_setup-all-mocks.ts @@ -1,4 +1,4 @@ -import * as atemState from './atem-state' +import * as atemConnection from './atem-connection' import * as casparcgConnection from './casparcg-connection' import * as emberplusConnection from './emberplus-connection' import * as emberplus from './emberplus' @@ -14,7 +14,7 @@ import * as ws from './ws' // does not work properly and need to be set up like this.. export function setupAllMocks() { - jest.mock('atem-state', () => atemState) + jest.mock('atem-connection', () => atemConnection) jest.mock('casparcg-connection', () => casparcgConnection) jest.mock('emberplus-connection', () => emberplusConnection) jest.mock('emberplus', () => emberplus) diff --git a/packages/timeline-state-resolver/src/__mocks__/atem-connection.ts b/packages/timeline-state-resolver/src/__mocks__/atem-connection.ts new file mode 100644 index 000000000..43fc4b6ef --- /dev/null +++ b/packages/timeline-state-resolver/src/__mocks__/atem-connection.ts @@ -0,0 +1,45 @@ +export * from 'atem-connection' +import * as OrigAtemConnection from 'atem-connection' +import { EventEmitter } from 'events' + +const setTimeoutOrg = setTimeout + +// @ts-ignore separate declarations +export class BasicAtem extends EventEmitter implements OrigAtemConnection.BasicAtem { + constructor(_options?: OrigAtemConnection.AtemOptions) { + super() + } + get state(): OrigAtemConnection.AtemState { + return OrigAtemConnection.AtemStateUtil.Create() + } + async connect(): Promise { + setTimeoutOrg(() => { + this.emit('connected') + }, 10) + + return new Promise((resolve) => { + setTimeoutOrg(() => { + resolve() + }, 5) + }) + } + async disconnect(): Promise { + return new Promise((resolve) => { + setTimeoutOrg(() => { + resolve() + }, 10) + }) + } + + async destroy(): Promise { + return new Promise((resolve) => { + setTimeoutOrg(() => { + resolve() + }, 10) + }) + } + + async sendCommand(): Promise { + return Promise.resolve() + } +} diff --git a/packages/timeline-state-resolver/src/__mocks__/atem-state.ts b/packages/timeline-state-resolver/src/__mocks__/atem-state.ts deleted file mode 100644 index e0fbedb59..000000000 --- a/packages/timeline-state-resolver/src/__mocks__/atem-state.ts +++ /dev/null @@ -1,49 +0,0 @@ -export * from 'atem-state' -import * as OrigAtemConnection from 'atem-connection' -import { EventEmitter } from 'events' - -const setTimeoutOrg = setTimeout - -// @ts-ignore separate declarations -export { OrigAtemConnection as AtemConnection } -export namespace AtemConnection { - // @ts-ignore separate declarations - export class BasicAtem extends EventEmitter implements OrigAtemConnection.BasicAtem { - constructor(_options?: OrigAtemConnection.AtemOptions) { - super() - } - get state(): OrigAtemConnection.AtemState { - return OrigAtemConnection.AtemStateUtil.Create() - } - async connect(): Promise { - setTimeoutOrg(() => { - this.emit('connected') - }, 10) - - return new Promise((resolve) => { - setTimeoutOrg(() => { - resolve() - }, 5) - }) - } - async disconnect(): Promise { - return new Promise((resolve) => { - setTimeoutOrg(() => { - resolve() - }, 10) - }) - } - - async destroy(): Promise { - return new Promise((resolve) => { - setTimeoutOrg(() => { - resolve() - }, 10) - }) - } - - async sendCommand(): Promise { - return Promise.resolve() - } - } -} diff --git a/packages/timeline-state-resolver/src/integrations/atem/__tests__/__snapshots__/diffStates.spec.ts.snap b/packages/timeline-state-resolver/src/integrations/atem/__tests__/__snapshots__/diffStates.spec.ts.snap new file mode 100644 index 000000000..bd6b6e6fc --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/atem/__tests__/__snapshots__/diffStates.spec.ts.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Diff States Simple diff against empty state 1`] = ` +{ + "audio": { + "classic": undefined, + "fairlight": { + "audioRouting": { + "outputs": { + "default": undefined, + }, + "sources": undefined, + }, + "crossfade": undefined, + "inputs": undefined, + "masterOutput": undefined, + "monitorOutput": undefined, + }, + }, + "colorGenerators": undefined, + "macros": { + "player": { + "player": true, + }, + }, + "media": { + "players": { + "source": true, + "status": true, + }, + }, + "settings": { + "multiviewer": undefined, + }, + "video": { + "auxiliaries": [], + "downstreamKeyers": { + "mask": true, + "onAir": true, + "properties": true, + "sources": true, + }, + "mixEffects": { + "programPreview": true, + "transitionProperties": true, + "transitionSettings": { + "DVE": false, + "dip": false, + "mix": true, + "stinger": false, + "wipe": true, + }, + "transitionStatus": true, + "upstreamKeyers": { + "advancedChromaSettings": false, + "chromaSettings": false, + "dveSettings": true, + "flyKeyframes": undefined, + "lumaSettings": true, + "mask": true, + "onAir": true, + "patternSettings": true, + "sources": true, + "type": true, + }, + }, + "superSources": { + "border": true, + "boxes": "all", + "properties": true, + }, + }, +} +`; diff --git a/packages/timeline-state-resolver/src/integrations/atem/__tests__/diffStates.spec.ts b/packages/timeline-state-resolver/src/integrations/atem/__tests__/diffStates.spec.ts index b69acb345..82f9b80e2 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/__tests__/diffStates.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/__tests__/diffStates.spec.ts @@ -1,5 +1,5 @@ import * as AtemConnection from 'atem-connection' -import { compareAtemCommands, createDevice } from './util' +import { compareAtemCommands, createDevice, expectIncludesAtemCommandName } from './util' import { AtemTransitionStyle, DeviceType, @@ -8,16 +8,38 @@ import { MappingAtemType, Mappings, } from 'timeline-state-resolver-types' +import { AtemState } from 'atem-state' +import { createDiffOptions } from '../diffState' + +const diffStatesSpy = jest.spyOn(AtemState, 'diffStates') describe('Diff States', () => { + beforeEach(() => { + diffStatesSpy.mockClear() + }) + test('Simple diff against empty state', async () => { const device = await createDevice() const state1 = AtemConnection.AtemStateUtil.Create() AtemConnection.AtemStateUtil.getMixEffect(state1, 0).programInput = 2 + const diffOptions = createDiffOptions({}) + expect(diffOptions).toMatchSnapshot() + + expect(diffStatesSpy).toHaveBeenCalledTimes(0) + const commands = device.diffStates(undefined, state1, {}) + expect(diffStatesSpy).toHaveBeenCalledTimes(1) + expect(diffStatesSpy).toHaveBeenNthCalledWith( + 1, + expect.anything(), + AtemConnection.AtemStateUtil.Create(), + state1, + diffOptions + ) + expect(commands).toHaveLength(1) compareAtemCommands(commands[0].command, new AtemConnection.Commands.ProgramInputCommand(0, 2)) }) @@ -30,8 +52,15 @@ describe('Diff States', () => { const state2 = AtemConnection.AtemStateUtil.Create() AtemConnection.AtemStateUtil.getMixEffect(state2, 0).programInput = 3 + const diffOptions = createDiffOptions({}) + + expect(diffStatesSpy).toHaveBeenCalledTimes(0) + const commands = device.diffStates(state1, state2, {}) + expect(diffStatesSpy).toHaveBeenCalledTimes(1) + expect(diffStatesSpy).toHaveBeenNthCalledWith(1, expect.anything(), state1, state2, diffOptions) + expect(commands).toHaveLength(1) compareAtemCommands(commands[0].command, new AtemConnection.Commands.ProgramInputCommand(0, 3)) }) @@ -42,8 +71,22 @@ describe('Diff States', () => { const state1 = AtemConnection.AtemStateUtil.Create() state1.video.auxilliaries[5] = 10 + const diffOptions = createDiffOptions({}) + expect(diffOptions.video?.auxiliaries).toStrictEqual([]) + + expect(diffStatesSpy).toHaveBeenCalledTimes(0) + const commands = device.diffStates(undefined, state1, {}) + expect(diffStatesSpy).toHaveBeenCalledTimes(1) + expect(diffStatesSpy).toHaveBeenNthCalledWith( + 1, + expect.anything(), + AtemConnection.AtemStateUtil.Create(), + state1, + diffOptions + ) + expect(commands).toHaveLength(0) }) @@ -64,8 +107,22 @@ describe('Diff States', () => { }, } + const diffOptions = createDiffOptions(mappings) + expect(diffOptions.video?.auxiliaries).toStrictEqual([5]) + + expect(diffStatesSpy).toHaveBeenCalledTimes(0) + const commands = device.diffStates(undefined, state1, mappings) + expect(diffStatesSpy).toHaveBeenCalledTimes(1) + expect(diffStatesSpy).toHaveBeenNthCalledWith( + 1, + expect.anything(), + AtemConnection.AtemStateUtil.Create(), + state1, + diffOptions + ) + expect(commands).toHaveLength(1) compareAtemCommands(commands[0].command, new AtemConnection.Commands.AuxSourceCommand(5, 10)) }) @@ -94,8 +151,7 @@ describe('Diff States', () => { const commands = device.diffStates(undefined, deviceState1, mappings) expect(commands).toHaveLength(2) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.PreviewInputCommand(0, 2)) - compareAtemCommands(commands[1].command, new AtemConnection.Commands.CutCommand(0)) + expectIncludesAtemCommandName(commands, AtemConnection.Commands.CutCommand.name) } const deviceState2 = AtemConnection.AtemStateUtil.Create() @@ -108,13 +164,8 @@ describe('Diff States', () => { const commands = device.diffStates(deviceState1, deviceState2, mappings) expect(commands).toHaveLength(4) - const transitionPropertiesCommand = new AtemConnection.Commands.TransitionPropertiesCommand(0) - transitionPropertiesCommand.updateProps({ nextStyle: 1 }) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.PreviewInputCommand(0, 3)) - compareAtemCommands(commands[1].command, transitionPropertiesCommand) - compareAtemCommands(commands[2].command, new AtemConnection.Commands.TransitionPositionCommand(0, 0)) - compareAtemCommands(commands[3].command, new AtemConnection.Commands.AutoTransitionCommand(0)) + expectIncludesAtemCommandName(commands, AtemConnection.Commands.AutoTransitionCommand.name) } }) @@ -153,12 +204,8 @@ describe('Diff States', () => { const commands = device.diffStates(deviceState1, deviceState2, mappings) expect(commands).toHaveLength(3) - const transitionPropertiesCommand = new AtemConnection.Commands.TransitionPropertiesCommand(0) - transitionPropertiesCommand.updateProps({ nextStyle: 1 }) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.PreviewInputCommand(0, 4)) - compareAtemCommands(commands[1].command, new AtemConnection.Commands.TransitionPositionCommand(0, 0)) - compareAtemCommands(commands[2].command, new AtemConnection.Commands.AutoTransitionCommand(0)) + expectIncludesAtemCommandName(commands, AtemConnection.Commands.AutoTransitionCommand.name) } }) }) diff --git a/packages/timeline-state-resolver/src/integrations/atem/__tests__/stateBuilder.spec.ts b/packages/timeline-state-resolver/src/integrations/atem/__tests__/stateBuilder.spec.ts index 6151db78f..8640c3f02 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/__tests__/stateBuilder.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/__tests__/stateBuilder.spec.ts @@ -529,7 +529,7 @@ describe('AtemStateBuilder', () => { expect(expectedState.audio?.channels).toBeFalsy() expectedState.audio = { channels: {} } - expectedState.audio.channels[1] = Object.assign(cloneDeep(Defaults.Audio.Channel), { + expectedState.audio.channels[1] = Object.assign(cloneDeep(Defaults.ClassicAudio.Channel), { gain: 123, balance: 456, mixOption: 2, diff --git a/packages/timeline-state-resolver/src/integrations/atem/__tests__/util.ts b/packages/timeline-state-resolver/src/integrations/atem/__tests__/util.ts index 2ecd23435..5602c81df 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/__tests__/util.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/__tests__/util.ts @@ -1,5 +1,5 @@ import { literal } from '../../../devices/device' -import { AtemDevice } from '..' +import { AtemCommandWithContext, AtemDevice } from '..' import * as AtemConnection from 'atem-connection' import { promisify } from 'util' import { AtemOptions } from 'timeline-state-resolver-types' @@ -39,3 +39,32 @@ export function compareAtemCommands( expected.serialize(AtemConnection.Enums.ProtocolVersion.V8_0) ) } + +export function expectIncludesAtemCommands( + received: AtemCommandWithContext[], + expected: AtemConnection.Commands.ISerializableCommand +) { + const failedCommands: AtemConnection.Commands.ISerializableCommand[] = [] + for (const candidate of received) { + if (candidate.command.constructor.name === expected.constructor.name) { + if ( + candidate.command + .serialize(AtemConnection.Enums.ProtocolVersion.V8_0) + .equals(expected.serialize(AtemConnection.Enums.ProtocolVersion.V8_0)) + ) { + // Buffer matched + return + } else { + failedCommands.push(candidate.command) + } + } + } + + // Found some candidates of the same type, with a different payload + expect(failedCommands).toBeFalsy() +} + +export function expectIncludesAtemCommandName(received: AtemCommandWithContext[], expectedName: string) { + const commandNames = received.map((cmd) => cmd.command.constructor.name) + expect(commandNames).toContain(expectedName) +} diff --git a/packages/timeline-state-resolver/src/integrations/atem/diffState.ts b/packages/timeline-state-resolver/src/integrations/atem/diffState.ts new file mode 100644 index 000000000..aa27e1e1c --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/atem/diffState.ts @@ -0,0 +1,110 @@ +import { Diff } from 'atem-state' +import { DeepComplete } from 'atem-state/dist/util' +import { + Mapping, + SomeMappingAtem, + MappingAtemAuxilliary, + MappingAtemType, + MappingAtemAudioChannel, + Mappings, +} from 'timeline-state-resolver-types' + +/** + Returns an option object to be passed into AtemState.diffStates(). + Based on the mappings, these options enables/disables certain areas-of-interest in the diff atem state. +*/ +export function createDiffOptions(mappings: Mappings): DeepComplete { + // Find the auxes that have mappings + const auxMappings = Object.values>(mappings) + .filter( + (mapping: Mapping): mapping is Mapping => + mapping.options.mappingType === MappingAtemType.Auxilliary + ) + .map((mapping) => mapping.options.index) + + // Find the audioOutputs that have mappings + const audioOutputs = Object.values>(mappings) + .filter( + (mapping: Mapping): mapping is Mapping => + mapping.options.mappingType === MappingAtemType.Auxilliary + ) + .map((mapping) => mapping.options.index) + const audioOutputsObj: DeepComplete> = { + default: undefined, + } + for (const audioOutput of audioOutputs) { + audioOutputsObj[audioOutput] = { + name: false, + sourceId: true, + } + } + + // Manually construct the tree of what to diff, to match the previous version of atem-state. + // Future: this should be computed from the mappings + return { + colorGenerators: undefined, + settings: { + multiviewer: undefined, + }, + macros: { + player: { player: true }, + }, + media: { + players: { + source: true, + status: true, + }, + }, + video: { + auxiliaries: auxMappings, + downstreamKeyers: { + sources: true, + onAir: true, + properties: true, + mask: true, + }, + mixEffects: { + programPreview: true, + transitionStatus: true, + transitionProperties: true, + transitionSettings: { + dip: false, + DVE: false, + mix: true, + stinger: false, + wipe: true, + }, + upstreamKeyers: { + sources: true, + onAir: true, + type: true, + mask: true, + flyKeyframes: undefined, + dveSettings: true, + chromaSettings: false, + advancedChromaSettings: false, + lumaSettings: true, + patternSettings: true, + }, + }, + superSources: { + boxes: 'all', + border: true, + properties: true, + }, + }, + audio: { + classic: undefined, + fairlight: { + inputs: undefined, + masterOutput: undefined, + monitorOutput: undefined, + crossfade: undefined, + audioRouting: { + sources: undefined, + outputs: audioOutputsObj, + }, + }, + }, + } +} diff --git a/packages/timeline-state-resolver/src/integrations/atem/index.ts b/packages/timeline-state-resolver/src/integrations/atem/index.ts index 66dfa5fe0..7badd2eff 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/index.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/index.ts @@ -1,14 +1,10 @@ import * as _ from 'underscore' import { DeviceStatus, StatusCode } from './../../devices/device' import { - SomeMappingAtem, - MappingAtemType, AtemOptions, Mappings, Timeline, TSRTimelineContent, - Mapping, - MappingAtemAuxilliary, ActionExecutionResult, ActionExecutionResultCode, AtemActions, @@ -23,6 +19,7 @@ import { } from 'atem-connection' import { CommandWithContext, Device } from '../../service/device' import { AtemStateBuilder } from './stateBuilder' +import { createDiffOptions } from './diffState' export interface AtemCommandWithContext { command: AtemCommands.ISerializableCommand @@ -157,23 +154,9 @@ export class AtemDevice extends Device>(mappings) - .filter( - (mapping: Mapping): mapping is Mapping => - mapping.options.mappingType === MappingAtemType.Auxilliary - ) - .map((mapping) => mapping.options.index) - - for (let i = 0; i < noOfAuxes; i++) { - if (!auxMappings.includes(i)) { - oldAtemState.video.auxilliaries[i] = undefined - newAtemState.video.auxilliaries[i] = undefined - } - } + const diffOptions = createDiffOptions(mappings) - return AtemState.diffStates(this._protocolVersion, oldAtemState, newAtemState).map((cmd) => { + return AtemState.diffStates(this._protocolVersion, oldAtemState, newAtemState, diffOptions).map((cmd) => { // backwards compability, to be removed later: return { command: cmd, diff --git a/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts b/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts index 08a15fd5f..d6200aca1 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts @@ -1,4 +1,4 @@ -import { AtemStateUtil, Enums } from 'atem-connection' +import { AtemStateUtil, Enums, MacroState, VideoState } from 'atem-connection' import { Mapping, SomeMappingAtem, @@ -25,19 +25,12 @@ import { MappingAtemSuperSourceBox, MappingAtemSuperSourceProperties, MappingAtemDownStreamKeyer, + MappingAtemAudioRouting, + TimelineContentAtemAudioRouting, } from 'timeline-state-resolver-types' import _ = require('underscore') -import * as underScoreDeepExtend from 'underscore-deep-extend' import { State as DeviceState, Defaults as StateDefault } from 'atem-state' -import { assertNever, cloneDeep } from '../../lib' -import { MappingAtemAudioRouting, TimelineContentAtemAudioRouting } from 'timeline-state-resolver-types/src' - -_.mixin({ deepExtend: underScoreDeepExtend(_) }) - -function deepExtend(destination: T, ...sources: any[]) { - // @ts-ignore (mixin) - return _.deepExtend(destination, ...sources) -} +import { assertNever, cloneDeep, deepMerge } from '../../lib' export class AtemStateBuilder { // Start out with default state: @@ -123,8 +116,20 @@ export class AtemStateBuilder { private _applyMixEffect(mapping: MappingAtemMixEffect, content: TimelineContentAtemME): void { if (typeof mapping.index !== 'number' || mapping.index < 0) return - const stateMixEffect = AtemStateUtil.getMixEffect(this.#deviceState, mapping.index) - deepExtend(stateMixEffect, _.omit(content.me, 'upstreamKeyers')) + const stateMixEffect = deepMerge( + AtemStateUtil.getMixEffect(this.#deviceState, mapping.index), + _.omit(content.me, 'upstreamKeyers', 'transitionPosition') + ) + this.#deviceState.video.mixEffects[mapping.index] = stateMixEffect + if (content.me.transitionPosition !== undefined) { + stateMixEffect.transitionPosition = { + handlePosition: content.me.transitionPosition, + + // Readonly properties + inTransition: false, + remainingFrames: 0, + } + } const objectTransition = content.me.transition if (this._isAssignableToNextStyle(objectTransition)) { @@ -134,8 +139,10 @@ export class AtemStateBuilder { const objectKeyers = content.me.upstreamKeyers if (objectKeyers) { for (const objKeyer of objectKeyers) { - const stateKeyer = AtemStateUtil.getUpstreamKeyer(stateMixEffect, objKeyer.upstreamKeyerId) - deepExtend(stateKeyer, objKeyer) + stateMixEffect.upstreamKeyers[objKeyer.upstreamKeyerId] = deepMerge( + AtemStateUtil.getUpstreamKeyer(stateMixEffect, objKeyer.upstreamKeyerId), + objKeyer + ) } } } @@ -143,8 +150,10 @@ export class AtemStateBuilder { private _applyDownStreamKeyer(mapping: MappingAtemDownStreamKeyer, content: TimelineContentAtemDSK): void { if (typeof mapping.index !== 'number' || mapping.index < 0) return - const stateDSK = AtemStateUtil.getDownstreamKeyer(this.#deviceState, mapping.index) - deepExtend(stateDSK, content.dsk) + this.#deviceState.video.downstreamKeyers[mapping.index] = deepMerge( + AtemStateUtil.getDownstreamKeyer(this.#deviceState, mapping.index), + content.dsk + ) } private _applySuperSourceBox(mapping: MappingAtemSuperSourceBox, content: TimelineContentAtemSsrc): void { @@ -153,12 +162,10 @@ export class AtemStateBuilder { const stateSuperSource = AtemStateUtil.getSuperSource(this.#deviceState, mapping.index) content.ssrc.boxes.forEach((objBox, i) => { - const stateBox = stateSuperSource.boxes[i] - if (stateBox) { - deepExtend(stateBox, objBox) - } else { - stateSuperSource.boxes[i] = deepExtend(cloneDeep(StateDefault.Video.SuperSourceBox), objBox) - } + stateSuperSource.boxes[i] = deepMerge( + stateSuperSource.boxes[i] ?? cloneDeep(StateDefault.Video.SuperSourceBox), + objBox + ) }) } @@ -184,11 +191,15 @@ export class AtemStateBuilder { 'borderLightSourceAltitude', ] - if (!stateSuperSource.properties) stateSuperSource.properties = cloneDeep(StateDefault.Video.SuperSourceProperties) - deepExtend(stateSuperSource.properties, _.omit(content.ssrcProps, ...borderKeys)) + stateSuperSource.properties = deepMerge( + stateSuperSource.properties ?? cloneDeep(StateDefault.Video.SuperSourceProperties), + _.omit(content.ssrcProps, ...borderKeys) + ) - if (!stateSuperSource.border) stateSuperSource.border = cloneDeep(StateDefault.Video.SuperSourceBorder) - deepExtend(stateSuperSource.border, _.pick(content.ssrcProps, ...borderKeys)) + stateSuperSource.border = deepMerge( + stateSuperSource.border ?? cloneDeep(StateDefault.Video.SuperSourceBorder), + _.pick(content.ssrcProps, ...borderKeys) + ) } private _applyAuxilliary(mapping: MappingAtemAuxilliary, content: TimelineContentAtemAUX): void { @@ -200,8 +211,10 @@ export class AtemStateBuilder { private _applyMediaPlayer(mapping: MappingAtemMediaPlayer, content: TimelineContentAtemMediaPlayer): void { if (typeof mapping.index !== 'number' || mapping.index < 0) return - const ms = AtemStateUtil.getMediaPlayer(this.#deviceState, mapping.index) - if (ms) deepExtend(ms, content.mediaPlayer) + this.#deviceState.media.players[mapping.index] = deepMerge( + AtemStateUtil.getMediaPlayer(this.#deviceState, mapping.index), + content.mediaPlayer + ) } private _applyAudioChannel(mapping: MappingAtemAudioChannel, content: TimelineContentAtemAudioChannel): void { @@ -209,7 +222,7 @@ export class AtemStateBuilder { if (!this.#deviceState.audio) this.#deviceState.audio = { channels: {} } - const stateAudioChannel = this.#deviceState.audio.channels[mapping.index] ?? cloneDeep(StateDefault.Audio.Channel) + const stateAudioChannel = this.#deviceState.audio.channels[mapping.index] ?? StateDefault.ClassicAudio.Channel this.#deviceState.audio.channels[mapping.index] = { ...cloneDeep(stateAudioChannel), ...content.audioChannel, @@ -217,6 +230,8 @@ export class AtemStateBuilder { } private _applyAudioRouting(mapping: MappingAtemAudioRouting, content: TimelineContentAtemAudioRouting): void { + if (typeof mapping.index !== 'number' || mapping.index < 0) return + // lazily generate the state properties, to make this be opt in per-mapping if (!this.#deviceState.fairlight) this.#deviceState.fairlight = { inputs: {} } if (!this.#deviceState.fairlight.audioRouting) @@ -239,7 +254,9 @@ export class AtemStateBuilder { } private _applyMacroPlayer(_mapping: MappingAtemMacroPlayer, content: TimelineContentAtemMacroPlayer): void { - const stateMacroPlayer = this.#deviceState.macro.macroPlayer - deepExtend(stateMacroPlayer, content.macroPlayer) + this.#deviceState.macro.macroPlayer = deepMerge( + this.#deviceState.macro.macroPlayer, + content.macroPlayer + ) } } diff --git a/packages/timeline-state-resolver/src/integrations/obs/index.ts b/packages/timeline-state-resolver/src/integrations/obs/index.ts index 490b97c1f..f0209c338 100644 --- a/packages/timeline-state-resolver/src/integrations/obs/index.ts +++ b/packages/timeline-state-resolver/src/integrations/obs/index.ts @@ -1,5 +1,4 @@ import * as _ from 'underscore' -import * as underScoreDeepExtend from 'underscore-deep-extend' import { DeviceWithState, CommandWithContext, DeviceStatus, StatusCode } from './../../devices/device' import { DoOnTime, SendMode } from '../../devices/doOnTime' @@ -24,11 +23,6 @@ interface OBSRequest { args: object } -_.mixin({ deepExtend: underScoreDeepExtend(_) }) -function deepExtend(destination: T, ...sources: any[]) { - // @ts-ignore (mixin) - return _.deepExtend(destination, ...sources) -} export interface DeviceOptionsOBSInternal extends DeviceOptionsOBS { commandReceiver?: CommandReceiver } @@ -353,15 +347,11 @@ export class OBSDevice extends DeviceWithState(destination: T, source: PartialDeep): T { + return deepmerge(destination, source) +} + export interface Trace { /** id of this trace, should be formatted as namespace:id */ measurement: string diff --git a/yarn.lock b/yarn.lock index 8130adf6a..1faf6e89d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -839,6 +839,17 @@ __metadata: languageName: node linkType: hard +"@julusian/freetype2@npm:^1.1.2": + version: 1.1.2 + resolution: "@julusian/freetype2@npm:1.1.2" + dependencies: + node-addon-api: ^5.0.0 + node-gyp: latest + pkg-prebuilds: ^0.2.1 + checksum: c690cf169908995c0cd3b2bd63f226c85ab9615d2da4c000644a311fd8ca736abae9e355ee7e2332dbaf1c59dd6be364f2711c6a31c4b0cfafdee28cb73a6beb + languageName: node + linkType: hard + "@lerna/child-process@npm:6.6.2": version: 6.6.2 resolution: "@lerna/child-process@npm:6.6.2" @@ -2529,31 +2540,34 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"atem-connection@npm:2.5.0": - version: 2.5.0 - resolution: "atem-connection@npm:2.5.0" +"atem-connection@npm:3.3.2": + version: 3.3.2 + resolution: "atem-connection@npm:3.3.2" dependencies: - big-integer: ^1.6.51 - eventemitter3: ^4.0.4 + "@julusian/freetype2": ^1.1.2 + debug: ^4.3.4 + eventemitter3: ^4.0.7 exit-hook: ^2.2.1 nanotimer: ^0.3.15 - threadedclass: ^0.8.0 - tslib: ^1.13.0 - wavefile: ^8.4.4 - checksum: beaaffceb729f16056b85fbcd711a371973905353e9c8cdf85a0732fa0ff7d89b11039ffb70a599f184d30042110c59fd695ee772b17580073746c5beeb07e5b + p-lazy: ^3.1.0 + p-queue: ^6.6.2 + threadedclass: ^1.2.1 + tslib: ^2.6.2 + wavefile: ^8.4.6 + checksum: dbfe751a75bd6d7de7877eda84800d9ca5a644a97f603cc60050eabbf02c2ebfe516344c3f6525a2417b83d35f98e542787a8178c85e660d54964a49f6b47664 languageName: node linkType: hard -"atem-state@npm:0.13.0": - version: 0.13.0 - resolution: "atem-state@npm:0.13.0" +"atem-state@npm:1.1.0": + version: 1.1.0 + resolution: "atem-state@npm:1.1.0" dependencies: - deepmerge: ^4.2.2 - tslib: ^2.3.1 - type-fest: ^1.4.0 + deepmerge: ^4.3.1 + tslib: ^2.6.2 + type-fest: ^3.13.1 peerDependencies: - atem-connection: ~2.5 - checksum: d6b912e6a6a8f2e8c1a101234b24687ef5b4a5ae6fb407479088c881af1ee4cd8882f6f56310d087bf006f5f432bb298306890b6af445e077e0526b5651fdee1 + atem-connection: 3.3.2 + checksum: 9e48469117bb00b10a59f983eb319d0ec2128164e24eb643b4e9cd52bfeaf9a942f5b9342c3a475d44352524315f761d960f878bb05401250db7414cfaa67640 languageName: node linkType: hard @@ -2704,7 +2718,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"big-integer@npm:^1.6.44, big-integer@npm:^1.6.51": +"big-integer@npm:^1.6.44": version: 1.6.51 resolution: "big-integer@npm:1.6.51" checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518 @@ -9087,6 +9101,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"p-lazy@npm:^3.1.0": + version: 3.1.0 + resolution: "p-lazy@npm:3.1.0" + checksum: 289dbfd9de7d241498fd2d1ac9ec4e36de78672a92175f4b3c5fa9527b62f7549b06f549570b47f218d90b946f3a75611f9b8407934fe01512bc4fb56fdbf47d + languageName: node + linkType: hard + "p-limit@npm:^1.1.0": version: 1.3.0 resolution: "p-limit@npm:1.3.0" @@ -9536,6 +9557,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"pkg-prebuilds@npm:^0.2.1": + version: 0.2.1 + resolution: "pkg-prebuilds@npm:0.2.1" + dependencies: + yargs: ^17.5.1 + bin: + pkg-prebuilds-copy: bin/copy.mjs + pkg-prebuilds-verify: bin/verify.mjs + checksum: ba4c802200558d5aa89e96e474976633c1f964ef0b2035a70dfde4b83af782a6624f33b4b83c19dd85db7aa9097e259a55ad53d63d55d62ac5d524997f3bbd17 + languageName: node + linkType: hard + "postcss-selector-parser@npm:^6.0.10": version: 6.0.13 resolution: "postcss-selector-parser@npm:6.0.13" @@ -11283,18 +11316,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"threadedclass@npm:^0.8.0": - version: 0.8.3 - resolution: "threadedclass@npm:0.8.3" - dependencies: - callsites: ^3.1.0 - eventemitter3: ^4.0.4 - is-running: ^2.1.0 - tslib: ^1.13.0 - checksum: 64f3c25e1c3178fb4f2bbc278329cdf348a5d7379fa80fe5489e2107c69c5bf01daadc44421881ffc314bbbc8d1a5e17f5886147a3fd55bc80cd667b63e842e6 - languageName: node - linkType: hard - "threadedclass@npm:^1.2.1": version: 1.2.1 resolution: "threadedclass@npm:1.2.1" @@ -11362,7 +11383,6 @@ asn1@evs-broadcast/node-asn1: symlink-dir: ^5.1.1 ts-jest: ^29.1.0 ts-node: ^8.10.2 - type-fest: ^3.10.0 typedoc: ^0.23.28 typescript: ~4.9.5 languageName: unknown @@ -11381,8 +11401,8 @@ asn1@evs-broadcast/node-asn1: resolution: "timeline-state-resolver@workspace:packages/timeline-state-resolver" dependencies: "@tv2media/v-connection": ^7.3.0 - atem-connection: 2.5.0 - atem-state: 0.13.0 + atem-connection: 3.3.2 + atem-state: 1.1.0 cacheable-lookup: ^5.0.3 casparcg-connection: ^6.0.6 casparcg-state: ^3.0.2 @@ -11409,8 +11429,8 @@ asn1@evs-broadcast/node-asn1: timeline-state-resolver-types: 9.1.0-release51 tslib: ^2.5.1 tv-automation-quantel-gateway-client: ^3.1.7 + type-fest: ^3.10.0 underscore: ^1.13.6 - underscore-deep-extend: ^1.1.5 utf-8-validate: ^5.0.10 vinyl-fs: ^3.0.3 ws: ^7.5.9 @@ -11621,17 +11641,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.1": - version: 2.5.1 - resolution: "tslib@npm:2.5.1" - checksum: 535e41e956b32d21aeff007a2c74989d1a6bf3fecb2b0141d80e5b9920bf90e23b4f0cd3772704bc03637c762dbdc1a314d75a407dd7189afbe3e83d80df9392 - languageName: node - linkType: hard - -"tslib@npm:^2.6.0": - version: 2.6.0 - resolution: "tslib@npm:2.6.0" - checksum: c01066038f950016a18106ddeca4649b4d76caa76ec5a31e2a26e10586a59fceb4ee45e96719bf6c715648e7c14085a81fee5c62f7e9ebee68e77a5396e5538f +"tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.1, tslib@npm:^2.6.0, tslib@npm:^2.6.2": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad languageName: node linkType: hard @@ -11756,7 +11769,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^1.0.1, type-fest@npm:^1.4.0": +"type-fest@npm:^1.0.1": version: 1.4.0 resolution: "type-fest@npm:1.4.0" checksum: b011c3388665b097ae6a109a437a04d6f61d81b7357f74cbcb02246f2f5bd72b888ae33631b99871388122ba0a87f4ff1c94078e7119ff22c70e52c0ff828201 @@ -11770,12 +11783,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^3.1.0, type-fest@npm:^3.10.0": - version: 3.10.0 - resolution: "type-fest@npm:3.10.0" - peerDependencies: - typescript: ">=4.7.0" - checksum: df3bd25809ea79ac7712da7c11648251118c23f65b226eba263797e42be60ba34085874ec492202b8ecf2e5a2273b27ddcbfe3be2c275dfcef3f0e2ee90ea179 +"type-fest@npm:^3.1.0, type-fest@npm:^3.10.0, type-fest@npm:^3.13.1": + version: 3.13.1 + resolution: "type-fest@npm:3.13.1" + checksum: c06b0901d54391dc46de3802375f5579868949d71f93b425ce564e19a428a0d411ae8d8cb0e300d330071d86152c3ea86e744c3f2860a42a79585b6ec2fdae8e languageName: node linkType: hard @@ -11859,13 +11870,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"underscore-deep-extend@npm:^1.1.5": - version: 1.1.5 - resolution: "underscore-deep-extend@npm:1.1.5" - checksum: 5bc6d1af0e4cc5cc177284eb00327991a08f5a6b632f45daa2cc625f54705b4e22330752b68d340356f0df2d5d01310aa22c28a9a9a63ec16ac78e082aba9e03 - languageName: node - linkType: hard - "underscore.string@npm:~3.3.4": version: 3.3.6 resolution: "underscore.string@npm:3.3.6" @@ -12246,7 +12250,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"wavefile@npm:^8.4.4": +"wavefile@npm:^8.4.6": version: 8.4.6 resolution: "wavefile@npm:8.4.6" dependencies: @@ -12581,7 +12585,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.6.2": +"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: