From b7ceb6950c875ff123dcc9bdd58a45c7922045d2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 28 Feb 2024 13:23:29 +0000 Subject: [PATCH] feat: atem color generator support SOFIE-2968 (#322) --- .../src/generated/atem.ts | 8 +- .../src/integrations/atem.ts | 15 ++++ .../integrations/atem/$schemas/mappings.json | 15 ++++ .../__snapshots__/diffStates.spec.ts.snap | 2 +- .../atem/__tests__/diffStates.spec.ts | 79 +++++++++++++++++++ .../atem/__tests__/stateBuilder.spec.ts | 50 ++++++++++++ .../src/integrations/atem/diffState.ts | 40 ++++------ .../src/integrations/atem/index.ts | 3 +- .../src/integrations/atem/stateBuilder.ts | 18 ++++- 9 files changed, 202 insertions(+), 28 deletions(-) diff --git a/packages/timeline-state-resolver-types/src/generated/atem.ts b/packages/timeline-state-resolver-types/src/generated/atem.ts index 868130de9..9aaee9588 100644 --- a/packages/timeline-state-resolver-types/src/generated/atem.ts +++ b/packages/timeline-state-resolver-types/src/generated/atem.ts @@ -67,6 +67,11 @@ export interface MappingAtemAudioRouting { mappingType: MappingAtemType.AudioRouting } +export interface MappingAtemColorGenerator { + index: number + mappingType: MappingAtemType.ColorGenerator +} + export enum MappingAtemType { MixEffect = 'mixEffect', DownStreamKeyer = 'downStreamKeyer', @@ -77,9 +82,10 @@ export enum MappingAtemType { AudioChannel = 'audioChannel', MacroPlayer = 'macroPlayer', AudioRouting = 'audioRouting', + ColorGenerator = 'colorGenerator', } -export type SomeMappingAtem = MappingAtemMixEffect | MappingAtemDownStreamKeyer | MappingAtemSuperSourceBox | MappingAtemAuxilliary | MappingAtemMediaPlayer | MappingAtemSuperSourceProperties | MappingAtemAudioChannel | MappingAtemMacroPlayer | MappingAtemAudioRouting +export type SomeMappingAtem = MappingAtemMixEffect | MappingAtemDownStreamKeyer | MappingAtemSuperSourceBox | MappingAtemAuxilliary | MappingAtemMediaPlayer | MappingAtemSuperSourceProperties | MappingAtemAudioChannel | MappingAtemMacroPlayer | MappingAtemAudioRouting | MappingAtemColorGenerator export enum AtemActions { Resync = 'resync' diff --git a/packages/timeline-state-resolver-types/src/integrations/atem.ts b/packages/timeline-state-resolver-types/src/integrations/atem.ts index 107c8336f..080c3058b 100644 --- a/packages/timeline-state-resolver-types/src/integrations/atem.ts +++ b/packages/timeline-state-resolver-types/src/integrations/atem.ts @@ -10,6 +10,7 @@ export enum TimelineContentTypeAtem { // Atem-state AUDIOCHANNEL = 'audioChan', MACROPLAYER = 'macroPlayer', AUDIOROUTING = 'audioRouting', + COLORGENERATOR = 'colorGenerator', } export enum AtemTransitionStyle { // Note: copied from atem-state @@ -112,6 +113,7 @@ export type TimelineContentAtemAny = | TimelineContentAtemAudioChannel | TimelineContentAtemMediaPlayer | TimelineContentAtemAudioRouting + | TimelineContentAtemColorGenerator export interface TimelineContentAtemBase { deviceType: DeviceType.ATEM @@ -414,3 +416,16 @@ export interface TimelineContentAtemAudioRouting extends TimelineContentAtemBase sourceId: number } } + +export interface TimelineContentAtemColorGenerator extends TimelineContentAtemBase { + type: TimelineContentTypeAtem.COLORGENERATOR + + colorGenerator: { + /** 0-3599 */ + hue: number + /** 0-1000 */ + saturation: number + /** 0-1000 */ + luma: number + } +} diff --git a/packages/timeline-state-resolver/src/integrations/atem/$schemas/mappings.json b/packages/timeline-state-resolver/src/integrations/atem/$schemas/mappings.json index b8e0aa48b..638ac375d 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/$schemas/mappings.json +++ b/packages/timeline-state-resolver/src/integrations/atem/$schemas/mappings.json @@ -128,6 +128,21 @@ }, "required": ["index"], "additionalProperties": false + }, + "colorGenerator": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "ui:title": "Index", + "ui:summaryTitle": "Color Generator", + "default": 0, + "min": 0, + "ui:zeroBased": true + } + }, + "required": ["index"], + "additionalProperties": false } } } 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 index c3bbfac3b..b092d8dc4 100644 --- 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 @@ -17,7 +17,7 @@ exports[`Diff States Simple diff against empty state 1`] = ` "monitorOutput": undefined, }, }, - "colorGenerators": undefined, + "colorGenerators": [], "macros": { "player": { "player": 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 201ae2aec..bcfb668b1 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 @@ -4,6 +4,7 @@ import { AtemTransitionStyle, DeviceType, MappingAtemAuxilliary, + MappingAtemColorGenerator, MappingAtemMixEffect, MappingAtemType, Mappings, @@ -216,4 +217,82 @@ describe('Diff States', () => { expectIncludesAtemCommandName(allCommands, AtemConnection.Commands.AutoTransitionCommand.name) } }) + + test('Diff color generator without mapping', async () => { + const device = await createDevice() + + const state1 = AtemConnection.AtemStateUtil.Create() + state1.colorGenerators = { + [0]: { + hue: 1, + saturation: 2, + luma: 3, + }, + } + + const diffOptions = createDiffOptions({}) + expect(diffOptions.colorGenerators).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) + }) + + test('Diff color generator with mapping', async () => { + const device = await createDevice() + + const state1 = AtemConnection.AtemStateUtil.Create() + state1.colorGenerators = { + [0]: { + hue: 1, + saturation: 2, + luma: 3, + }, + } + + const mappings: Mappings = { + myAux: { + device: DeviceType.ATEM, + deviceId: '', + options: { + mappingType: MappingAtemType.ColorGenerator, + index: 0, + }, + }, + } + + const diffOptions = createDiffOptions(mappings) + expect(diffOptions.colorGenerators).toStrictEqual([0]) + + 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 + ) + + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(1) + + const expectedCommand = new AtemConnection.Commands.ColorGeneratorCommand(0) + expectedCommand.updateProps({ hue: 1, saturation: 2, luma: 3 }) + compareAtemCommands(allCommands[0], expectedCommand) + }) }) 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 8640c3f02..fcf751193 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 @@ -6,6 +6,7 @@ import { MappingAtemAudioChannel, MappingAtemAudioRouting, MappingAtemAuxilliary, + MappingAtemColorGenerator, MappingAtemDownStreamKeyer, MappingAtemMacroPlayer, MappingAtemMediaPlayer, @@ -20,6 +21,7 @@ import { TimelineContentAtemAUX, TimelineContentAtemAudioChannel, TimelineContentAtemAudioRouting, + TimelineContentAtemColorGenerator, TimelineContentAtemDSK, TimelineContentAtemMacroPlayer, TimelineContentAtemMediaPlayer, @@ -642,4 +644,52 @@ describe('AtemStateBuilder', () => { expect(deviceState1).toEqual(expectedState) }) }) + + describe('Color Generator', () => { + const myLayerMapping0: Mapping = { + device: DeviceType.ATEM, + deviceId: 'myAtem', + options: { + mappingType: MappingAtemType.ColorGenerator, + index: 0, + }, + } + const myLayerMapping: Mappings = { + myLayer0: myLayerMapping0, + } + + test('Basic', async () => { + const mockState1: Timeline.StateInTime = { + myLayer0: makeTimelineObjectResolved({ + id: 'obj0', + enable: { + start: -1000, // 1 seconds ago + duration: 2000, + }, + layer: 'myLayer0', + content: { + deviceType: DeviceType.ATEM, + type: TimelineContentTypeAtem.COLORGENERATOR, + colorGenerator: { + hue: 123, + luma: 456, + saturation: 789, + }, + }, + }), + } + + const expectedState = AtemConnection.AtemStateUtil.Create() + expectedState.colorGenerators = { + [0]: { + hue: 123, + luma: 456, + saturation: 789, + }, + } + + const deviceState1 = AtemStateBuilder.fromTimeline(mockState1, myLayerMapping) + expect(deviceState1).toEqual(expectedState) + }) + }) }) diff --git a/packages/timeline-state-resolver/src/integrations/atem/diffState.ts b/packages/timeline-state-resolver/src/integrations/atem/diffState.ts index 749b94a96..0c14cf309 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/diffState.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/diffState.ts @@ -1,34 +1,26 @@ import { Diff } from 'atem-state' import { DeepComplete } from 'atem-state/dist/util' -import { - Mapping, - SomeMappingAtem, - MappingAtemAuxilliary, - MappingAtemType, - MappingAtemAudioChannel, - Mappings, -} from 'timeline-state-resolver-types' +import { Mapping, SomeMappingAtem, MappingAtemType, 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 is Mapping => - (mapping as Mapping).options.mappingType === MappingAtemType.Auxilliary - ) - .map((mapping) => mapping.options.index) +export function createDiffOptions(mappings: Mappings): DeepComplete { + const auxMappings: number[] = [] + const audioOutputs: number[] = [] + const colorGenerators: number[] = [] + + for (const mapping of Object.values>(mappings)) { + if (mapping.options.mappingType === MappingAtemType.Auxilliary) { + auxMappings.push(mapping.options.index) + } else if (mapping.options.mappingType === MappingAtemType.AudioChannel) { + audioOutputs.push(mapping.options.index) + } else if (mapping.options.mappingType === MappingAtemType.ColorGenerator) { + colorGenerators.push(mapping.options.index) + } + } - // Find the audioOutputs that have mappings - const audioOutputs = Object.values>(mappings) - .filter( - (mapping): mapping is Mapping => - (mapping as Mapping).options.mappingType === MappingAtemType.Auxilliary - ) - .map((mapping) => mapping.options.index) const audioOutputsObj: DeepComplete> = { default: undefined, } @@ -42,7 +34,7 @@ export function createDiffOptions(mappings: Mappings): DeepComplete) const commands = AtemState.diffStates(this._protocolVersion, oldAtemState, newAtemState, diffOptions) if (commands.length > 0) { diff --git a/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts b/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts index ce6f3580c..7246d0134 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/stateBuilder.ts @@ -27,9 +27,11 @@ import { MappingAtemDownStreamKeyer, MappingAtemAudioRouting, TimelineContentAtemAudioRouting, + MappingAtemColorGenerator, + TimelineContentAtemColorGenerator, } from 'timeline-state-resolver-types' import _ = require('underscore') -import { State as DeviceState, Defaults as StateDefault } from 'atem-state' +import { Defaults, State as DeviceState, Defaults as StateDefault } from 'atem-state' import { assertNever, cloneDeep, deepMerge, literal } from '../../lib' import { PartialDeep } from 'type-fest' @@ -98,6 +100,11 @@ export class AtemStateBuilder { builder._applyMacroPlayer(mapping.options, content) } break + case MappingAtemType.ColorGenerator: + if (content.type === TimelineContentTypeAtem.COLORGENERATOR) { + builder._applyColorGenerator(mapping.options, content) + } + break default: assertNever(mapping.options) break @@ -294,4 +301,13 @@ export class AtemStateBuilder { content.macroPlayer ) } + + private _applyColorGenerator(mapping: MappingAtemColorGenerator, content: TimelineContentAtemColorGenerator): void { + if (!this.#deviceState.colorGenerators) this.#deviceState.colorGenerators = {} + this.#deviceState.colorGenerators[mapping.index] = { + ...Defaults.Color.ColorGenerator, + ...this.#deviceState.colorGenerators[mapping.index], + ...content.colorGenerator, + } + } }