Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/quick-tsr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export interface Input {
export interface TSRSettings {
multiThreading?: boolean
multiThreadedResolver?: boolean
logCommandReports?: boolean
}

// ------------
Expand Down
55 changes: 37 additions & 18 deletions packages/quick-tsr/src/tsrHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,36 +90,55 @@ export class TSRHandler {
this.tsr.connectionManager.on('connectionRemoved', (deviceId: string) => {
console.log(`Device ${deviceId} removed`)
})
this.tsr.connectionManager.on('connectionEvent:error', (deviceId: string, context: string, err: Error) => {
console.error(`Device ${deviceId} connection error: ${context} = ${err.message}`)
this.tsr.connectionManager.on('error', (ctx: string, err) => {
console.log(`Error: connectionManager (${ctx})`, err)
})
this.tsr.connectionManager.on('warning', (msg: string) => {
console.log('Warning: connectionManager', msg)
})
this.tsr.connectionManager.on('info', (msg: string) => {
console.log('Warning: connectionManager', msg)
})
this.tsr.connectionManager.on('debug', (...args: any) => {
console.log('Debug: connectionManager', ...args)
})

this.tsr.connectionManager.on('connectionEvent:connectionChanged', (deviceId: string, status: DeviceStatus) => {
console.log(`Device ${deviceId} status changed: ${JSON.stringify(status)}`)
this.tsr.connectionManager.on('connectionEvent:error', (deviceId: string, context: string, err: Error) => {
console.error(`Device ${deviceId} connection error: ${context} = ${err.message}`)
})
this.tsr.connectionManager.on(
'connectionEvent:slowSentCommand',
(_deviceId: string, _info: SlowSentCommandInfo) => {
// console.log(`Device ${device.deviceId} slow sent command: ${_info}`)
}
)
this.tsr.connectionManager.on(
'connectionEvent:slowFulfilledCommand',
(_deviceId: string, _info: SlowFulfilledCommandInfo) => {
// console.log(`Device ${device.deviceId} slow fulfilled command: ${_info}`)
}
)
this.tsr.connectionManager.on('connectionEvent:commandReport', (deviceId: string, command: any) => {
console.log(`Device ${deviceId} command: ${JSON.stringify(command)}`)
this.tsr.connectionManager.on('connectionEvent:warning', (deviceId: string, warning: string) => {
console.error(`Device ${deviceId} connection warning: ${warning}`)
})
this.tsr.connectionManager.on('connectionEvent:info', (deviceId: string, info: string) => {
console.error(`Device ${deviceId} connection info: ${info}`)
})
this.tsr.connectionManager.on('connectionEvent:debug', (deviceId: string, ...args: any[]) => {
const data = args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
console.log(`Device ${deviceId} debug: ${data}`)
})

this.tsr.connectionManager.on('connectionEvent:connectionChanged', (deviceId: string, status: DeviceStatus) => {
console.log(`Device ${deviceId} status changed: ${JSON.stringify(status)}`)
})

if (tsrSettings.logCommandReports) {
this.tsr.connectionManager.on(
'connectionEvent:slowSentCommand',
(_deviceId: string, _info: SlowSentCommandInfo) => {
// console.log(`Device ${device.deviceId} slow sent command: ${_info}`)
}
)
this.tsr.connectionManager.on(
'connectionEvent:slowFulfilledCommand',
(_deviceId: string, _info: SlowFulfilledCommandInfo) => {
// console.log(`Device ${device.deviceId} slow fulfilled command: ${_info}`)
}
)
this.tsr.connectionManager.on('connectionEvent:commandReport', (deviceId: string, command: any) => {
console.log(`Device ${deviceId} command: ${JSON.stringify(command)}`)
})
}

await this.tsr.init()

// this._initialized = true
Expand Down
3 changes: 2 additions & 1 deletion packages/quick-tsr/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"include": ["src/**/*.ts", "input/**/*.ts"],
"exclude": ["node_modules/**"],
"compilerOptions": {
"types": ["jest", "node"]
"types": ["jest", "node"],
"noUnusedLocals": false
}
}
4 changes: 2 additions & 2 deletions packages/timeline-state-resolver-api/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ export interface BaseDeviceAPI<DeviceState, AddressState, Command extends Comman
*/
diffAddressStates?(state1: AddressState, state2: AddressState): boolean
/**
* Returns true if the
* Returns true if the new state warrants reasserting control over the address
*/
addressStateReassertsControl?(oldState: AddressState | undefined, newState: AddressState): boolean
addressStateReassertsControl?(oldState: AddressState | undefined, newState: AddressState | undefined): boolean
/**
* This method takes 2 states and returns a set of device-commands that will
* transition the device from oldState to newState.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export interface DeviceCommonOptions {
disable?: boolean
threadUsage?: number
disableSharedHardwareControl?: boolean
syncOnStartup?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
"ui:title": "Disable Shared Hardware Control",
"ui:hint": "Some integrations provide a way to read back external state such that non-Sofie actors can control the device too, you can disable this in case the mechanism is causing issues.",
"default": false
},
"syncOnStartup": {
"type": "boolean",
"ui:title": "Sync the device after connecting",
"ui:hint": "Should the device be synced upon first connection. Defaults to true and can only be turned off if \" Shared Hardware Control\" is supported and enabled",
"default": true
}
},
"required": [],
Expand Down
13 changes: 10 additions & 3 deletions packages/timeline-state-resolver/src/integrations/atem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export class AtemDevice implements Device<AtemDeviceTypes, AtemDeviceState, Atem
this._connectionChanged()
})
this._atem.on('error', (e) => this.context.logger.error('Atem', new Error(e)))
this._atem.on('stateChanged', (state) => {

this._atem.on('stateChanged', (state, changes) => {
if (changes.length === 1 && changes[0] === 'displayClock.currentTime') return

// the external device is communicating something changed, the tracker should be updated (and may fire a "blocked" event if the change is caused by someone else)
updateFromAtemState((addr, addrState) => this.context.setAddressState(addr, addrState), state) // note - improvement can be to update depending on the actual paths that changed

Expand All @@ -86,6 +89,8 @@ export class AtemDevice implements Device<AtemDeviceTypes, AtemDeviceState, Atem
this._connectionChanged()

if (this._atem.state) {
updateFromAtemState((addr, addrState) => this.context.setAddressState(addr, addrState), this._atem.state)

// Do a state diff to get to the desired state
this._protocolVersion = this._atem.state.info.apiVersion
this.context
Expand Down Expand Up @@ -210,7 +215,7 @@ export class AtemDevice implements Device<AtemDeviceTypes, AtemDeviceState, Atem
async sendCommand({ command, context, timelineObjId }: AtemCommandWithContext): Promise<void> {
const cwc: AtemCommandWithContext = {
context,
command,
command: command.map((c) => ({ name: c.constructor.name, ...c })),
timelineObjId,
}
this.context.logger.debug(cwc)
Expand All @@ -231,7 +236,9 @@ export class AtemDevice implements Device<AtemDeviceTypes, AtemDeviceState, Atem
diffAddressStates(state1: AnyAddressState, state2: AnyAddressState): boolean {
return diffAddressStates(state1, state2)
}
addressStateReassertsControl(oldState: AnyAddressState | undefined, newState: AnyAddressState): boolean {
addressStateReassertsControl(oldState: AnyAddressState | undefined, newState: AnyAddressState | undefined): boolean {
if (!newState) return false // undefined incoming state should never reassert

return oldState?.controlValue !== newState.controlValue
}

Expand Down
17 changes: 17 additions & 0 deletions packages/timeline-state-resolver/src/integrations/atem/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SuperSource, TransitionSettings } from 'atem-connection/dist/state/vide
import { DownstreamKeyer } from 'atem-connection/dist/state/video/downstreamKeyers'
import { UpstreamKeyer } from 'atem-connection/dist/state/video/upstreamKeyers'
import { State as DeviceState } from 'atem-state'
import { resolveUpstreamKeyerState } from 'atem-state/dist/resolvers/upstreamKeyers'
import { assertNever, deepMerge } from '../../lib'
import { AtemStateUtil } from 'atem-connection'
import * as _ from 'underscore'
Expand Down Expand Up @@ -251,6 +252,22 @@ export function diffAddressStates(state1: AnyAddressState, state2: AnyAddressSta
return true
if (state1.state.wipe && state2.state.wipe && !_.isEqual(state1.state.wipe, state2.state.wipe)) return true
return false
} else if (state1.type === AddressType.UpStreamKey && state2.type === AddressType.UpStreamKey) {
const output = resolveUpstreamKeyerState(0, [state1.state], [state2.state], {
sources: true,
onAir: true,
type: true,
mask: true,
flyKeyframes: 'all',
flyProperties: true,
dveSettings: true,
chromaSettings: false,
advancedChromaSettings: false,
lumaSettings: true,
patternSettings: true,
})

return !!output.length
}

return !_.isEqual(state1.state, state2.state)
Expand Down
21 changes: 16 additions & 5 deletions packages/timeline-state-resolver/src/service/DeviceInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,14 @@ export class DeviceInstanceWrapper extends EventEmitter<DeviceInstanceEvents> {
this._instanceId = Math.floor(Math.random() * 10000)
this._startTime = time

this._logDebug = config.debug ?? this._logDebug

this._updateTimeSync()

if (!config.disableSharedHardwareControl && this._device.diffAddressStates && this._device.applyAddressState) {
this._stateTracker = new StateTracker((state1, state2) =>
this._device.diffAddressStates ? this._device.diffAddressStates(state1, state2) : false
this._stateTracker = new StateTracker(
(state1, state2) => (this._device.diffAddressStates ? this._device.diffAddressStates(state1, state2) : false),
config.syncOnStartup ?? true
)

// for now we just do some logging but in the future we could inform library users so they can react to a device changing
Expand All @@ -126,10 +129,18 @@ export class DeviceInstanceWrapper extends EventEmitter<DeviceInstanceEvents> {
})

// make sure the commands for the next state change are correct:
let doRecalc = false
this._stateTracker.on('deviceUpdated', (ahead) => {
if (ahead) {
this._stateHandler.recalcDiff()
}
if (doRecalc) return
doRecalc = true

// do a little debounce for multiple calls
setImmediate(() => {
doRecalc = false
if (ahead) {
this._stateHandler.recalcDiff()
}
})
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('stateHandler', () => {
sendCommand: MOCK_COMMAND_RECEIVER,
...trackerMethods,
},
withStateHandler ? new StateTracker(diff) : undefined
withStateHandler ? new StateTracker(diff, true) : undefined
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('State Tracker', () => {
})

function getNewStateTracker(): StateTracker<AddressState> {
const st = new StateTracker(MOCK_DIFF_FN)
const st = new StateTracker(MOCK_DIFF_FN, false)

st.on('deviceAhead', MOCK_EVENT_DEVICE_AHEAD)
st.on('deviceUnderControl', MOCK_EVENT_DEVICE_SYNC)
Expand Down
14 changes: 6 additions & 8 deletions packages/timeline-state-resolver/src/service/stateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,8 @@ export class StateHandler<
const addrState = nextState.addressStates?.[addr]
const curExpectedState = this._stateTracker.getExpectedState(addr)
if (
addrState &&
!(
this.device.addressStateReassertsControl(curExpectedState, addrState) ||
this.device.diffAddressStates(curExpectedState, addrState)
)
!this.device.addressStateReassertsControl(curExpectedState, addrState) &&
!(addrState && this.device.diffAddressStates(curExpectedState, addrState))
) {
this.device.applyAddressState(newState, addr, currentState)
}
Expand Down Expand Up @@ -306,9 +303,10 @@ export class StateHandler<
if (this._stateTracker && newState.addressStates && this.device.diffAddressStates) {
for (const [a, s] of Object.entries<AddressState>(newState.addressStates)) {
const currentAddrState = this._stateTracker.getExpectedState(a)
const reassertsControl = this.device.addressStateReassertsControl
? this.device.addressStateReassertsControl(currentAddrState, s)
: this.device.diffAddressStates(currentAddrState, s)
const reassertsControl =
(this.device.addressStateReassertsControl
? this.device.addressStateReassertsControl(currentAddrState, s)
: false) || this.device.diffAddressStates(currentAddrState, s)

this._stateTracker.updateExpectedState(a, s, reassertsControl)
}
Expand Down
13 changes: 12 additions & 1 deletion packages/timeline-state-resolver/src/service/stateTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class StateTracker<State> extends EventEmitter<StateTrackerEvents> {
} = {}
private waitToSettle = new Map<string, NodeJS.Timeout>()

constructor(private diff: (state1: State, state2: State) => boolean) {
constructor(private diff: (state1: State, state2: State) => boolean, private syncOnFirstBlood: boolean) {
super()
}

Expand All @@ -62,9 +62,20 @@ export class StateTracker<State> extends EventEmitter<StateTrackerEvents> {
}

updateState(address: string, state: State) {
const firstBlood = !this._state[address]

this._assertAddressExists(address)
this._state[address].currentState = state

if (firstBlood && !this.getExpectedState(address)) {
if (!this.syncOnFirstBlood) {
this._state[address].deviceAhead = true
this.emit('deviceAhead', address)
}

return
}

if (this.waitToSettle.get(address)) clearTimeout(this.waitToSettle.get(address))
this.waitToSettle.set(
address,
Expand Down
Loading