Skip to content

feat: atem command batching SOFIE-2549 #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/timeline-state-resolver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
],
"dependencies": {
"@tv2media/v-connection": "^7.3.2",
"atem-connection": "3.3.2",
"atem-connection": "3.4.0",
"atem-state": "1.2.0-nightly-master-20231129-144508-fb53d10.0",
"cacheable-lookup": "^5.0.3",
"casparcg-connection": "^6.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<TSRTimelineContent> = {
Expand Down Expand Up @@ -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))
}
})

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<TSRTimelineContent> = {
Expand Down Expand Up @@ -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))
}
})
})
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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()
Expand All @@ -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)
}
})

Expand All @@ -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()
Expand All @@ -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)
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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)
}
26 changes: 15 additions & 11 deletions packages/timeline-state-resolver/src/integrations/atem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AtemStateBuilder } from './stateBuilder'
import { createDiffOptions } from './diffState'

export interface AtemCommandWithContext {
command: AtemCommands.ISerializableCommand
command: AtemCommands.ISerializableCommand[]
context: string
timelineObjId: string
}
Expand Down Expand Up @@ -155,15 +155,19 @@ export class AtemDevice extends Device<AtemOptions, AtemDeviceState, AtemCommand
oldAtemState = oldAtemState ?? this._atem.state ?? AtemStateUtil.Create()

const diffOptions = createDiffOptions(mappings)

return AtemState.diffStates(this._protocolVersion, oldAtemState, newAtemState, diffOptions).map((cmd) => {
// backwards compability, to be removed later:
return {
command: cmd,
context: '',
timelineObjId: '', // @todo: implement in Atem-state
}
})
const commands = AtemState.diffStates(this._protocolVersion, oldAtemState, newAtemState, diffOptions)

if (commands.length > 0) {
return [
{
command: commands,
context: '',
timelineObjId: '',
},
]
} else {
return []
}
}

async sendCommand({ command, context, timelineObjId }: AtemCommandWithContext): Promise<void> {
Expand All @@ -178,7 +182,7 @@ export class AtemDevice extends Device<AtemOptions, AtemDeviceState, AtemCommand
if (!this._connected) return

try {
await this._atem.sendCommand(command)
await this._atem.sendCommands(command)
} catch (error: any) {
this.context.commandError(error, cwc)
}
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2497,9 +2497,9 @@ asn1@evs-broadcast/node-asn1:
languageName: node
linkType: hard

"atem-connection@npm:3.3.2":
version: 3.3.2
resolution: "atem-connection@npm:3.3.2"
"atem-connection@npm:3.4.0":
version: 3.4.0
resolution: "atem-connection@npm:3.4.0"
dependencies:
"@julusian/freetype2": ^1.1.2
debug: ^4.3.4
Expand All @@ -2511,7 +2511,7 @@ asn1@evs-broadcast/node-asn1:
threadedclass: ^1.2.1
tslib: ^2.6.2
wavefile: ^8.4.6
checksum: dbfe751a75bd6d7de7877eda84800d9ca5a644a97f603cc60050eabbf02c2ebfe516344c3f6525a2417b83d35f98e542787a8178c85e660d54964a49f6b47664
checksum: 03a0c4ce624afe404f74735a37f469776d6eaab942731b950ff4e4bfe434021858fb54ffd8d7732e7290f28302646b4420265c86cfe3af421b68638216d31856
languageName: node
linkType: hard

Expand Down Expand Up @@ -11072,7 +11072,7 @@ asn1@evs-broadcast/node-asn1:
resolution: "timeline-state-resolver@workspace:packages/timeline-state-resolver"
dependencies:
"@tv2media/v-connection": ^7.3.2
atem-connection: 3.3.2
atem-connection: 3.4.0
atem-state: 1.2.0-nightly-master-20231129-144508-fb53d10.0
cacheable-lookup: ^5.0.3
casparcg-connection: ^6.1.1
Expand Down