From 83c66c820c2b2ae9766e6474f6f0e22ab575c4ba Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 4 Dec 2023 13:59:45 +0000 Subject: [PATCH] feat: atem command batching --- .../integrations/atem/__tests__/atem.spec.ts | 39 +++++++++++-------- .../atem/__tests__/diffStates.spec.ts | 36 ++++++++++------- .../src/integrations/atem/__tests__/util.ts | 17 +++++--- .../src/integrations/atem/index.ts | 21 ++++++---- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/packages/timeline-state-resolver/src/integrations/atem/__tests__/atem.spec.ts b/packages/timeline-state-resolver/src/integrations/atem/__tests__/atem.spec.ts index 7e82d5e0f..675870f52 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/__tests__/atem.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/__tests__/atem.spec.ts @@ -17,7 +17,7 @@ import { } from 'timeline-state-resolver-types' import { literal } from '../../../lib' import { makeTimelineObjectResolved } from '../../../__mocks__/objects' -import { compareAtemCommands, createDevice, waitForConnection } from './util' +import { compareAtemCommands, createDevice, extractAllCommands, waitForConnection } from './util' import { getDeviceContext } from '../../__tests__/testlib' describe('Atem', () => { @@ -163,9 +163,10 @@ describe('Atem', () => { { const commands = device.diffStates(undefined, deviceState1, myLayerMapping) - expect(commands).toHaveLength(2) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.PreviewInputCommand(0, 2)) - compareAtemCommands(commands[1].command, new AtemConnection.Commands.CutCommand(0)) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(2) + compareAtemCommands(allCommands[0], new AtemConnection.Commands.PreviewInputCommand(0, 2)) + compareAtemCommands(allCommands[1], new AtemConnection.Commands.CutCommand(0)) } const mockState2: Timeline.TimelineState = { @@ -199,9 +200,10 @@ describe('Atem', () => { { const commands = device.diffStates(deviceState1, deviceState2, myLayerMapping) - expect(commands).toHaveLength(2) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.PreviewInputCommand(0, 3)) - compareAtemCommands(commands[1].command, new AtemConnection.Commands.CutCommand(0)) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(2) + compareAtemCommands(allCommands[0], new AtemConnection.Commands.PreviewInputCommand(0, 3)) + compareAtemCommands(allCommands[1], new AtemConnection.Commands.CutCommand(0)) } }) @@ -239,7 +241,8 @@ describe('Atem', () => { // Expect that a command has been scheduled const commands = device.diffStates(undefined, deviceState, myLayerMapping) - expect(commands).toHaveLength(2) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(2) // Diff the same state, after the commands have been sent const commands2 = device.diffStates(deviceState, deviceState, myLayerMapping) @@ -281,9 +284,10 @@ describe('Atem', () => { { const commands = device.diffStates(undefined, deviceState1, myLayerMapping) - expect(commands).toHaveLength(2) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.PreviewInputCommand(0, 2)) - compareAtemCommands(commands[1].command, new AtemConnection.Commands.CutCommand(0)) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(2) + compareAtemCommands(allCommands[0], new AtemConnection.Commands.PreviewInputCommand(0, 2)) + compareAtemCommands(allCommands[1], new AtemConnection.Commands.CutCommand(0)) } const mockState2: Timeline.TimelineState = { @@ -317,15 +321,16 @@ describe('Atem', () => { { const commands = device.diffStates(deviceState1, deviceState2, myLayerMapping) - expect(commands).toHaveLength(5) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(5) const transitionPropertiesCommand = new AtemConnection.Commands.TransitionPropertiesCommand(0) transitionPropertiesCommand.updateProps({ nextStyle: 1 }) - compareAtemCommands(commands[0].command, transitionPropertiesCommand) - compareAtemCommands(commands[1].command, new AtemConnection.Commands.PreviewInputCommand(0, 3)) - compareAtemCommands(commands[2].command, transitionPropertiesCommand) // TODO - why is this sent twice? - compareAtemCommands(commands[3].command, new AtemConnection.Commands.TransitionPositionCommand(0, 0)) - compareAtemCommands(commands[4].command, new AtemConnection.Commands.AutoTransitionCommand(0)) + compareAtemCommands(allCommands[0], transitionPropertiesCommand) + compareAtemCommands(allCommands[1], new AtemConnection.Commands.PreviewInputCommand(0, 3)) + compareAtemCommands(allCommands[2], transitionPropertiesCommand) // TODO - why is this sent twice? + compareAtemCommands(allCommands[3], new AtemConnection.Commands.TransitionPositionCommand(0, 0)) + compareAtemCommands(allCommands[4], new AtemConnection.Commands.AutoTransitionCommand(0)) } }) }) 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 82f9b80e2..201ae2aec 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, expectIncludesAtemCommandName } from './util' +import { compareAtemCommands, createDevice, expectIncludesAtemCommandName, extractAllCommands } from './util' import { AtemTransitionStyle, DeviceType, @@ -40,8 +40,9 @@ describe('Diff States', () => { diffOptions ) - expect(commands).toHaveLength(1) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.ProgramInputCommand(0, 2)) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(1) + compareAtemCommands(allCommands[0], new AtemConnection.Commands.ProgramInputCommand(0, 2)) }) test('Simple diff against other state', async () => { @@ -61,8 +62,9 @@ describe('Diff States', () => { 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)) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(1) + compareAtemCommands(allCommands[0], new AtemConnection.Commands.ProgramInputCommand(0, 3)) }) test('Diff aux without mapping', async () => { @@ -123,8 +125,9 @@ describe('Diff States', () => { diffOptions ) - expect(commands).toHaveLength(1) - compareAtemCommands(commands[0].command, new AtemConnection.Commands.AuxSourceCommand(5, 10)) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(1) + compareAtemCommands(allCommands[0], new AtemConnection.Commands.AuxSourceCommand(5, 10)) }) test('Diff set input with transition', async () => { @@ -150,8 +153,9 @@ describe('Diff States', () => { { const commands = device.diffStates(undefined, deviceState1, mappings) - expect(commands).toHaveLength(2) - expectIncludesAtemCommandName(commands, AtemConnection.Commands.CutCommand.name) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(2) + expectIncludesAtemCommandName(allCommands, AtemConnection.Commands.CutCommand.name) } const deviceState2 = AtemConnection.AtemStateUtil.Create() @@ -163,9 +167,10 @@ describe('Diff States', () => { { const commands = device.diffStates(deviceState1, deviceState2, mappings) - expect(commands).toHaveLength(4) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(4) - expectIncludesAtemCommandName(commands, AtemConnection.Commands.AutoTransitionCommand.name) + expectIncludesAtemCommandName(allCommands, AtemConnection.Commands.AutoTransitionCommand.name) } }) @@ -191,7 +196,9 @@ describe('Diff States', () => { { const commands = device.diffStates(undefined, deviceState1, mappings) - expect(commands).toHaveLength(4) + + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(4) } const deviceState2 = AtemConnection.AtemStateUtil.Create() @@ -203,9 +210,10 @@ describe('Diff States', () => { { const commands = device.diffStates(deviceState1, deviceState2, mappings) - expect(commands).toHaveLength(3) + const allCommands = extractAllCommands(commands) + expect(allCommands).toHaveLength(3) - expectIncludesAtemCommandName(commands, AtemConnection.Commands.AutoTransitionCommand.name) + expectIncludesAtemCommandName(allCommands, AtemConnection.Commands.AutoTransitionCommand.name) } }) }) 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 1cf87491d..bfcb55775 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/__tests__/util.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/__tests__/util.ts @@ -4,6 +4,7 @@ import { promisify } from 'util' import { AtemOptions } from 'timeline-state-resolver-types' import { getDeviceContext } from '../../__tests__/testlib' import { literal } from '../../../lib' +import { ISerializableCommand } from 'atem-connection/dist/commands' const sleep = promisify(setTimeout) @@ -41,21 +42,21 @@ export function compareAtemCommands( } export function expectIncludesAtemCommands( - received: AtemCommandWithContext[], + received: ISerializableCommand[], expected: AtemConnection.Commands.ISerializableCommand ) { const failedCommands: AtemConnection.Commands.ISerializableCommand[] = [] for (const candidate of received) { - if (candidate.command.constructor.name === expected.constructor.name) { + if (candidate.constructor.name === expected.constructor.name) { if ( - candidate.command + candidate .serialize(AtemConnection.Enums.ProtocolVersion.V8_0) .equals(expected.serialize(AtemConnection.Enums.ProtocolVersion.V8_0)) ) { // Buffer matched return } else { - failedCommands.push(candidate.command) + failedCommands.push(candidate) } } } @@ -64,7 +65,11 @@ export function expectIncludesAtemCommands( expect(failedCommands).toBeFalsy() } -export function expectIncludesAtemCommandName(received: AtemCommandWithContext[], expectedName: string) { - const commandNames = received.map((cmd) => cmd.command.constructor.name) +export function expectIncludesAtemCommandName(received: ISerializableCommand[], expectedName: string) { + const commandNames = received.map((cmd) => cmd.constructor.name) expect(commandNames).toContain(expectedName) } + +export function extractAllCommands(commands: AtemCommandWithContext[]): ISerializableCommand[] { + return commands.flatMap((cmd) => cmd.command) +} diff --git a/packages/timeline-state-resolver/src/integrations/atem/index.ts b/packages/timeline-state-resolver/src/integrations/atem/index.ts index 13d4f18c3..2f65cc93f 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/index.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/index.ts @@ -155,14 +155,19 @@ export class AtemDevice extends Device 0) { + return [ + { + command: commands, + context: '', + timelineObjId: '', + }, + ] + } else { + return [] + } } async sendCommand({ command, context, timelineObjId }: AtemCommandWithContext): Promise {