diff --git a/packages/quick-tsr/input/settings.ts b/packages/quick-tsr/input/settings.ts index 1f8560382..32f919fff 100644 --- a/packages/quick-tsr/input/settings.ts +++ b/packages/quick-tsr/input/settings.ts @@ -1,8 +1,12 @@ import { TSRInput } from '../src/index.js' +// import { DeviceType } from 'timeline-state-resolver-types' export const input: TSRInput = { settings: { multiThreading: true, multiThreadedResolver: false, + // stateEvents: { + // atem0: { type: DeviceType.ATEM, events: ['me.0.inputs'] }, + // }, }, } diff --git a/packages/quick-tsr/src/index.ts b/packages/quick-tsr/src/index.ts index 315952f88..633b7827c 100644 --- a/packages/quick-tsr/src/index.ts +++ b/packages/quick-tsr/src/index.ts @@ -3,7 +3,7 @@ import * as chokidar from 'chokidar' import * as fs from 'fs' import * as _ from 'underscore' import * as path from 'path' -import { Mappings, TSRTimeline, DeviceOptionsAny, Datastore } from 'timeline-state-resolver-types' +import { Mappings, TSRTimeline, DeviceOptionsAny, Datastore, TSREventTypesMap } from 'timeline-state-resolver-types' import { TSRHandler } from './tsrHandler.js' // import { TSRHandler } from './tsrHandler' @@ -207,6 +207,13 @@ export interface TSRSettings { multiThreading?: boolean multiThreadedResolver?: boolean logCommandReports?: boolean + stateEvents?: { + [deviceId: string]: { + [K in keyof TSREventTypesMap]: TSREventTypesMap[K] extends Record + ? { type: K; events: (string & keyof TSREventTypesMap[K])[] } + : never + }[keyof TSREventTypesMap] + } } // ------------ diff --git a/packages/quick-tsr/src/tsrHandler.ts b/packages/quick-tsr/src/tsrHandler.ts index 8b0a14472..41ae0aab3 100644 --- a/packages/quick-tsr/src/tsrHandler.ts +++ b/packages/quick-tsr/src/tsrHandler.ts @@ -6,6 +6,7 @@ import { SlowFulfilledCommandInfo, CasparCGDevice, DevicesRegistry, + type SomeTSRStateEvent, } from 'timeline-state-resolver' import { DeviceOptionsAny, @@ -136,6 +137,26 @@ export class TSRHandler { }) } + if (tsrSettings.stateEvents && Object.keys(tsrSettings.stateEvents).length > 0) { + const stateEvents = tsrSettings.stateEvents + + this.tsr.connectionManager.on('connectionInitialised', (deviceId: string) => { + const sub = stateEvents[deviceId] + if (sub) { + this.tsr.setDeviceEventSubscriptions(deviceId, sub.events).catch((e) => { + console.error(`Failed to set event subscriptions for ${deviceId}:`, e) + }) + } + }) + + this.tsr.connectionManager.on('connectionEvent:stateEvent', (deviceId: string, events: SomeTSRStateEvent[]) => { + for (const e of events) { + const payloadStr = e.payload === null ? 'null' : JSON.stringify(e.payload) + console.log(`State event [${deviceId}] ${e.event}: ${payloadStr}`) + } + }) + } + await this.tsr.init() } async destroy(): Promise { diff --git a/packages/timeline-state-resolver-api/src/device.ts b/packages/timeline-state-resolver-api/src/device.ts index aae962cf5..d62b9853c 100644 --- a/packages/timeline-state-resolver-api/src/device.ts +++ b/packages/timeline-state-resolver-api/src/device.ts @@ -36,7 +36,12 @@ export type CommandWithContext = { * API for use by the DeviceInstance to be able to use a device */ export interface Device< - DeviceTypes extends { Options: any; Mappings: any; Actions: Record | null }, + DeviceTypes extends { + Options: any + Mappings: any + Actions: Record | null + Events?: Record + }, DeviceState, Command extends CommandWithContext, AddressState = void, @@ -58,25 +63,11 @@ export interface Device< // todo - add media objects - // From BaseDeviceAPI: ----------------------------------------------- + // Override types from BaseDeviceAPI: ----------------------------------------------- convertTimelineStateToDeviceState( state: DeviceTimelineState, newMappings: Record> ): DeviceState | { deviceState: DeviceState; addressStates: Record } - diffStates( - oldState: DeviceState | undefined, - newState: DeviceState, - mappings: Record>, - time: number - ): Array - sendCommand(command: Command): Promise - - applyAddressState?(state: DeviceState, address: string, addressState: AddressState): void - diffAddressStates?(state1: AddressState, state2: AddressState | undefined): boolean - diffAddressStates?(state1: AddressState | undefined, state2: AddressState): boolean - addressStateReassertsControl?(oldState: AddressState, newState: AddressState | undefined): boolean - addressStateReassertsControl?(oldState: AddressState | undefined, newState: AddressState): boolean - // ------------------------------------------------------------------- } /** @@ -121,6 +112,14 @@ export interface BaseDeviceAPI { +export interface DeviceContextAPI< + DeviceTypes extends { + Options: any + Mappings: any + Actions: Record | null + Events?: Record + }, + DeviceState, + AddressState = void, +> { /** Human-readable name for this device */ deviceName: string @@ -227,4 +235,16 @@ export interface DeviceContextAPI { recalcDiff: () => void setAddressState: (address: string, state: AddressState) => void + + /** + * Report a state event to the consumer of TSR, to be listened on by `connectionEvent:stateEvent` + * @param eventName The name of the event + * @param payload The payload of the event. Note: this should be null to indicate a return to TSR controlled state. + * @param isFromTimeline Indicate whether this event is for a state from the timeline + */ + reportStateEvent: ( + eventName: K, + payload: DeviceTypes['Events'] extends Record ? DeviceTypes['Events'][K] : never, + isFromTimeline: boolean + ) => void } diff --git a/packages/timeline-state-resolver-api/src/tsr-api.ts b/packages/timeline-state-resolver-api/src/tsr-api.ts index 1e58ebd6d..0aba3ec28 100644 --- a/packages/timeline-state-resolver-api/src/tsr-api.ts +++ b/packages/timeline-state-resolver-api/src/tsr-api.ts @@ -14,7 +14,7 @@ export { } from 'timeline-state-resolver-types' export interface DeviceEntry { - deviceClass: new (context: DeviceContextAPI) => Device + deviceClass: new (context: DeviceContextAPI) => Device canConnect: boolean deviceName: (deviceId: string, options: any) => string executionMode: (options: any) => 'salvo' | 'sequential' diff --git a/packages/timeline-state-resolver-tools/bin/schema-types.mjs b/packages/timeline-state-resolver-tools/bin/schema-types.mjs index b0a43ea5c..3fe58e2a7 100755 --- a/packages/timeline-state-resolver-tools/bin/schema-types.mjs +++ b/packages/timeline-state-resolver-tools/bin/schema-types.mjs @@ -214,6 +214,7 @@ if (isMainRepository) { indexFile += `export * from './action-schema.js' export * from './generic-ptz-actions.js' export * from './device-options.js' +import type { DeviceType } from './device-options.js' ` } @@ -413,12 +414,25 @@ ${actionDefinitions } deviceTypeEnum.push(deviceTypeId) + // Check for an optional events side-car file in integrations/{dir}/events.ts + // Future: This needs some implementation thought for plugins + const eventsFilePath = path.join(resolvedOutputPath, '..', 'integrations', dir, 'events.ts') + const hasEventsFile = isMainRepository && (await fsExists(eventsFilePath)) + if (hasEventsFile) { + output = `import type ${dirId}Events from '../integrations/${dir}/events.js'\n` + output + } + output += ` export interface ${dirId}DeviceTypes { Type: DeviceType.${deviceTypeId} Options: ${dirId}Options Mappings: SomeMapping${dirId} - Actions: ${actionDefinitions.length > 0 ? `${dirId}ActionMethods` : 'null'} + Actions: ${actionDefinitions.length > 0 ? `${dirId}ActionMethods` : 'null'}${ + hasEventsFile + ? ` + Events: ${dirId}Events` + : '' + } } ` @@ -453,14 +467,29 @@ export type DeviceOptions${dirId} = DeviceOptionsBase `\t[DeviceType.${typeId}]: ${dirs[i] ? capitalise(dirs[i]) : typeId}DeviceTypes` + ) + indexFile += ` +/** + * A map of all built-in DeviceTypes. + * TSR plugins can augment this interface to add their own device types: + */ +export interface TSRDeviceTypesMap {\n${deviceTypesMapEntries.join('\n')}\n}\n` +} else if (baseMappingsTypes.length) { indexFile += `\nexport type TSRMappingOptions =\n\t| ${baseMappingsTypes.join('\n\t| ')}` } diff --git a/packages/timeline-state-resolver-types/src/events.ts b/packages/timeline-state-resolver-types/src/events.ts new file mode 100644 index 000000000..006b61616 --- /dev/null +++ b/packages/timeline-state-resolver-types/src/events.ts @@ -0,0 +1,39 @@ +import type { DeviceTypeExt, TSRDeviceTypesMap } from './index.js' + +/** + * A map of device types to their event types, derived from TSRDeviceTypesMap. This is used to type the events emitted by devices, to be listened on by `connectionEvent:stateEvent` + */ +export type TSREventTypesMap = { + [K in keyof TSRDeviceTypesMap]: 'Events' extends keyof TSRDeviceTypesMap[K] ? TSRDeviceTypesMap[K]['Events'] : never +} + +/** + * A union of all event types from all known device types. + */ +export type TSREventTypes = TSREventTypesMap[keyof TSREventTypesMap] + +/** + * Represents an event emitted by a device, to be listened on by `connectionEvent:stateEvent` + * Note: the payload can be null to indicate a return to TSR controlled state. + */ +export type TSRStateEvent> = { + [K in keyof TEventTypes]: { + deviceId: string // eg atem0 + deviceType: TDeviceType + event: K // the 'shared control address', or somethins like `me-program.1` + payload: TEventTypes[K] + /** + * Indicate whether this event is for a state from the timeline + */ + isFromTimeline: boolean + } +}[keyof TEventTypes] + +/** + * A union of all possible state events from all devices, to be listened on by `connectionEvent:stateEvent` + */ +export type SomeTSRStateEvent = TDevice extends keyof TSREventTypesMap + ? TSREventTypesMap[TDevice] extends Record + ? TSRStateEvent + : never + : never diff --git a/packages/timeline-state-resolver-types/src/expectedPlayoutItems.ts b/packages/timeline-state-resolver-types/src/expectedPlayoutItems.ts index 0d40e9cb8..0360a107c 100644 --- a/packages/timeline-state-resolver-types/src/expectedPlayoutItems.ts +++ b/packages/timeline-state-resolver-types/src/expectedPlayoutItems.ts @@ -1,3 +1,3 @@ -import { VIZMSEPlayoutItemContent } from './integrations/vizMSE.js' +import { VIZMSEPlayoutItemContent } from './integrations/vizMSE/timeline.js' export type ExpectedPlayoutItemContent = VIZMSEPlayoutItemContent diff --git a/packages/timeline-state-resolver-types/src/generated/atem.ts b/packages/timeline-state-resolver-types/src/generated/atem.ts index 978fe9950..8565395b7 100644 --- a/packages/timeline-state-resolver-types/src/generated/atem.ts +++ b/packages/timeline-state-resolver-types/src/generated/atem.ts @@ -4,6 +4,7 @@ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, * and re-run the "tsr-schema-types" tool to regenerate this file. */ +import type AtemEvents from '../integrations/atem/events.js' import type { ActionExecutionResult } from '../actions.js' import type { DeviceType } from './device-options.js' @@ -118,4 +119,5 @@ export interface AtemDeviceTypes { Options: AtemOptions Mappings: SomeMappingAtem Actions: AtemActionMethods + Events: AtemEvents } diff --git a/packages/timeline-state-resolver-types/src/generated/index.ts b/packages/timeline-state-resolver-types/src/generated/index.ts index 09221fac9..80b52969f 100644 --- a/packages/timeline-state-resolver-types/src/generated/index.ts +++ b/packages/timeline-state-resolver-types/src/generated/index.ts @@ -8,113 +8,120 @@ export * from './action-schema.js' export * from './generic-ptz-actions.js' export * from './device-options.js' +import type { DeviceType } from './device-options.js' export * from './abstract' -import type { SomeMappingAbstract } from './abstract.js' +import type { AbstractDeviceTypes } from './abstract.js' export * from './atem' -import type { SomeMappingAtem } from './atem.js' +import type { AtemDeviceTypes } from './atem.js' export * from './casparCG' -import type { SomeMappingCasparCG } from './casparCG.js' +import type { CasparCGDeviceTypes } from './casparCG.js' export * from './httpSend' -import type { SomeMappingHttpSend } from './httpSend.js' +import type { HttpSendDeviceTypes } from './httpSend.js' export * from './httpWatcher' -import type { SomeMappingHttpWatcher } from './httpWatcher.js' +import type { HttpWatcherDeviceTypes } from './httpWatcher.js' export * from './hyperdeck' -import type { SomeMappingHyperdeck } from './hyperdeck.js' +import type { HyperdeckDeviceTypes } from './hyperdeck.js' export * from './kairos' -import type { SomeMappingKairos } from './kairos.js' +import type { KairosDeviceTypes } from './kairos.js' export * from './lawo' -import type { SomeMappingLawo } from './lawo.js' +import type { LawoDeviceTypes } from './lawo.js' export * from './multiOsc' -import type { SomeMappingMultiOsc } from './multiOsc.js' +import type { MultiOscDeviceTypes } from './multiOsc.js' export * from './obs' -import type { SomeMappingObs } from './obs.js' +import type { ObsDeviceTypes } from './obs.js' export * from './ograf' -import type { SomeMappingOgraf } from './ograf.js' +import type { OgrafDeviceTypes } from './ograf.js' export * from './osc' -import type { SomeMappingOsc } from './osc.js' +import type { OscDeviceTypes } from './osc.js' export * from './panasonicPTZ' -import type { SomeMappingPanasonicPTZ } from './panasonicPTZ.js' +import type { PanasonicPTZDeviceTypes } from './panasonicPTZ.js' export * from './pharos' -import type { SomeMappingPharos } from './pharos.js' +import type { PharosDeviceTypes } from './pharos.js' export * from './quantel' -import type { SomeMappingQuantel } from './quantel.js' +import type { QuantelDeviceTypes } from './quantel.js' export * from './shotoku' -import type { SomeMappingShotoku } from './shotoku.js' +import type { ShotokuDeviceTypes } from './shotoku.js' export * from './singularLive' -import type { SomeMappingSingularLive } from './singularLive.js' +import type { SingularLiveDeviceTypes } from './singularLive.js' export * from './sisyfos' -import type { SomeMappingSisyfos } from './sisyfos.js' +import type { SisyfosDeviceTypes } from './sisyfos.js' export * from './sofieChef' -import type { SomeMappingSofieChef } from './sofieChef.js' +import type { SofieChefDeviceTypes } from './sofieChef.js' export * from './tcpSend' -import type { SomeMappingTcpSend } from './tcpSend.js' +import type { TcpSendDeviceTypes } from './tcpSend.js' export * from './telemetrics' -import type { SomeMappingTelemetrics } from './telemetrics.js' +import type { TelemetricsDeviceTypes } from './telemetrics.js' export * from './tricaster' -import type { SomeMappingTricaster } from './tricaster.js' +import type { TricasterDeviceTypes } from './tricaster.js' export * from './udpSend' -import type { SomeMappingUdpSend } from './udpSend.js' +import type { UdpSendDeviceTypes } from './udpSend.js' export * from './viscaOverIP' -import type { SomeMappingViscaOverIP } from './viscaOverIP.js' +import type { ViscaOverIPDeviceTypes } from './viscaOverIP.js' export * from './vizMSE' -import type { SomeMappingVizMSE } from './vizMSE.js' +import type { VizMSEDeviceTypes } from './vizMSE.js' export * from './vmix' -import type { SomeMappingVmix } from './vmix.js' +import type { VmixDeviceTypes } from './vmix.js' export * from './websocketClient' -import type { SomeMappingWebsocketClient } from './websocketClient.js' - -export type TSRMappingOptions = - | SomeMappingAbstract - | SomeMappingAtem - | SomeMappingCasparCG - | SomeMappingHttpSend - | SomeMappingHttpWatcher - | SomeMappingHyperdeck - | SomeMappingKairos - | SomeMappingLawo - | SomeMappingMultiOsc - | SomeMappingObs - | SomeMappingOgraf - | SomeMappingOsc - | SomeMappingPanasonicPTZ - | SomeMappingPharos - | SomeMappingQuantel - | SomeMappingShotoku - | SomeMappingSingularLive - | SomeMappingSisyfos - | SomeMappingSofieChef - | SomeMappingTcpSend - | SomeMappingTelemetrics - | SomeMappingTricaster - | SomeMappingUdpSend - | SomeMappingViscaOverIP - | SomeMappingVizMSE - | SomeMappingVmix - | SomeMappingWebsocketClient +import type { WebsocketClientDeviceTypes } from './websocketClient.js' + +/** + * A map of all built-in DeviceTypes. + * TSR plugins can augment this interface to add their own device types: + */ +export interface TSRDeviceTypesMap { + [DeviceType.ABSTRACT]: AbstractDeviceTypes + [DeviceType.ATEM]: AtemDeviceTypes + [DeviceType.CASPARCG]: CasparCGDeviceTypes + [DeviceType.HTTPSEND]: HttpSendDeviceTypes + [DeviceType.HTTPWATCHER]: HttpWatcherDeviceTypes + [DeviceType.HYPERDECK]: HyperdeckDeviceTypes + [DeviceType.KAIROS]: KairosDeviceTypes + [DeviceType.LAWO]: LawoDeviceTypes + [DeviceType.MULTI_OSC]: MultiOscDeviceTypes + [DeviceType.OBS]: ObsDeviceTypes + [DeviceType.OGRAF]: OgrafDeviceTypes + [DeviceType.OSC]: OscDeviceTypes + [DeviceType.PANASONIC_PTZ]: PanasonicPTZDeviceTypes + [DeviceType.PHAROS]: PharosDeviceTypes + [DeviceType.QUANTEL]: QuantelDeviceTypes + [DeviceType.SHOTOKU]: ShotokuDeviceTypes + [DeviceType.SINGULAR_LIVE]: SingularLiveDeviceTypes + [DeviceType.SISYFOS]: SisyfosDeviceTypes + [DeviceType.SOFIE_CHEF]: SofieChefDeviceTypes + [DeviceType.TCPSEND]: TcpSendDeviceTypes + [DeviceType.TELEMETRICS]: TelemetricsDeviceTypes + [DeviceType.TRICASTER]: TricasterDeviceTypes + [DeviceType.UDP_SEND]: UdpSendDeviceTypes + [DeviceType.VISCA_OVER_IP]: ViscaOverIPDeviceTypes + [DeviceType.VIZMSE]: VizMSEDeviceTypes + [DeviceType.VMIX]: VmixDeviceTypes + [DeviceType.WEBSOCKET_CLIENT]: WebsocketClientDeviceTypes +} + diff --git a/packages/timeline-state-resolver-types/src/index.ts b/packages/timeline-state-resolver-types/src/index.ts index 9e27d0e46..a1540eb69 100644 --- a/packages/timeline-state-resolver-types/src/index.ts +++ b/packages/timeline-state-resolver-types/src/index.ts @@ -2,71 +2,72 @@ import * as Timeline from './superfly-timeline/index.js' import { TSRTimelineObjProps } from './mapping.js' import { Content } from './superfly-timeline/index.js' -import { TimelineContentTelemetricsAny } from './integrations/telemetrics.js' -import { TimelineContentAtemAny } from './integrations/atem.js' -import { TimelineContentCasparCGAny } from './integrations/casparcg.js' -import { TimelineContentHTTPSendAny } from './integrations/httpSend.js' -import { TimelineContentTCPSendAny } from './integrations/tcpSend.js' -import { TimelineContentHyperdeckAny } from './integrations/hyperdeck.js' -import { TimelineContentLawoAny } from './integrations/lawo.js' -import { TimelineContentOSCAny } from './integrations/osc.js' -import { TimelineContentPharosAny } from './integrations/pharos.js' -import { TimelineContentPanasonicPtzAny } from './integrations/panasonicPTZ.js' -import { TimelineContentAbstractAny } from './integrations/abstract.js' -import { TimelineContentQuantelAny } from './integrations/quantel.js' -import { TimelineContentShotoku } from './integrations/shotoku.js' -import { TimelineContentSisyfosAny } from './integrations/sisyfos.js' -import { TimelineContentSofieChefAny } from './integrations/sofieChef.js' -import { TimelineContentVIZMSEAny } from './integrations/vizMSE.js' -import { TimelineContentSingularLiveAny } from './integrations/singularLive.js' -import { TimelineContentVMixAny } from './integrations/vmix.js' -import { TimelineContentOBSAny } from './integrations/obs.js' -import { TimelineContentTriCasterAny } from './integrations/tricaster.js' -import { TimelineContentWebSocketClientAny } from './integrations/websocketClient.js' -import { TimelineContentKairosAny } from './integrations/kairos.js' -import { DeviceType } from './generated/index.js' -import { TimelineContentUDPSendAny } from './integrations/udpSend.js' -import { TimelineContentOgrafAny } from './integrations/ograf.js' - -export * from './integrations/abstract.js' -export * from './integrations/atem.js' -export * from './integrations/casparcg.js' -export * from './integrations/httpSend.js' -export * from './integrations/httpWatcher.js' -export * from './integrations/hyperdeck.js' -export * from './integrations/kairos.js' -export * from './integrations/lawo.js' -export * from './integrations/ograf.js' -export * from './integrations/osc.js' -export * from './integrations/pharos.js' -export * from './integrations/panasonicPTZ.js' -export * from './integrations/sisyfos.js' -export * from './integrations/sofieChef.js' -export * from './integrations/quantel.js' -export * from './integrations/shotoku.js' -export * from './integrations/tcpSend.js' -export * from './integrations/vizMSE.js' -export * from './integrations/singularLive.js' -export * from './integrations/vmix.js' -export * from './integrations/obs.js' -export * from './integrations/tricaster.js' -export * from './integrations/telemetrics.js' -export * from './integrations/multiOsc.js' -export * from './integrations/udpSend.js' -export * from './integrations/viscaOverIP.js' -export * from './integrations/websocketClient.js' +import { TimelineContentTelemetricsAny } from './integrations/telemetrics/timeline.js' +import { TimelineContentAtemAny } from './integrations/atem/timeline.js' +import { TimelineContentCasparCGAny } from './integrations/casparcg/timeline.js' +import { TimelineContentHTTPSendAny } from './integrations/httpSend/timeline.js' +import { TimelineContentTCPSendAny } from './integrations/tcpSend/timeline.js' +import { TimelineContentHyperdeckAny } from './integrations/hyperdeck/timeline.js' +import { TimelineContentLawoAny } from './integrations/lawo/timeline.js' +import { TimelineContentOSCAny } from './integrations/osc/timeline.js' +import { TimelineContentPharosAny } from './integrations/pharos/timeline.js' +import { TimelineContentPanasonicPtzAny } from './integrations/panasonicPTZ/timeline.js' +import { TimelineContentAbstractAny } from './integrations/abstract/timeline.js' +import { TimelineContentQuantelAny } from './integrations/quantel/timeline.js' +import { TimelineContentShotoku } from './integrations/shotoku/timeline.js' +import { TimelineContentSisyfosAny } from './integrations/sisyfos/timeline.js' +import { TimelineContentSofieChefAny } from './integrations/sofieChef/timeline.js' +import { TimelineContentVIZMSEAny } from './integrations/vizMSE/timeline.js' +import { TimelineContentSingularLiveAny } from './integrations/singularLive/timeline.js' +import { TimelineContentVMixAny } from './integrations/vmix/timeline.js' +import { TimelineContentOBSAny } from './integrations/obs/timeline.js' +import { TimelineContentTriCasterAny } from './integrations/tricaster/timeline.js' +import { TimelineContentWebSocketClientAny } from './integrations/websocketClient/timeline.js' +import { TimelineContentKairosAny } from './integrations/kairos/timeline.js' +import { DeviceType, TSRDeviceTypesMap } from './generated/index.js' +import { TimelineContentUDPSendAny } from './integrations/udpSend/timeline.js' +import { TimelineContentOgrafAny } from './integrations/ograf/timeline.js' + +export * from './integrations/abstract/timeline.js' +export * from './integrations/atem/timeline.js' +export * from './integrations/casparcg/timeline.js' +export * from './integrations/httpSend/timeline.js' +export * from './integrations/httpWatcher/timeline.js' +export * from './integrations/hyperdeck/timeline.js' +export * from './integrations/kairos/timeline.js' +export * from './integrations/lawo/timeline.js' +export * from './integrations/ograf/timeline.js' +export * from './integrations/osc/timeline.js' +export * from './integrations/pharos/timeline.js' +export * from './integrations/panasonicPTZ/timeline.js' +export * from './integrations/sisyfos/timeline.js' +export * from './integrations/sofieChef/timeline.js' +export * from './integrations/quantel/timeline.js' +export * from './integrations/shotoku/timeline.js' +export * from './integrations/tcpSend/timeline.js' +export * from './integrations/vizMSE/timeline.js' +export * from './integrations/singularLive/timeline.js' +export * from './integrations/vmix/timeline.js' +export * from './integrations/obs/timeline.js' +export * from './integrations/tricaster/timeline.js' +export * from './integrations/telemetrics/timeline.js' +export * from './integrations/multiOsc/timeline.js' +export * from './integrations/udpSend/timeline.js' +export * from './integrations/viscaOverIP/timeline.js' +export * from './integrations/websocketClient/timeline.js' export * from './actions.js' export * from './datastore.js' export * from './device.js' export * from './deviceStatusDetail.js' +export * from './events.js' export * from './expectedPlayoutItems.js' export * from './mapping.js' export * from './mediaObject.js' export * from './templateString.js' export * from './translations.js' - export * from './generated/index.js' + export { Timeline } export interface TSRTimelineKeyframe extends Omit { @@ -140,6 +141,8 @@ export interface TimelineContentMap { export type TSRTimelineContent = TimelineContentMap[keyof TimelineContentMap] +export type TSRMappingOptions = TSRDeviceTypesMap[keyof TSRDeviceTypesMap]['Mappings'] + /** * A simple key value store that can be referred to from the timeline objects */ diff --git a/packages/timeline-state-resolver-types/src/integrations/abstract.ts b/packages/timeline-state-resolver-types/src/integrations/abstract/timeline.ts similarity index 76% rename from packages/timeline-state-resolver-types/src/integrations/abstract.ts rename to packages/timeline-state-resolver-types/src/integrations/abstract/timeline.ts index bc3a38bca..81a4ca279 100644 --- a/packages/timeline-state-resolver-types/src/integrations/abstract.ts +++ b/packages/timeline-state-resolver-types/src/integrations/abstract/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../generated/index.js' +import { DeviceType } from '../../generated/index.js' export type TimelineContentAbstractAny = TSRTimelineContentAbstract export interface TSRTimelineContentAbstract { diff --git a/packages/timeline-state-resolver-types/src/integrations/atem/events.ts b/packages/timeline-state-resolver-types/src/integrations/atem/events.ts new file mode 100644 index 000000000..861d1ebc4 --- /dev/null +++ b/packages/timeline-state-resolver-types/src/integrations/atem/events.ts @@ -0,0 +1,8 @@ +type AtemEvents = { + [key: `me.${number}.inputs`]: { + programInput: number + previewInput: number + } +} + +export default AtemEvents diff --git a/packages/timeline-state-resolver-types/src/integrations/atem.ts b/packages/timeline-state-resolver-types/src/integrations/atem/timeline.ts similarity index 99% rename from packages/timeline-state-resolver-types/src/integrations/atem.ts rename to packages/timeline-state-resolver-types/src/integrations/atem/timeline.ts index 63232e176..28f30f043 100644 --- a/packages/timeline-state-resolver-types/src/integrations/atem.ts +++ b/packages/timeline-state-resolver-types/src/integrations/atem/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for ATEM device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/casparcg.ts b/packages/timeline-state-resolver-types/src/integrations/casparcg/timeline.ts similarity index 99% rename from packages/timeline-state-resolver-types/src/integrations/casparcg.ts rename to packages/timeline-state-resolver-types/src/integrations/casparcg/timeline.ts index 4cb023cae..68dda31f1 100644 --- a/packages/timeline-state-resolver-types/src/integrations/casparcg.ts +++ b/packages/timeline-state-resolver-types/src/integrations/casparcg/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType, TemplateString } from '../index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType, TemplateString } from '../../index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for CasparCG device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/httpSend.ts b/packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts similarity index 95% rename from packages/timeline-state-resolver-types/src/integrations/httpSend.ts rename to packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts index bebe0d4b0..8013dddcd 100644 --- a/packages/timeline-state-resolver-types/src/integrations/httpSend.ts +++ b/packages/timeline-state-resolver-types/src/integrations/httpSend/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceType, HTTPSendCommandContent, TemplateString } from '../index.js' +import { DeviceType, HTTPSendCommandContent, TemplateString } from '../../index.js' export type TimelineContentHTTPSendAny = TimelineContentHTTPRequest export interface TimelineContentHTTPSendBase { diff --git a/packages/timeline-state-resolver-types/src/integrations/httpWatcher.ts b/packages/timeline-state-resolver-types/src/integrations/httpWatcher/timeline.ts similarity index 96% rename from packages/timeline-state-resolver-types/src/integrations/httpWatcher.ts rename to packages/timeline-state-resolver-types/src/integrations/httpWatcher/timeline.ts index 294054a1a..48d54668f 100644 --- a/packages/timeline-state-resolver-types/src/integrations/httpWatcher.ts +++ b/packages/timeline-state-resolver-types/src/integrations/httpWatcher/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' export const HTTPWatcherStatusCode = { URI_NOT_SET: 'DEVICE_HTTPWATCHER_URI_NOT_SET', diff --git a/packages/timeline-state-resolver-types/src/integrations/hyperdeck.ts b/packages/timeline-state-resolver-types/src/integrations/hyperdeck/timeline.ts similarity index 97% rename from packages/timeline-state-resolver-types/src/integrations/hyperdeck.ts rename to packages/timeline-state-resolver-types/src/integrations/hyperdeck/timeline.ts index 32bc20ad0..c5a622b26 100644 --- a/packages/timeline-state-resolver-types/src/integrations/hyperdeck.ts +++ b/packages/timeline-state-resolver-types/src/integrations/hyperdeck/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for Hyperdeck device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/kairos.ts b/packages/timeline-state-resolver-types/src/integrations/kairos/timeline.ts similarity index 99% rename from packages/timeline-state-resolver-types/src/integrations/kairos.ts rename to packages/timeline-state-resolver-types/src/integrations/kairos/timeline.ts index d3191c4ec..f5b17f6c7 100644 --- a/packages/timeline-state-resolver-types/src/integrations/kairos.ts +++ b/packages/timeline-state-resolver-types/src/integrations/kairos/timeline.ts @@ -1,4 +1,4 @@ -import type { DeviceType } from '../index.js' +import { DeviceType } from '../../generated/index.js' import type { RefPath, MediaClipRef, diff --git a/packages/timeline-state-resolver-types/src/integrations/lawo.ts b/packages/timeline-state-resolver-types/src/integrations/lawo/timeline.ts similarity index 95% rename from packages/timeline-state-resolver-types/src/integrations/lawo.ts rename to packages/timeline-state-resolver-types/src/integrations/lawo/timeline.ts index 164a7d3d4..ce119a6c4 100644 --- a/packages/timeline-state-resolver-types/src/integrations/lawo.ts +++ b/packages/timeline-state-resolver-types/src/integrations/lawo/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import type { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import type { DeviceStatusDetail } from '../../deviceStatusDetail.js' export type EmberValue = number | string | boolean | Buffer | null enum ParameterType { diff --git a/packages/timeline-state-resolver-types/src/integrations/multiOsc.ts b/packages/timeline-state-resolver-types/src/integrations/multiOsc/timeline.ts similarity index 100% rename from packages/timeline-state-resolver-types/src/integrations/multiOsc.ts rename to packages/timeline-state-resolver-types/src/integrations/multiOsc/timeline.ts diff --git a/packages/timeline-state-resolver-types/src/integrations/obs.ts b/packages/timeline-state-resolver-types/src/integrations/obs/timeline.ts similarity index 97% rename from packages/timeline-state-resolver-types/src/integrations/obs.ts rename to packages/timeline-state-resolver-types/src/integrations/obs/timeline.ts index f891bd10c..06a2de8d2 100644 --- a/packages/timeline-state-resolver-types/src/integrations/obs.ts +++ b/packages/timeline-state-resolver-types/src/integrations/obs/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for OBS device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/ograf.ts b/packages/timeline-state-resolver-types/src/integrations/ograf/timeline.ts similarity index 97% rename from packages/timeline-state-resolver-types/src/integrations/ograf.ts rename to packages/timeline-state-resolver-types/src/integrations/ograf/timeline.ts index 526a4ea0a..d36e714ab 100644 --- a/packages/timeline-state-resolver-types/src/integrations/ograf.ts +++ b/packages/timeline-state-resolver-types/src/integrations/ograf/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../generated/index.js' +import { DeviceType } from '../../generated/index.js' export type TimelineContentOgrafAny = | TimelineContentOGrafGraphic diff --git a/packages/timeline-state-resolver-types/src/integrations/osc.ts b/packages/timeline-state-resolver-types/src/integrations/osc/timeline.ts similarity index 95% rename from packages/timeline-state-resolver-types/src/integrations/osc.ts rename to packages/timeline-state-resolver-types/src/integrations/osc/timeline.ts index 8ff3bfb34..c0e5e42e5 100644 --- a/packages/timeline-state-resolver-types/src/integrations/osc.ts +++ b/packages/timeline-state-resolver-types/src/integrations/osc/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for OSC device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/panasonicPTZ.ts b/packages/timeline-state-resolver-types/src/integrations/panasonicPTZ/timeline.ts similarity index 94% rename from packages/timeline-state-resolver-types/src/integrations/panasonicPTZ.ts rename to packages/timeline-state-resolver-types/src/integrations/panasonicPTZ/timeline.ts index 5aabc50a1..6df8dccfb 100644 --- a/packages/timeline-state-resolver-types/src/integrations/panasonicPTZ.ts +++ b/packages/timeline-state-resolver-types/src/integrations/panasonicPTZ/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for Panasonic PTZ device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/pharos.ts b/packages/timeline-state-resolver-types/src/integrations/pharos/timeline.ts similarity index 91% rename from packages/timeline-state-resolver-types/src/integrations/pharos.ts rename to packages/timeline-state-resolver-types/src/integrations/pharos/timeline.ts index 59992b7fd..346f2a4b3 100644 --- a/packages/timeline-state-resolver-types/src/integrations/pharos.ts +++ b/packages/timeline-state-resolver-types/src/integrations/pharos/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import type { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import type { DeviceStatusDetail } from '../../deviceStatusDetail.js' export enum TimelineContentTypePharos { SCENE = 'scene', diff --git a/packages/timeline-state-resolver-types/src/integrations/quantel.ts b/packages/timeline-state-resolver-types/src/integrations/quantel/timeline.ts similarity index 94% rename from packages/timeline-state-resolver-types/src/integrations/quantel.ts rename to packages/timeline-state-resolver-types/src/integrations/quantel/timeline.ts index 9b0b4171c..e7c0b40b6 100644 --- a/packages/timeline-state-resolver-types/src/integrations/quantel.ts +++ b/packages/timeline-state-resolver-types/src/integrations/quantel/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import type { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import type { DeviceStatusDetail } from '../../deviceStatusDetail.js' export type TimelineContentQuantelAny = TimelineContentQuantelClip export interface TimelineContentQuantelClip { diff --git a/packages/timeline-state-resolver-types/src/integrations/shotoku.ts b/packages/timeline-state-resolver-types/src/integrations/shotoku/timeline.ts similarity index 91% rename from packages/timeline-state-resolver-types/src/integrations/shotoku.ts rename to packages/timeline-state-resolver-types/src/integrations/shotoku/timeline.ts index 8265345b5..465b6512a 100644 --- a/packages/timeline-state-resolver-types/src/integrations/shotoku.ts +++ b/packages/timeline-state-resolver-types/src/integrations/shotoku/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import type { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import type { DeviceStatusDetail } from '../../deviceStatusDetail.js' export enum TimelineContentTypeShotoku { SHOT = 'shot', diff --git a/packages/timeline-state-resolver-types/src/integrations/singularLive.ts b/packages/timeline-state-resolver-types/src/integrations/singularLive/timeline.ts similarity index 94% rename from packages/timeline-state-resolver-types/src/integrations/singularLive.ts rename to packages/timeline-state-resolver-types/src/integrations/singularLive/timeline.ts index afbd1f620..65becd603 100644 --- a/packages/timeline-state-resolver-types/src/integrations/singularLive.ts +++ b/packages/timeline-state-resolver-types/src/integrations/singularLive/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../generated/index.js' +import { DeviceType } from '../../generated/index.js' export interface SingularLiveContent { type: TimelineContentTypeSingularLive diff --git a/packages/timeline-state-resolver-types/src/integrations/sisyfos.ts b/packages/timeline-state-resolver-types/src/integrations/sisyfos/timeline.ts similarity index 96% rename from packages/timeline-state-resolver-types/src/integrations/sisyfos.ts rename to packages/timeline-state-resolver-types/src/integrations/sisyfos/timeline.ts index d034ed849..b4759a001 100644 --- a/packages/timeline-state-resolver-types/src/integrations/sisyfos.ts +++ b/packages/timeline-state-resolver-types/src/integrations/sisyfos/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for Sisyfos device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/sofieChef.ts b/packages/timeline-state-resolver-types/src/integrations/sofieChef/timeline.ts similarity index 93% rename from packages/timeline-state-resolver-types/src/integrations/sofieChef.ts rename to packages/timeline-state-resolver-types/src/integrations/sofieChef/timeline.ts index 8576041af..1894b6e8b 100644 --- a/packages/timeline-state-resolver-types/src/integrations/sofieChef.ts +++ b/packages/timeline-state-resolver-types/src/integrations/sofieChef/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType, TemplateString } from '../index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType, TemplateString } from '../../index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for SofieChef device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/tcpSend.ts b/packages/timeline-state-resolver-types/src/integrations/tcpSend/timeline.ts similarity index 89% rename from packages/timeline-state-resolver-types/src/integrations/tcpSend.ts rename to packages/timeline-state-resolver-types/src/integrations/tcpSend/timeline.ts index f3fddc18a..dcd0fcd97 100644 --- a/packages/timeline-state-resolver-types/src/integrations/tcpSend.ts +++ b/packages/timeline-state-resolver-types/src/integrations/tcpSend/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../generated/index.js' +import { DeviceType } from '../../generated/index.js' export type TimelineContentTCPSendAny = TimelineContentTCPRequest export interface TimelineContentTCPSendBase { diff --git a/packages/timeline-state-resolver-types/src/integrations/telemetrics.ts b/packages/timeline-state-resolver-types/src/integrations/telemetrics/timeline.ts similarity index 92% rename from packages/timeline-state-resolver-types/src/integrations/telemetrics.ts rename to packages/timeline-state-resolver-types/src/integrations/telemetrics/timeline.ts index cf27c41fa..2e5c75108 100644 --- a/packages/timeline-state-resolver-types/src/integrations/telemetrics.ts +++ b/packages/timeline-state-resolver-types/src/integrations/telemetrics/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for Telemetrics device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/tricaster.ts b/packages/timeline-state-resolver-types/src/integrations/tricaster/timeline.ts similarity index 98% rename from packages/timeline-state-resolver-types/src/integrations/tricaster.ts rename to packages/timeline-state-resolver-types/src/integrations/tricaster/timeline.ts index 23a823f1a..923081723 100644 --- a/packages/timeline-state-resolver-types/src/integrations/tricaster.ts +++ b/packages/timeline-state-resolver-types/src/integrations/tricaster/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import type { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import type { DeviceStatusDetail } from '../../deviceStatusDetail.js' export type TriCasterMixEffectName = 'main' | `v${number}` export type TriCasterKeyerName = `dsk${number}` diff --git a/packages/timeline-state-resolver-types/src/integrations/udpSend.ts b/packages/timeline-state-resolver-types/src/integrations/udpSend/timeline.ts similarity index 88% rename from packages/timeline-state-resolver-types/src/integrations/udpSend.ts rename to packages/timeline-state-resolver-types/src/integrations/udpSend/timeline.ts index 7c6d59ba3..17f57bc8b 100644 --- a/packages/timeline-state-resolver-types/src/integrations/udpSend.ts +++ b/packages/timeline-state-resolver-types/src/integrations/udpSend/timeline.ts @@ -1,4 +1,4 @@ -import type { DeviceType } from '../generated/index.js' +import type { DeviceType } from '../../generated/index.js' export type TimelineContentUDPSendAny = TimelineContentUDPRequest export interface TimelineContentUDPSendBase { diff --git a/packages/timeline-state-resolver-types/src/integrations/viscaOverIP.ts b/packages/timeline-state-resolver-types/src/integrations/viscaOverIP/timeline.ts similarity index 81% rename from packages/timeline-state-resolver-types/src/integrations/viscaOverIP.ts rename to packages/timeline-state-resolver-types/src/integrations/viscaOverIP/timeline.ts index c876d3131..ab55a6c92 100644 --- a/packages/timeline-state-resolver-types/src/integrations/viscaOverIP.ts +++ b/packages/timeline-state-resolver-types/src/integrations/viscaOverIP/timeline.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../generated/index.js' +import { DeviceType } from '../../generated/index.js' export type TimelineContentViscaOverIpAny = TimelineContentViscaOverIp export interface TimelineContentViscaOverIpBase { diff --git a/packages/timeline-state-resolver-types/src/integrations/vizMSE.ts b/packages/timeline-state-resolver-types/src/integrations/vizMSE/timeline.ts similarity index 98% rename from packages/timeline-state-resolver-types/src/integrations/vizMSE.ts rename to packages/timeline-state-resolver-types/src/integrations/vizMSE/timeline.ts index 3f6dbfa55..167c964e1 100644 --- a/packages/timeline-state-resolver-types/src/integrations/vizMSE.ts +++ b/packages/timeline-state-resolver-types/src/integrations/vizMSE/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for Viz MSE device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/vmix.ts b/packages/timeline-state-resolver-types/src/integrations/vmix/timeline.ts similarity index 98% rename from packages/timeline-state-resolver-types/src/integrations/vmix.ts rename to packages/timeline-state-resolver-types/src/integrations/vmix/timeline.ts index 742fb40ef..ea81d1714 100644 --- a/packages/timeline-state-resolver-types/src/integrations/vmix.ts +++ b/packages/timeline-state-resolver-types/src/integrations/vmix/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import { DeviceStatusDetail } from '../../deviceStatusDetail.js' /** * Status codes for VMix device issues. diff --git a/packages/timeline-state-resolver-types/src/integrations/websocketClient.ts b/packages/timeline-state-resolver-types/src/integrations/websocketClient/timeline.ts similarity index 92% rename from packages/timeline-state-resolver-types/src/integrations/websocketClient.ts rename to packages/timeline-state-resolver-types/src/integrations/websocketClient/timeline.ts index b3cdf06c6..8f4823a34 100644 --- a/packages/timeline-state-resolver-types/src/integrations/websocketClient.ts +++ b/packages/timeline-state-resolver-types/src/integrations/websocketClient/timeline.ts @@ -1,5 +1,5 @@ -import { DeviceType } from '../generated/index.js' -import type { DeviceStatusDetail } from '../deviceStatusDetail.js' +import { DeviceType } from '../../generated/index.js' +import type { DeviceStatusDetail } from '../../deviceStatusDetail.js' export enum TimelineContentTypeWebSocketClient { WEBSOCKET_MESSAGE = 'websocketMessage', diff --git a/packages/timeline-state-resolver/src/__tests__/mockDeviceInstanceWrapper.ts b/packages/timeline-state-resolver/src/__tests__/mockDeviceInstanceWrapper.ts index a27a2e696..0cc10e33e 100644 --- a/packages/timeline-state-resolver/src/__tests__/mockDeviceInstanceWrapper.ts +++ b/packages/timeline-state-resolver/src/__tests__/mockDeviceInstanceWrapper.ts @@ -108,6 +108,10 @@ export class MockDeviceInstanceWrapper setDebugState = jest.fn((_value: boolean): void => { throw new Error('Method not implemented.') }) + + setEventSubscriptions = jest.fn((_events: string[]): void => { + throw new Error('Method not implemented.') + }) } export function DiscardAllMockDevices(): void { diff --git a/packages/timeline-state-resolver/src/conductor.ts b/packages/timeline-state-resolver/src/conductor.ts index 2971d649c..511954cd6 100644 --- a/packages/timeline-state-resolver/src/conductor.ts +++ b/packages/timeline-state-resolver/src/conductor.ts @@ -339,6 +339,14 @@ export class Conductor extends EventEmitter { this.connectionManager.setConnections({}) } + /** + * Subscribe to state events for a specific device. Only events with names in the + * provided list will be emitted via the `stateEvent` event. + */ + public async setDeviceEventSubscriptions(deviceId: string, events: string[]): Promise { + return this.connectionManager.setDeviceEventSubscriptions(deviceId, events) + } + /** * Resets the resolve-time, so that the resolving will happen for the point-in time NOW * next time diff --git a/packages/timeline-state-resolver/src/devices/device.ts b/packages/timeline-state-resolver/src/devices/device.ts index 7d1d1cbeb..956144b33 100644 --- a/packages/timeline-state-resolver/src/devices/device.ts +++ b/packages/timeline-state-resolver/src/devices/device.ts @@ -202,6 +202,10 @@ export abstract class Device< this.debugState = debug } + setEventSubscriptions(_events: string[]): void { + // no-op: legacy Device subclasses do not emit state events + } + protected emitDebugState(state: object) { if (this.debugState) { this.emit('debugState', state) diff --git a/packages/timeline-state-resolver/src/integrations/__tests__/testlib.ts b/packages/timeline-state-resolver/src/integrations/__tests__/testlib.ts index f78576322..d605f11eb 100644 --- a/packages/timeline-state-resolver/src/integrations/__tests__/testlib.ts +++ b/packages/timeline-state-resolver/src/integrations/__tests__/testlib.ts @@ -3,9 +3,9 @@ import { DeviceContextAPI } from 'timeline-state-resolver-api' /** A default context for devices used in unit tests */ -export function getDeviceContext(): MockProxy> { +export function getDeviceContext(): MockProxy> { // only properties (functions) needing a specific default (return) value, incl. async ones need to be explicitly set in the first arg - return mockDeep>({ + return mockDeep>({ deviceName: 'Test Device', getCurrentTime: () => Date.now(), }) diff --git a/packages/timeline-state-resolver/src/integrations/abstract/index.ts b/packages/timeline-state-resolver/src/integrations/abstract/index.ts index 990a40cd6..9000e49eb 100644 --- a/packages/timeline-state-resolver/src/integrations/abstract/index.ts +++ b/packages/timeline-state-resolver/src/integrations/abstract/index.ts @@ -36,7 +36,7 @@ export class AbstractDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/atem/index.ts b/packages/timeline-state-resolver/src/integrations/atem/index.ts index 37d53e80c..d8bbe7f14 100644 --- a/packages/timeline-state-resolver/src/integrations/atem/index.ts +++ b/packages/timeline-state-resolver/src/integrations/atem/index.ts @@ -62,7 +62,7 @@ export class AtemDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } @@ -258,6 +258,27 @@ export class AtemDevice implements Device) { const psus = newState.info.power || [] diff --git a/packages/timeline-state-resolver/src/integrations/httpSend/index.ts b/packages/timeline-state-resolver/src/integrations/httpSend/index.ts index 9c5e55c12..1bc152f2a 100644 --- a/packages/timeline-state-resolver/src/integrations/httpSend/index.ts +++ b/packages/timeline-state-resolver/src/integrations/httpSend/index.ts @@ -47,7 +47,7 @@ export class HTTPSendDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/httpWatcher/index.ts b/packages/timeline-state-resolver/src/integrations/httpWatcher/index.ts index 67b926130..9b2960ebf 100644 --- a/packages/timeline-state-resolver/src/integrations/httpWatcher/index.ts +++ b/packages/timeline-state-resolver/src/integrations/httpWatcher/index.ts @@ -39,7 +39,7 @@ export class HTTPWatcherDevice implements Device< private status: StatusCode = StatusCode.UNKNOWN private statusDetails: HTTPWatcherStatusDetail[] = [] - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/hyperdeck/index.ts b/packages/timeline-state-resolver/src/integrations/hyperdeck/index.ts index 551565ee7..ca2fb2848 100644 --- a/packages/timeline-state-resolver/src/integrations/hyperdeck/index.ts +++ b/packages/timeline-state-resolver/src/integrations/hyperdeck/index.ts @@ -51,7 +51,7 @@ export class HyperdeckDevice implements Device< private _expectedTransportStatus: TransportStatus | undefined private _suppressEmptySlotWarnings = false - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/kairos/index.ts b/packages/timeline-state-resolver/src/integrations/kairos/index.ts index a7ae03e80..512780cb4 100644 --- a/packages/timeline-state-resolver/src/integrations/kairos/index.ts +++ b/packages/timeline-state-resolver/src/integrations/kairos/index.ts @@ -28,7 +28,7 @@ export class KairosDevice implements Device) { + constructor(public context: DeviceContextAPI) { this._kairos = new KairosConnection() this._kairosRamLoader = new KairosRamLoader(this._kairos, context) diff --git a/packages/timeline-state-resolver/src/integrations/kairos/kairos-application-monitor.ts b/packages/timeline-state-resolver/src/integrations/kairos/kairos-application-monitor.ts index 4cc915ed8..37386f65d 100644 --- a/packages/timeline-state-resolver/src/integrations/kairos/kairos-application-monitor.ts +++ b/packages/timeline-state-resolver/src/integrations/kairos/kairos-application-monitor.ts @@ -23,6 +23,7 @@ import { AuxRef, DeviceStatus, DeviceType, + KairosDeviceTypes, Mapping, MappingKairosType, Mappings, @@ -88,7 +89,7 @@ export class KairosApplicationMonitor extends EventEmitter, + private readonly context: DeviceContextAPI, private readonly kairos: KairosConnection ) { super() diff --git a/packages/timeline-state-resolver/src/integrations/kairos/lib/kairosRamLoader.ts b/packages/timeline-state-resolver/src/integrations/kairos/lib/kairosRamLoader.ts index c16ecc08b..5b2832b70 100644 --- a/packages/timeline-state-resolver/src/integrations/kairos/lib/kairosRamLoader.ts +++ b/packages/timeline-state-resolver/src/integrations/kairos/lib/kairosRamLoader.ts @@ -10,13 +10,14 @@ import { } from 'kairos-connection' import { KairosDeviceState } from '../stateBuilder.js' import { DeviceContextAPI } from 'timeline-state-resolver-api' +import { KairosDeviceTypes } from 'timeline-state-resolver-types' export class KairosRamLoader { private debounceTrackLoadRAM = new Set() constructor( private kairos: KairosConnection, - private context: DeviceContextAPI + private context: DeviceContextAPI ) {} async ensureRAMLoaded( diff --git a/packages/timeline-state-resolver/src/integrations/lawo/index.ts b/packages/timeline-state-resolver/src/integrations/lawo/index.ts index 2d5c424f9..bd13b99af 100644 --- a/packages/timeline-state-resolver/src/integrations/lawo/index.ts +++ b/packages/timeline-state-resolver/src/integrations/lawo/index.ts @@ -21,7 +21,7 @@ const debug = Debug('timeline-state-resolver:lawo') export class LawoDevice implements Device { private _lawo: LawoConnection | undefined - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/multiOsc/index.ts b/packages/timeline-state-resolver/src/integrations/multiOsc/index.ts index 62ac068fa..5d4a3757a 100644 --- a/packages/timeline-state-resolver/src/integrations/multiOsc/index.ts +++ b/packages/timeline-state-resolver/src/integrations/multiOsc/index.ts @@ -49,7 +49,7 @@ export class MultiOSCMessageDevice implements Device< private _timeBetweenCommands: number | undefined - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/obs/index.ts b/packages/timeline-state-resolver/src/integrations/obs/index.ts index b5884428a..595a73ccd 100644 --- a/packages/timeline-state-resolver/src/integrations/obs/index.ts +++ b/packages/timeline-state-resolver/src/integrations/obs/index.ts @@ -28,7 +28,7 @@ export class OBSDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/ograf/index.ts b/packages/timeline-state-resolver/src/integrations/ograf/index.ts index 13a52a35b..5bea1f59a 100644 --- a/packages/timeline-state-resolver/src/integrations/ograf/index.ts +++ b/packages/timeline-state-resolver/src/integrations/ograf/index.ts @@ -40,7 +40,7 @@ export class OGrafDevice implements Device) { + constructor(protected context: DeviceContextAPI) { this.ografConnectionStatus.on('connected', () => this._connectionChanged()) this.ografConnectionStatus.on('disconnected', () => this._connectionChanged()) this.ografConnectionStatus.on('error', (err) => this.context.logger.error('OGrafConnectionStatus', err)) diff --git a/packages/timeline-state-resolver/src/integrations/osc/index.ts b/packages/timeline-state-resolver/src/integrations/osc/index.ts index 3a3d6628d..778e6243d 100644 --- a/packages/timeline-state-resolver/src/integrations/osc/index.ts +++ b/packages/timeline-state-resolver/src/integrations/osc/index.ts @@ -43,7 +43,7 @@ export class OscDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/panasonicPTZ/index.ts b/packages/timeline-state-resolver/src/integrations/panasonicPTZ/index.ts index 9c87a41fb..813ad3db3 100644 --- a/packages/timeline-state-resolver/src/integrations/panasonicPTZ/index.ts +++ b/packages/timeline-state-resolver/src/integrations/panasonicPTZ/index.ts @@ -54,7 +54,7 @@ export class PanasonicPtzDevice implements Device< private _host: string | undefined private _port: number | undefined - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/pharos/index.ts b/packages/timeline-state-resolver/src/integrations/pharos/index.ts index 259749e58..66122c27e 100644 --- a/packages/timeline-state-resolver/src/integrations/pharos/index.ts +++ b/packages/timeline-state-resolver/src/integrations/pharos/index.ts @@ -35,7 +35,7 @@ export class PharosDevice implements Device) { + constructor(protected context: DeviceContextAPI) { this._pharos = new Pharos() this._pharos.on('error', (e) => this.context.logger.error('Pharos', e)) this._pharos.on('connected', () => { diff --git a/packages/timeline-state-resolver/src/integrations/quantel/index.ts b/packages/timeline-state-resolver/src/integrations/quantel/index.ts index 426b9ec73..97e0c6f43 100644 --- a/packages/timeline-state-resolver/src/integrations/quantel/index.ts +++ b/packages/timeline-state-resolver/src/integrations/quantel/index.ts @@ -43,7 +43,7 @@ export class QuantelDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/shotoku/index.ts b/packages/timeline-state-resolver/src/integrations/shotoku/index.ts index 5a96e0d2e..980ae2f54 100644 --- a/packages/timeline-state-resolver/src/integrations/shotoku/index.ts +++ b/packages/timeline-state-resolver/src/integrations/shotoku/index.ts @@ -30,7 +30,7 @@ export type ShotokuCommandWithContext = CommandWithContext { private readonly _shotoku = new ShotokuAPI() - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/singularLive/index.ts b/packages/timeline-state-resolver/src/integrations/singularLive/index.ts index a4f6b349d..0bc03b8dc 100644 --- a/packages/timeline-state-resolver/src/integrations/singularLive/index.ts +++ b/packages/timeline-state-resolver/src/integrations/singularLive/index.ts @@ -58,7 +58,7 @@ export class SingularLiveDevice implements Device< private _accessToken: string | undefined - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/sisyfos/__tests__/sisyfos.spec.ts b/packages/timeline-state-resolver/src/integrations/sisyfos/__tests__/sisyfos.spec.ts index 95720f11a..ae7cae091 100644 --- a/packages/timeline-state-resolver/src/integrations/sisyfos/__tests__/sisyfos.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/sisyfos/__tests__/sisyfos.spec.ts @@ -1487,7 +1487,7 @@ describe('Sisyfos', () => { }) }) -function getSisyfosDevice(mockContext?: DeviceContextAPI) { +function getSisyfosDevice(mockContext?: DeviceContextAPI) { const dev = new SisyfosMessageDevice(mockContext ?? getDeviceContext()) return dev } diff --git a/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts b/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts index a57e82fe1..086e5be6c 100644 --- a/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts +++ b/packages/timeline-state-resolver/src/integrations/sisyfos/index.ts @@ -35,9 +35,9 @@ export class SisyfosMessageDevice implements Device['logger'] + private logger: DeviceContextAPI['logger'] - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { this.logger = this.context.logger // just for convenience this._sisyfos = new SisyfosApi() diff --git a/packages/timeline-state-resolver/src/integrations/sofieChef/index.ts b/packages/timeline-state-resolver/src/integrations/sofieChef/index.ts index f9fc972fb..d7dd49ff1 100644 --- a/packages/timeline-state-resolver/src/integrations/sofieChef/index.ts +++ b/packages/timeline-state-resolver/src/integrations/sofieChef/index.ts @@ -80,7 +80,7 @@ export class SofieChefDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/tcpSend/index.ts b/packages/timeline-state-resolver/src/integrations/tcpSend/index.ts index 42c039c24..f28d28fa3 100644 --- a/packages/timeline-state-resolver/src/integrations/tcpSend/index.ts +++ b/packages/timeline-state-resolver/src/integrations/tcpSend/index.ts @@ -37,7 +37,7 @@ export class TcpSendDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing this.tcpConnection.on('error', (errContext, err) => { diff --git a/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts b/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts index dd0928686..86255f233 100644 --- a/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts +++ b/packages/timeline-state-resolver/src/integrations/telemetrics/index.ts @@ -40,7 +40,7 @@ export class TelemetricsDevice implements Device< private retryConnectionTimer: NodeJS.Timeout | undefined - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/tricaster/index.ts b/packages/timeline-state-resolver/src/integrations/tricaster/index.ts index 3dff13ab7..144125045 100644 --- a/packages/timeline-state-resolver/src/integrations/tricaster/index.ts +++ b/packages/timeline-state-resolver/src/integrations/tricaster/index.ts @@ -31,7 +31,7 @@ export class TriCasterDevice implements Device< private _connection?: TriCasterConnection private _stateDiffer?: TriCasterStateDiffer - constructor(protected context: DeviceContextAPI>) { + constructor(protected context: DeviceContextAPI>) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/udpSend/index.ts b/packages/timeline-state-resolver/src/integrations/udpSend/index.ts index 683287218..1f30ebcac 100644 --- a/packages/timeline-state-resolver/src/integrations/udpSend/index.ts +++ b/packages/timeline-state-resolver/src/integrations/udpSend/index.ts @@ -37,7 +37,7 @@ export class UdpSendDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing this.udpConnection.on('error', (errContext, err) => { diff --git a/packages/timeline-state-resolver/src/integrations/viscaOverIP/index.ts b/packages/timeline-state-resolver/src/integrations/viscaOverIP/index.ts index db9e9e6bd..2a5067981 100644 --- a/packages/timeline-state-resolver/src/integrations/viscaOverIP/index.ts +++ b/packages/timeline-state-resolver/src/integrations/viscaOverIP/index.ts @@ -36,7 +36,7 @@ export class ViscaOverIpDevice implements Device) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/integrations/vmix/index.ts b/packages/timeline-state-resolver/src/integrations/vmix/index.ts index 585a4f909..05d24ac89 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/index.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/index.ts @@ -52,9 +52,9 @@ export class VMixDevice implements Device['logger'] + private logger: DeviceContextAPI['logger'] - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { this.logger = this.context.logger // just for convenience this._stateDiffer = new VMixStateDiffer(() => this.context.getCurrentTime(), this._sendCommands) diff --git a/packages/timeline-state-resolver/src/integrations/websocketClient/index.ts b/packages/timeline-state-resolver/src/integrations/websocketClient/index.ts index 446209a1a..41d286f94 100644 --- a/packages/timeline-state-resolver/src/integrations/websocketClient/index.ts +++ b/packages/timeline-state-resolver/src/integrations/websocketClient/index.ts @@ -37,7 +37,7 @@ export class WebSocketClientDevice implements Device< // Use ! as the connection will be initialized in init: private connection: WebSocketConnection | undefined - constructor(protected context: DeviceContextAPI) { + constructor(protected context: DeviceContextAPI) { // Nothing } diff --git a/packages/timeline-state-resolver/src/service/ConnectionManager.ts b/packages/timeline-state-resolver/src/service/ConnectionManager.ts index 0668c12ce..9b3a53a46 100644 --- a/packages/timeline-state-resolver/src/service/ConnectionManager.ts +++ b/packages/timeline-state-resolver/src/service/ConnectionManager.ts @@ -91,6 +91,12 @@ export class ConnectionManager extends EventEmitter { } } + public async setDeviceEventSubscriptions(deviceId: string, events: string[]): Promise { + const connection = this.getConnection(deviceId) + if (!connection) throw new Error(`No initialized connection found for device "${deviceId}"`) + await connection.setEventSubscriptions(events) + } + /** * Iterate over config and check that the existing connection has the right config, if * not... recreate it @@ -363,6 +369,7 @@ export class ConnectionManager extends EventEmitter { passEvent('updateMediaObject') passEvent('clearMediaObjects') passEvent('timeTrace') + passEvent('stateEvent') } } diff --git a/packages/timeline-state-resolver/src/service/DeviceInstance.ts b/packages/timeline-state-resolver/src/service/DeviceInstance.ts index aabc3f340..e746c112f 100644 --- a/packages/timeline-state-resolver/src/service/DeviceInstance.ts +++ b/packages/timeline-state-resolver/src/service/DeviceInstance.ts @@ -18,8 +18,9 @@ import { type TSRTimelineContent, } from 'timeline-state-resolver-types' import { StateHandler } from './stateHandler.js' +import { StateEventHandler } from './stateEventHandler.js' import { DevicesDict } from './devices.js' -import type { DeviceOptionsAny, ExpectedPlayoutItem } from '../index.js' +import type { DeviceOptionsAny, DeviceTypeExt, ExpectedPlayoutItem, SomeTSRStateEvent } from '../index.js' import type { StateChangeReport } from './measure.js' import { StateTracker } from './stateTracker.js' @@ -66,6 +67,8 @@ export interface DeviceDetails { export interface DeviceInstanceEvents extends Omit { /** The connection status has changed */ connectionChanged: [status: DeviceStatus] + /** A state event has occurred */ + stateEvent: [events: SomeTSRStateEvent[]] } // Future: it would be nice for this to be async, so that we can support proper ESM, but that isnt compatible with calling this in the constructor. @@ -102,7 +105,7 @@ export class DeviceInstanceWrapper extends EventEmitter { private _stateTracker?: StateTracker private _deviceId: string - private _deviceType: DeviceType + private _deviceType: DeviceTypeExt private _deviceName: string private _instanceId: number private _startTime: number @@ -112,6 +115,8 @@ export class DeviceInstanceWrapper extends EventEmitter { private _logDebug = false private _logDebugStates = false + private _stateEventHandler: StateEventHandler + private _lastUpdateCurrentTime: number | undefined private _tDiff: number | undefined @@ -147,6 +152,8 @@ export class DeviceInstanceWrapper extends EventEmitter { } this._logDebug = config.debug ?? this._logDebug + this._stateEventHandler = new StateEventHandler(id, config.type, (events) => this.emit('stateEvent', events)) + this._updateTimeSync() if (!config.disableSharedHardwareControl && this._device.diffAddressStates && this._device.applyAddressState) { @@ -165,7 +172,13 @@ export class DeviceInstanceWrapper extends EventEmitter { // make sure the commands for the next state change are correct: let doRecalc = false - this._stateTracker.on('deviceUpdated', (_addr, ahead) => { + this._stateTracker.on('deviceUpdated', (addr, ahead) => { + try { + this._device.onAddressChanged?.(addr, ahead) + } catch (e) { + this.emit('error', 'onAddressChanged hook threw', e as Error) + } + if (doRecalc) return doRecalc = true @@ -298,6 +311,10 @@ export class DeviceInstanceWrapper extends EventEmitter { setDebugState(value: boolean) { this._logDebugStates = value } + setEventSubscriptions(events: string[]): void { + this.emit('debug', `Setting event subscriptions: [${events.join(', ')}]`) + this._stateEventHandler.setEventSubscriptions(events) + } getCurrentTime(): number { if ( @@ -311,7 +328,7 @@ export class DeviceInstanceWrapper extends EventEmitter { return Date.now() + (this._tDiff ?? 0) } - private _getDeviceContextAPI(): DeviceContextAPI { + private _getDeviceContextAPI(): DeviceContextAPI { return { deviceName: this._deviceName, @@ -387,6 +404,10 @@ export class DeviceInstanceWrapper extends EventEmitter { setAddressState: (address, state) => { this._stateTracker?.updateState(address, state) }, + + reportStateEvent: (eventName, payload, isFromTimeline) => { + this._stateEventHandler.report(eventName, payload, isFromTimeline) + }, } } diff --git a/packages/timeline-state-resolver/src/service/__tests__/stateEventHandler.spec.ts b/packages/timeline-state-resolver/src/service/__tests__/stateEventHandler.spec.ts new file mode 100644 index 000000000..4d4c8f352 --- /dev/null +++ b/packages/timeline-state-resolver/src/service/__tests__/stateEventHandler.spec.ts @@ -0,0 +1,161 @@ +import { DeviceType } from 'timeline-state-resolver-types' +import { StateEventHandler } from '../stateEventHandler.js' +import type { SomeTSRStateEvent } from '../../index.js' + +const DEVICE_ID = 'device0' +const DEVICE_TYPE = DeviceType.ABSTRACT + +function makeHandler(onFlush = jest.fn()): { handler: StateEventHandler; onFlush: jest.Mock } { + return { handler: new StateEventHandler(DEVICE_ID, DEVICE_TYPE, onFlush), onFlush } +} + +describe('StateEventHandler', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + describe('filtering', () => { + test('does not emit when allowlist is empty (default)', () => { + const { handler, onFlush } = makeHandler() + + handler.report('some.event', { value: 1 }, false) + jest.runAllTimers() + + expect(onFlush).not.toHaveBeenCalled() + }) + + test('emits only subscribed event names', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a.event']) + + handler.report('a.event', { x: 1 }, false) + handler.report('b.event', { x: 2 }, false) + jest.runAllTimers() + + expect(onFlush).toHaveBeenCalledTimes(1) + const [events] = onFlush.mock.calls[0] as [SomeTSRStateEvent[]] + expect(events).toHaveLength(1) + expect(events[0].event).toBe('a.event') + }) + + test('emits nothing after subscriptions cleared to empty', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a.event']) + handler.setEventSubscriptions([]) + + handler.report('a.event', { x: 1 }, false) + jest.runAllTimers() + + expect(onFlush).not.toHaveBeenCalled() + }) + + test('respects updated subscriptions for subsequent reports', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a.event']) + + handler.report('a.event', { x: 1 }, false) + jest.runAllTimers() + onFlush.mockClear() + + handler.setEventSubscriptions(['b.event']) + handler.report('a.event', { x: 2 }, false) // no longer subscribed + handler.report('b.event', { x: 3 }, false) // now subscribed + jest.runAllTimers() + + expect(onFlush).toHaveBeenCalledTimes(1) + const [events] = onFlush.mock.calls[0] as [SomeTSRStateEvent[]] + expect(events).toHaveLength(1) + expect(events[0].event).toBe('b.event') + }) + }) + + describe('batching', () => { + test('multiple reports in the same tick are batched into one flush', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a.event', 'b.event']) + + handler.report('a.event', { x: 1 }, false) + handler.report('b.event', { x: 2 }, false) + handler.report('a.event', { x: 3 }, false) + + expect(onFlush).not.toHaveBeenCalled() // not yet + + jest.runAllTimers() + + expect(onFlush).toHaveBeenCalledTimes(1) + const [events] = onFlush.mock.calls[0] as [SomeTSRStateEvent[]] + expect(events).toHaveLength(3) + }) + + test('reports across separate ticks produce separate flushes', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a.event']) + + handler.report('a.event', { tick: 1 }, false) + jest.runAllTimers() + + handler.report('a.event', { tick: 2 }, false) + jest.runAllTimers() + + expect(onFlush).toHaveBeenCalledTimes(2) + }) + + test('calls onFlush for events queued before subscriptions cleared', () => { + // The pending snapshot is taken at flush time, not at report time. + // An event queued while 'a.event' was subscribed must still be flushed + // even if subscriptions are cleared before the flush fires. + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a.event']) + + // Queue an event that passes the current subscription filter + handler.report('a.event', { x: 1 }, false) + // Clearing subscriptions after report does not remove already-queued events + handler.setEventSubscriptions([]) // won't affect already-queued events + + jest.runAllTimers() + + // The event was queued before subscriptions were cleared, + // so it IS flushed — the pending snapshot is taken at flush time + expect(onFlush).toHaveBeenCalledTimes(1) + }) + }) + + describe('event shape', () => { + test('emitted events carry correct deviceId, deviceType, event name and payload', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['me.1.inputs']) + + const payload = { programInput: 5, previewInput: 2 } + handler.report('me.1.inputs', payload, true) + jest.runAllTimers() + + const [events] = onFlush.mock.calls[0] as [SomeTSRStateEvent[]] + expect(events[0]).toMatchObject({ + deviceId: DEVICE_ID, + deviceType: DEVICE_TYPE, + event: 'me.1.inputs', + payload, + isFromTimeline: true, + }) + }) + + test('multiple events in a batch each carry the correct device identity', () => { + const { handler, onFlush } = makeHandler() + handler.setEventSubscriptions(['a', 'b']) + + handler.report('a', 1, false) + handler.report('b', 2, false) + jest.runAllTimers() + + const [events] = onFlush.mock.calls[0] as [SomeTSRStateEvent[]] + for (const ev of events) { + expect(ev.deviceId).toBe(DEVICE_ID) + expect(ev.deviceType).toBe(DEVICE_TYPE) + } + }) + }) +}) diff --git a/packages/timeline-state-resolver/src/service/remoteDeviceInstance.ts b/packages/timeline-state-resolver/src/service/remoteDeviceInstance.ts index c1dec0b9d..04ecb704c 100644 --- a/packages/timeline-state-resolver/src/service/remoteDeviceInstance.ts +++ b/packages/timeline-state-resolver/src/service/remoteDeviceInstance.ts @@ -61,6 +61,10 @@ export abstract class BaseRemoteDeviceIntegration< await this._device.setDebugState(debug) } + public async setEventSubscriptions(events: string[]): Promise { + await this._device.setEventSubscriptions(events) + } + public get device(): ThreadedClass | ThreadedClass> { return this._device } diff --git a/packages/timeline-state-resolver/src/service/stateEventHandler.ts b/packages/timeline-state-resolver/src/service/stateEventHandler.ts new file mode 100644 index 000000000..624fe3da9 --- /dev/null +++ b/packages/timeline-state-resolver/src/service/stateEventHandler.ts @@ -0,0 +1,49 @@ +import type { DeviceTypeExt, SomeTSRStateEvent } from '../index.js' + +/** + * Manages filtering and batched emission of state events for a single device. + * + * Events are only emitted when their name appears in the active subscription set. + * Pending events are accumulated and flushed together in a single `setImmediate` tick. + */ +export class StateEventHandler { + readonly #deviceId: string + readonly #deviceType: DeviceTypeExt + readonly #onFlush: (events: SomeTSRStateEvent[]) => void + + #allowedEvents: Set = new Set() + #pendingEvents: SomeTSRStateEvent[] = [] + #flushScheduled = false + + constructor(deviceId: string, deviceType: DeviceTypeExt, onFlush: (events: SomeTSRStateEvent[]) => void) { + this.#deviceId = deviceId + this.#deviceType = deviceType + this.#onFlush = onFlush + } + + setEventSubscriptions(events: string[]): void { + this.#allowedEvents = new Set(events) + } + + report(eventName: string, payload: unknown, isFromTimeline: boolean): void { + if (!this.#allowedEvents.has(eventName)) return + + this.#pendingEvents.push({ + deviceId: this.#deviceId, + deviceType: this.#deviceType, + event: eventName, + payload, + isFromTimeline, + } as SomeTSRStateEvent) + + // Defer flushing to the next tick, to batch multiple events from the same handler call together + if (!this.#flushScheduled) { + this.#flushScheduled = true + setImmediate(() => { + this.#flushScheduled = false + const events = this.#pendingEvents.splice(0) + if (events.length > 0) this.#onFlush(events) + }) + } + } +}