From bb4c22b33062a6bf26a23a9f6b0e241d3206b517 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 2 Oct 2024 13:36:24 +0100 Subject: [PATCH 1/5] fix: handle durations of onRundownChange infinites correctly when spanning into another part --- .../job-worker/src/playout/timeline/rundown.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index f637c666ef..0e7a107986 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -328,8 +328,16 @@ function generateCurrentInfinitePieceObjects( infiniteGroup.enable.duration = infiniteInNextPart.piece.enable.duration } - // If this piece does not continue in the next part, then set it to end with the part it belongs to - if ( + const pieceInstanceWithUpdatedEndCap: PieceInstanceWithTimings = { ...pieceInstance } + // Give the infinite group and end cap when the end of the piece is known + if (pieceInstance.resolvedEndCap) { + // If the cap is a number, it is relative to the part, not the parent group so needs to be handled here + if (typeof pieceInstance.resolvedEndCap === 'number') { + infiniteGroup.enable.end = `#${timingContext.currentPartGroup.id}.start + ${pieceInstance.resolvedEndCap}` + delete pieceInstanceWithUpdatedEndCap.resolvedEndCap + } + } else if ( + // If this piece does not continue in the next part, then set it to end with the part it belongs to !infiniteInNextPart && currentPartInfo.partInstance.part.autoNext && infiniteGroup.enable.duration === undefined && @@ -355,7 +363,7 @@ function generateCurrentInfinitePieceObjects( activePlaylist._id, infiniteGroup, nowInParent, - pieceInstance, + pieceInstanceWithUpdatedEndCap, pieceEnable, 0, groupClasses, From ee3e5693a21504af3daab68313d2e6f70b4370bc Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Tue, 8 Oct 2024 08:03:34 +0200 Subject: [PATCH 2/5] Rework playout connection management (sofie 1152) (#1271) * chore: wip * chore: wip * chore: update tsr --------- Co-authored-by: Johan Nyman --- meteor/yarn.lock | 10 +- packages/playout-gateway/package.json | 2 +- packages/playout-gateway/src/coreHandler.ts | 22 +- packages/playout-gateway/src/tsrHandler.ts | 713 ++++++-------------- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 22 +- 6 files changed, 224 insertions(+), 547 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 007a73b429..102c802baa 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1242,7 +1242,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@portal:../packages/shared-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/model": v4.2.0-alpha.1 - timeline-state-resolver-types: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 + timeline-state-resolver-types: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: node @@ -10197,12 +10197,12 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240909-111856-517f0ee37.0": - version: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 - resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240909-111856-517f0ee37.0" +"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0": + version: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0" dependencies: tslib: ^2.6.3 - checksum: 6f9526e3e60021b722fd152272a7697a2b78517fb800a9e0879170388dcfeaaa2d386f80b9868b20bffe058033c9158077f93294cff7907c5d8a6d7b27e186f6 + checksum: c041363201bcfc0daac2ebca021b09fddc1f5b12fdeb932d9c19bfadc3ee308aa81f36c74c005edad2e756ed1c6465de779bfca5ed63ffd940878bf015497231 languageName: node linkType: hard diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index d884d638d1..cb6bfdc773 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -60,7 +60,7 @@ "@sofie-automation/shared-lib": "1.52.0-in-development", "debug": "^4.3.4", "influx": "^5.9.3", - "timeline-state-resolver": "9.2.0-nightly-release52-20240909-111856-517f0ee37.0", + "timeline-state-resolver": "9.2.0-nightly-release52-20240923-122840-58cfbb259.0", "tslib": "^2.6.2", "underscore": "^1.13.6", "winston": "^3.11.0" diff --git a/packages/playout-gateway/src/coreHandler.ts b/packages/playout-gateway/src/coreHandler.ts index dd31642400..1f73222973 100644 --- a/packages/playout-gateway/src/coreHandler.ts +++ b/packages/playout-gateway/src/coreHandler.ts @@ -346,7 +346,7 @@ export class CoreHandler { const devices: any[] = [] if (this._tsrHandler) { - for (const device of this._tsrHandler.tsr.getDevices()) { + for (const device of this._tsrHandler.tsr.connectionManager.getConnections()) { devices.push({ instanceId: device.instanceId, deviceId: device.deviceId, @@ -416,7 +416,6 @@ export class CoreTSRDeviceHandler { public _deviceId: string public _device!: BaseRemoteDeviceIntegration private _coreParentHandler: CoreHandler - private _tsrHandler: TSRHandler private _hasGottenStatusChange = false private _deviceStatus: PeripheralDeviceAPI.PeripheralDeviceStatusObject = { statusCode: StatusCode.BAD, @@ -424,16 +423,10 @@ export class CoreTSRDeviceHandler { } private disposed = false - constructor( - parent: CoreHandler, - device: Promise>, - deviceId: string, - tsrHandler: TSRHandler - ) { + constructor(parent: CoreHandler, device: Promise>, deviceId: string) { this._coreParentHandler = parent this._devicePr = device this._deviceId = deviceId - this._tsrHandler = tsrHandler } async init(): Promise { this._device = await this._devicePr @@ -455,10 +448,11 @@ export class CoreTSRDeviceHandler { ) }) + console.log('has got status? ' + this._hasGottenStatusChange) if (!this._hasGottenStatusChange) { this._deviceStatus = await this._device.device.getStatus() - this.sendStatus() } + this.sendStatus() if (this.disposed) throw new Error('CoreTSRDeviceHandler cant init, is disposed') await this.setupSubscriptionsAndObservers() if (this.disposed) throw new Error('CoreTSRDeviceHandler cant init, is disposed') @@ -490,8 +484,9 @@ export class CoreTSRDeviceHandler { // setup observers this._coreParentHandler.setupObserverForPeripheralDeviceCommands(this) } - statusChanged(deviceStatus: Partial): void { - this._hasGottenStatusChange = true + statusChanged(deviceStatus: Partial, fromDevice = true): void { + console.log('device ' + this._deviceId + ' status set to ' + deviceStatus.statusCode) + if (fromDevice) this._hasGottenStatusChange = true this._deviceStatus = { ...this._deviceStatus, @@ -545,7 +540,8 @@ export class CoreTSRDeviceHandler { async dispose(subdevice: 'keepSubDevice' | 'removeSubDevice' = 'keepSubDevice'): Promise { this._observers.forEach((obs) => obs.stop()) - await this._tsrHandler.tsr.removeDevice(this._deviceId) + if (!this.core) return + await this.core.setStatus({ statusCode: StatusCode.BAD, messages: ['Uninitialized'], diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index d3245bd383..40def38103 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -7,15 +7,10 @@ import { TSRTimelineObj, TSRTimeline, TSRTimelineContent, - CommandReport, DeviceOptionsAtem, AtemMediaPoolAsset, - MediaObject, ExpectedPlayoutItem, ExpectedPlayoutItemContent, - SlowSentCommandInfo, - SlowFulfilledCommandInfo, - DeviceStatus, StatusCode, Datastore, } from 'timeline-state-resolver' @@ -55,8 +50,6 @@ import { unprotectString, } from '@sofie-automation/server-core-integration' import { BaseRemoteDeviceIntegration } from 'timeline-state-resolver/dist/service/remoteDeviceInstance' -import { DeviceEvents } from 'timeline-state-resolver/dist/service/device' -import EventEmitter = require('eventemitter3') const debug = Debug('playout-gateway') @@ -69,31 +62,6 @@ export interface TimelineContentObjectTmp { inGroup?: string } -/** Max time for initializing devices */ -const INIT_TIMEOUT = 10000 - -enum DeviceAction { - ADD = 'add', - READD = 'readd', - REMOVE = 'remove', -} - -type DeviceActionResult = { - success: boolean - deviceId: string - action: DeviceAction -} - -type UpdateDeviceOperationsResult = - | { - success: true - results: DeviceActionResult[] - } - | { - success: false - reason: 'timeout' | 'error' - details: string[] - } /** * Represents a connection between Gateway and TSR @@ -223,6 +191,8 @@ export class TSRHandler { }) this.tsr.on('timeTrace', (trace: FinishedTrace) => sendTrace(trace)) + this.attachTSRConnectionEvents() + this.logger.debug('tsr init') await this.tsr.init() @@ -234,6 +204,195 @@ export class TSRHandler { this.logger.debug('tsr init done') } + private attachTSRConnectionEvents() { + this.tsr.connectionManager.on('info', (info) => this.logger.info('TSR ConnectionManager: ' + info)) + this.tsr.connectionManager.on('warning', (warning) => this.logger.warn('TSR ConnectionManager: ' + warning)) + this.tsr.connectionManager.on('debug', (...args) => { + if (!this._coreHandler.logDebug) { + return + } + const data = args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)) + this.logger.debug(`TSR ConnectionManager debug (${args.length})`, { data }) + }) + + this.tsr.connectionManager.on('connectionAdded', (id, container) => { + const coreTsrHandler = new CoreTSRDeviceHandler(this._coreHandler, Promise.resolve(container), id) + this._coreTsrHandlers[id] = coreTsrHandler + + // set the status to uninitialized for now: + coreTsrHandler.statusChanged( + { + statusCode: StatusCode.BAD, + messages: ['Device initialising...'], + }, + false + ) + + this._triggerupdateExpectedPlayoutItems() // So that any recently created devices will get all the ExpectedPlayoutItems + }) + + this.tsr.connectionManager.on('connectionInitialised', (id) => { + const coreTsrHandler = this._coreTsrHandlers[id] + + if (!coreTsrHandler) { + this.logger.error('TSR Connection initialised when there was not CoreTSRHandler for it') + return + } + + coreTsrHandler.init().catch((e) => this.logger.error('CoreTSRHandler failed to initialise', e)) // todo - is this the right way to log this? + }) + + this.tsr.connectionManager.on('connectionRemoved', (id) => { + const coreTsrHandler = this._coreTsrHandlers[id] + + if (!coreTsrHandler) { + this.logger.error('TSR Connection was removed when but there was not CoreTSRHandler to handle that') + return + } + + coreTsrHandler.dispose('removeSubDevice').catch((e) => { + this.logger.error('Failed to dispose of coreTsrHandler for ' + id + ': ' + e) + }) + delete this._coreTsrHandlers[id] + }) + + const fixLog = (id: string, e: string): string => { + const device = this._coreTsrHandlers[id]?._device + + return `Device "${device?.deviceName ?? id}" (${device?.instanceId ?? 'instance unknown'}): ` + e + } + const fixError = (id: string, e: Error): any => { + const device = this._coreTsrHandlers[id]?._device + const name = `Device "${device?.deviceName ?? id}" (${device?.instanceId ?? 'instance unknown'})` + + return { + message: e.message && name + ': ' + e.message, + name: e.name && name + ': ' + e.name, + stack: e.stack && e.stack + '\nAt device' + name, + } + } + const fixContext = (...context: any[]): any => { + return { + context, + } + } + + this.tsr.connectionManager.on('connectionEvent:connectionChanged', (id, status) => { + const coreTsrHandler = this._coreTsrHandlers[id] + if (!coreTsrHandler) return + + coreTsrHandler.statusChanged(status) + + // When the status has changed, the deviceName might have changed: + coreTsrHandler._device.reloadProps().catch((err) => { + this.logger.error(`Error in reloadProps: ${stringifyError(err)}`) + }) + // hack to make sure atem has media after restart + if ( + (status.statusCode === StatusCode.GOOD || + status.statusCode === StatusCode.WARNING_MINOR || + status.statusCode === StatusCode.WARNING_MAJOR) && + coreTsrHandler._device.deviceType === DeviceType.ATEM && + !disableAtemUpload + ) { + const assets = (coreTsrHandler._device.deviceOptions as DeviceOptionsAtem).options?.mediaPoolAssets + if (assets && assets.length > 0) { + try { + this.uploadFilesToAtem( + coreTsrHandler._device, + assets.filter((asset) => _.isNumber(asset.position) && asset.path) + ) + } catch (e) { + // don't worry about it. + } + } + } + }) + this.tsr.connectionManager.on('connectionEvent:slowSentCommand', (id, info) => { + // If the internalDelay is too large, it should be logged as an error, + // since something took too long internally. + + if (info.internalDelay > 100) { + this.logger.error('slowSentCommand', { + id, + ...info, + }) + } else { + this.logger.warn('slowSentCommand', { + id, + ...info, + }) + } + }) + this.tsr.connectionManager.on('connectionEvent:slowFulfilledCommand', (id, info) => { + // Note: we don't emit slow fulfilled commands as error, since + // the fulfillment of them lies on the device being controlled, not on us. + + this.logger.warn('slowFulfilledCommand', { + id, + ...info, + }) + }) + this.tsr.connectionManager.on('connectionEvent:commandError', (id, error, context) => { + // todo: handle this better + this.logger.error(fixError(id, error), { context }) + }) + this.tsr.connectionManager.on('connectionEvent:commandReport', (_id, commandReport) => { + if (this._reportAllCommands) { + // Todo: send these to Core + this.logger.info('commandReport', { + commandReport: commandReport, + }) + } + }) + this.tsr.connectionManager.on('connectionEvent:updateMediaObject', (id, collectionId, docId, doc) => { + const coreTsrHandler = this._coreTsrHandlers[id] + if (!coreTsrHandler) return + + coreTsrHandler.onUpdateMediaObject(collectionId, docId, doc) + }) + this.tsr.connectionManager.on('connectionEvent:clearMediaObjects', (id, collectionId) => { + const coreTsrHandler = this._coreTsrHandlers[id] + if (!coreTsrHandler) return + + coreTsrHandler.onClearMediaObjectCollection(collectionId) + }) + this.tsr.connectionManager.on('connectionEvent:info', (id, info) => { + this.logger.info(fixLog(id, info)) + }) + this.tsr.connectionManager.on('connectionEvent:warning', (id, warning) => { + this.logger.warn(fixLog(id, warning)) + }) + this.tsr.connectionManager.on('connectionEvent:error', (id, context, error) => { + this.logger.error(fixError(id, error), fixContext(context)) + }) + this.tsr.connectionManager.on('connectionEvent:debug', (id, ...args) => { + const device = this._coreTsrHandlers[id]?._device + + if (!device?.debugLogging && !this._coreHandler.logDebug) { + return + } + if (args.length === 0) { + this.logger.debug('>empty message<') + return + } + const data = args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)) + this.logger.debug(`Device "${device?.deviceName || id}" (${device?.instanceId})`, { data }) + }) + this.tsr.connectionManager.on('connectionEvent:debugState', (id, state) => { + const device = this._coreTsrHandlers[id]?._device + + if (device?.debugState && this._coreHandler.logDebug) { + // Fetch the Id that core knows this device by + const coreId = this._coreTsrHandlers[device.deviceId].core.deviceId + this._debugStates.set(unprotectString(coreId), state) + } + }) + this.tsr.connectionManager.on('connectionEvent:timeTrace', (_id, trace) => { + sendTrace(trace) + }) + } + private loadSubdeviceConfigurations(): { [deviceType: string]: Record } { const defaultDeviceOptions: { [deviceType: string]: Record } = {} @@ -496,40 +655,14 @@ export class TSRHandler { } private async _updateDevices(): Promise { - this.logger.debug('updateDevices start') - const peripheralDevice = this._getPeripheralDevice() - const ps: Promise[] = [] - const promiseOperations: { [id: string]: { deviceId: string; operation: DeviceAction } } = {} - const keepTrack = async (p: Promise, deviceId: string, operation: DeviceAction) => { - const name = `${operation}_${deviceId}` - promiseOperations[name] = { - deviceId, - operation, - } - return p.then((result) => { - delete promiseOperations[name] - return result - }) - } - const deviceOptions = new Map() - if (peripheralDevice) { + const connections: Record = {} const devices = peripheralDevice.playoutDevices for (const [deviceId, device0] of Object.entries(devices)) { - const device = device0 - if (!device.disable) { - deviceOptions.set(deviceId, device) - } - } - - for (const [deviceId, orgDeviceOptions] of deviceOptions.entries()) { - const oldDevice: BaseRemoteDeviceIntegration | undefined = this.tsr.getDevice( - deviceId, - true - ) + if (device0.disable) continue const deviceOptions = _.extend( { @@ -538,9 +671,8 @@ export class TSRHandler { limitSlowFulfilledCommand: 100, options: {}, }, - this.populateDefaultValuesIfMissing(orgDeviceOptions) + this.populateDefaultValuesIfMissing(device0) ) - if (this._multiThreaded !== null && deviceOptions.isMultiThreaded === undefined) { deviceOptions.isMultiThreaded = this._multiThreaded } @@ -548,130 +680,11 @@ export class TSRHandler { deviceOptions.reportAllCommands = this._reportAllCommands } - if (!oldDevice) { - if (deviceOptions.options) { - this.logger.info('Initializing device: ' + deviceId) - this.logger.info('new', deviceOptions) - ps.push(keepTrack(this._addDevice(deviceId, deviceOptions), deviceId, DeviceAction.ADD)) - } - } else { - if (deviceOptions.options) { - let anyChanged = false - - if ( - // Changing the debug flag shouldn't restart the device: - !_.isEqual(_.omit(oldDevice.deviceOptions, 'debug'), _.omit(deviceOptions, 'debug')) - ) { - anyChanged = true - } - - if (anyChanged) { - deviceOptions.debug = this.getDeviceDebug(orgDeviceOptions) - - this.logger.info('Re-initializing device: ' + deviceId) - this.logger.info('old', oldDevice.deviceOptions) - this.logger.info('new', deviceOptions) - ps.push( - keepTrack(this._removeDevice(deviceId), deviceId, DeviceAction.REMOVE).then(async () => - keepTrack(this._addDevice(deviceId, deviceOptions), deviceId, DeviceAction.READD) - ) - ) - } - } - } - } - - for (const oldDevice of this.tsr.getDevices()) { - const deviceId = oldDevice.deviceId - if (!deviceOptions.has(deviceId)) { - this.logger.info('Un-initializing device: ' + deviceId) - ps.push(keepTrack(this._removeDevice(deviceId), deviceId, DeviceAction.REMOVE)) - } - } - } - - const resultsOrTimeout = await Promise.race([ - Promise.all(ps).then((results) => ({ - success: true, - results, - })), - new Promise((resolve) => - setTimeout(() => { - const keys = Object.keys(promiseOperations) - if (keys.length) { - this.logger.warn( - `Timeout in _updateDevices: ${Object.values<{ deviceId: string; operation: DeviceAction }>( - promiseOperations - ) - .map((op) => op.deviceId) - .join(',')}` - ) - } - - Promise.all( - // At this point in time, promiseOperations contains the promises that have timed out. - // If we tried to add or re-add a device, that apparently failed so we should remove the device in order to - // give it another chance next time _updateDevices() is called. - Object.values<{ deviceId: string; operation: DeviceAction }>(promiseOperations) - .filter((op) => op.operation === DeviceAction.ADD || op.operation === DeviceAction.READD) - .map(async (op) => - // the device was never added, should retry next round - this._removeDevice(op.deviceId) - ) - ) - .catch((e) => { - this.logger.error( - `Error when trying to remove unsuccessfully initialized devices: ${stringifyIds( - Object.values<{ deviceId: string; operation: DeviceAction }>(promiseOperations).map( - (op) => op.deviceId - ) - )}`, - e - ) - }) - .finally(() => { - resolve({ - success: false, - reason: 'error', - details: keys, - }) - }) - }, INIT_TIMEOUT) - ), // Timeout if not all are resolved within INIT_TIMEOUT - ]) - - await this._reportResult(resultsOrTimeout) - - const debugLoggingPs: Promise[] = [] - // Set logDebug on the devices: - for (const device of this.tsr.getDevices()) { - const options: DeviceOptionsAny | undefined = deviceOptions.get(device.deviceId) - if (!options) { - continue - } - const debug: boolean = this.getDeviceDebug(options) - if (device.debugLogging !== debug) { - this.logger.info(`Setting logDebug of device ${device.deviceId} to ${debug}`) - debugLoggingPs.push(device.setDebugLogging(debug)) - } - } - // Set debugState on devices: - for (const device of this.tsr.getDevices()) { - const options: DeviceOptionsAny | undefined = deviceOptions.get(device.deviceId) - if (!options) { - continue + connections[deviceId] = deviceOptions } - const debug: boolean = this.getDeviceDebugState(options) - if (device.debugState !== debug) { - this.logger.info(`Setting debugState of device ${device.deviceId} to ${debug}`) - debugLoggingPs.push(device.setDebugState(debug)) - } + this.tsr.connectionManager.setConnections(connections) } - await Promise.all(debugLoggingPs) - - this._triggerupdateExpectedPlayoutItems() // So that any recently created devices will get all the ExpectedPlayoutItems - this.logger.debug('updateDevices end') } private populateDefaultValuesIfMissing(deviceOptions: DeviceOptionsAny): DeviceOptionsAny { @@ -681,303 +694,6 @@ export class TSRHandler { deviceOptions.options = { ...this.defaultDeviceOptions[deviceOptions.type], ...options } return deviceOptions } - - private getDeviceDebug(deviceOptions: DeviceOptionsAny): boolean { - return deviceOptions.debug || this._coreHandler.logDebug || false - } - private getDeviceDebugState(deviceOptions: DeviceOptionsAny): boolean { - return (deviceOptions.debugState && this._coreHandler.logState) || false - } - private async _reportResult(resultsOrTimeout: UpdateDeviceOperationsResult): Promise { - this.logger.warn(JSON.stringify(resultsOrTimeout)) - // Check if the updateDevice operation failed before completing - if (!resultsOrTimeout.success) { - // It failed because there was a global timeout (not a device-specific failure) - if (resultsOrTimeout.reason === 'timeout') { - await this._coreHandler.core.setStatus({ - statusCode: StatusCode.FATAL, - messages: [ - `Time-out during device update. Timed-out on devices: ${stringifyIds( - resultsOrTimeout.details - )}`, - ], - }) - // It failed for an unknown reason - } else { - await this._coreHandler.core.setStatus({ - statusCode: StatusCode.BAD, - messages: [ - `Unknown error during device update: ${resultsOrTimeout.reason}. Devices: ${stringifyIds( - resultsOrTimeout.details - )}`, - ], - }) - } - - return - } - - // updateDevice finished successfully, let's see if any of the individual devices failed - const failures = resultsOrTimeout.results.filter((result) => !result.success) - // Group the failures according to what sort of an operation was executed - const addFailureDeviceIds = failures - .filter((failure) => failure.action === DeviceAction.ADD) - .map((failure) => failure.deviceId) - const removeFailureDeviceIds = failures - .filter((failure) => failure.action === DeviceAction.REMOVE) - .map((failure) => failure.deviceId) - - // There were no failures, good - if (failures.length === 0) { - await this._coreHandler.core.setStatus({ - statusCode: StatusCode.GOOD, - messages: [], - }) - return - } - // Something did fail, let's report it as the status - await this._coreHandler.core.setStatus({ - statusCode: StatusCode.BAD, - messages: [ - addFailureDeviceIds.length > 0 - ? `Unable to initialize devices, check configuration: ${stringifyIds(addFailureDeviceIds)}` - : null, - removeFailureDeviceIds.length > 0 - ? `Failed to remove devices: ${stringifyIds(removeFailureDeviceIds)}` - : null, - ].filter(Boolean) as string[], - }) - } - - private async _addDevice(deviceId: string, options: DeviceOptionsAny): Promise { - this.logger.debug('Adding device ' + deviceId) - - try { - if (this._coreTsrHandlers[deviceId]) { - throw new Error(`There is already a _coreTsrHandlers for deviceId "${deviceId}"!`) - } - - const devicePr: Promise> = this.tsr.createDevice( - deviceId, - options - ) - - const coreTsrHandler = new CoreTSRDeviceHandler(this._coreHandler, devicePr, deviceId, this) - - this._coreTsrHandlers[deviceId] = coreTsrHandler - - // set the status to uninitialized for now: - coreTsrHandler.statusChanged({ - statusCode: StatusCode.BAD, - messages: ['Device initialising...'], - }) - - const device = await devicePr - - // Set up device status - const deviceType = device.deviceType - - const onDeviceStatusChanged = (connectedOrStatus: Partial) => { - let deviceStatus: Partial - if (_.isBoolean(connectedOrStatus)) { - // for backwards compability, to be removed later - if (connectedOrStatus) { - deviceStatus = { - statusCode: StatusCode.GOOD, - } - } else { - deviceStatus = { - statusCode: StatusCode.BAD, - messages: ['Disconnected'], - } - } - } else { - deviceStatus = connectedOrStatus - } - coreTsrHandler.statusChanged(deviceStatus) - - // When the status has changed, the deviceName might have changed: - device.reloadProps().catch((err) => { - this.logger.error(`Error in reloadProps: ${stringifyError(err)}`) - }) - // hack to make sure atem has media after restart - if ( - (deviceStatus.statusCode === StatusCode.GOOD || - deviceStatus.statusCode === StatusCode.WARNING_MINOR || - deviceStatus.statusCode === StatusCode.WARNING_MAJOR) && - deviceType === DeviceType.ATEM && - !disableAtemUpload - ) { - const assets = (options as DeviceOptionsAtem).options?.mediaPoolAssets - if (assets && assets.length > 0) { - try { - this.uploadFilesToAtem( - device, - assets.filter((asset) => _.isNumber(asset.position) && asset.path) - ) - } catch (e) { - // don't worry about it. - } - } - } - } - const onSlowSentCommand = (info: SlowSentCommandInfo) => { - // If the internalDelay is too large, it should be logged as an error, - // since something took too long internally. - - if (info.internalDelay > 100) { - this.logger.error('slowSentCommand', { - deviceName: device.deviceName, - ...info, - }) - } else { - this.logger.warn('slowSentCommand', { - deviceName: device.deviceName, - ...info, - }) - } - } - const onSlowFulfilledCommand = (info: SlowFulfilledCommandInfo) => { - // Note: we don't emit slow fulfilled commands as error, since - // the fulfillment of them lies on the device being controlled, not on us. - - this.logger.warn('slowFulfilledCommand', { - deviceName: device.deviceName, - ...info, - }) - } - const onCommandReport = (commandReport: CommandReport) => { - if (this._reportAllCommands) { - // Todo: send these to Core - this.logger.info('commandReport', { - commandReport: commandReport, - }) - } - } - const onCommandError = (error: any, context: any) => { - // todo: handle this better - this.logger.error(fixError(error), { context }) - } - const onUpdateMediaObject = (collectionId: string, docId: string, doc: MediaObject | null) => { - coreTsrHandler.onUpdateMediaObject(collectionId, docId, doc) - } - const onClearMediaObjectCollection = (collectionId: string) => { - coreTsrHandler.onClearMediaObjectCollection(collectionId) - } - const fixLog = (e: string): string => `Device "${device.deviceName || deviceId}" (${device.instanceId})` + e - const fixError = (e: Error): any => { - const name = `Device "${device.deviceName || deviceId}" (${device.instanceId})` - - return { - message: e.message && name + ': ' + e.message, - name: e.name && name + ': ' + e.name, - stack: e.stack && e.stack + '\nAt device' + name, - } - } - const fixContext = (...context: any[]): any => { - return { - context, - } - } - await coreTsrHandler.init() - - device.onChildClose = () => { - // Called if a child is closed / crashed - this.logger.warn(`Child of device ${deviceId} closed/crashed`) - debug(`Trigger update devices because "${deviceId}" process closed`) - - onDeviceStatusChanged({ - statusCode: StatusCode.BAD, - messages: ['Child process closed'], - }) - - this._removeDevice(deviceId).then( - () => { - this._triggerUpdateDevices() - }, - () => { - this._triggerUpdateDevices() - } - ) - } - - await addListenerToDevice(device, 'connectionChanged', onDeviceStatusChanged) - // await addListenerToDevice(device, 'slowCommand', onSlowCommand) - await addListenerToDevice(device, 'slowSentCommand', onSlowSentCommand) - await addListenerToDevice(device, 'slowFulfilledCommand', onSlowFulfilledCommand) - await addListenerToDevice(device, 'commandError', onCommandError) - await addListenerToDevice(device, 'commandReport', onCommandReport) - await addListenerToDevice(device, 'updateMediaObject', onUpdateMediaObject) - await addListenerToDevice(device, 'clearMediaObjects', onClearMediaObjectCollection) - - await addListenerToDevice(device, 'info', (info) => { - this.logger.info(fixLog(info)) - }) - await addListenerToDevice(device, 'warning', (warning: string) => { - this.logger.warn(fixLog(warning)) - }) - await addListenerToDevice(device, 'error', (context, error) => { - this.logger.error(fixError(error), fixContext(context)) - }) - - await addListenerToDevice(device, 'debug', (...args) => { - if (!device.debugLogging && !this._coreHandler.logDebug) { - return - } - if (args.length === 0) { - this.logger.debug('>empty message<') - return - } - const data = args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg)) - this.logger.debug(`Device "${device.deviceName || deviceId}" (${device.instanceId})`, { data }) - }) - - await addListenerToDevice(device, 'debugState', (...args) => { - if (device.debugState && this._coreHandler.logDebug) { - // Fetch the Id that core knows this device by - const coreId = this._coreTsrHandlers[device.deviceId].core.deviceId - this._debugStates.set(unprotectString(coreId), args[0]) - } - }) - - await addListenerToDevice(device, 'timeTrace', (trace) => sendTrace(trace)) - /* eslint-enable @typescript-eslint/await-thenable */ - - // now initialize it - await this.tsr.initDevice(deviceId, options) - - // also ask for the status now, and update: - onDeviceStatusChanged(await device.device.getStatus()) - return { - action: DeviceAction.ADD, - deviceId, - success: true, - } - } catch (error) { - // Initialization failed, clean up any artifacts and see if we can try again later: - this.logger.error(`Error when adding device "${deviceId}"`, { error }) - debug(`Error when adding device "${deviceId}"`) - try { - await this._removeDevice(deviceId) - } catch (error) { - this.logger.error(`Error when cleaning up after adding device "${deviceId}" error...`, error) - } - - if (!this._triggerUpdateDevicesTimeout) { - this._triggerUpdateDevicesTimeout = setTimeout(() => { - debug(`Trigger updateDevices from failure "${deviceId}"`) - // try again later: - this._triggerUpdateDevices() - }, 10 * 1000) - } - - return { - action: DeviceAction.ADD, - deviceId, - success: false, - } - } - } /** * This function is a quick and dirty solution to load a still to the atem mixers. * This does not serve as a proper implementation! And need to be refactor @@ -1000,25 +716,6 @@ export class TSRHandler { process.stderr.on('data', (data) => this.logger.info(data.toString())) process.on('close', () => process.removeAllListeners()) } - private async _removeDevice(deviceId: string): Promise { - let success = false - if (this._coreTsrHandlers[deviceId]) { - try { - await this._coreTsrHandlers[deviceId].dispose('removeSubDevice') - this.logger.debug('Disposed device ' + deviceId) - success = true - } catch (error) { - this.logger.error(`Error when removing device "${deviceId}"`, error) - } - } - delete this._coreTsrHandlers[deviceId] - - return { - deviceId, - action: DeviceAction.REMOVE, - success, - } - } private _triggerupdateExpectedPlayoutItems() { if (!this._initialized) return if (this._triggerupdateExpectedPlayoutItemsTimeout) { @@ -1049,7 +746,7 @@ export class TSRHandler { } await Promise.all( - _.map(this.tsr.getDevices(), async (container) => { + _.map(this.tsr.connectionManager.getConnections(), async (container) => { if (!container.details.supportsExpectedPlayoutItems) { return } @@ -1241,19 +938,3 @@ export function getHash(str: string): string { export function stringifyIds(ids: string[]): string { return ids.map((id) => `"${id}"`).join(', ') } - -async function addListenerToDevice( - device: BaseRemoteDeviceIntegration, - eventName: T, - fcn: EventEmitter.EventListener -): Promise { - // Note for the future: - // It is important that the callbacks returns void, - // otherwise there might be problems with threadedclass! - // Also, it is critical that all of these `.on` calls be `await`ed. - // They aren't typed as promises due to limitations of TypeScript, - // but due to threadedclass they _are_ promises. - - const emitterHack = device.device as unknown as EventEmitter - await Promise.resolve(emitterHack.on(eventName, fcn)) -} diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index f0640fd264..40c506ca0c 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -39,7 +39,7 @@ ], "dependencies": { "@mos-connection/model": "v4.2.0-alpha.1", - "timeline-state-resolver-types": "9.2.0-nightly-release52-20240909-111856-517f0ee37.0", + "timeline-state-resolver-types": "9.2.0-nightly-release52-20240923-122840-58cfbb259.0", "tslib": "^2.6.2", "type-fest": "^3.13.1" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 1e8700246e..1456ec7553 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5283,7 +5283,7 @@ __metadata: resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": v4.2.0-alpha.1 - timeline-state-resolver-types: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 + timeline-state-resolver-types: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 tslib: ^2.6.2 type-fest: ^3.13.1 languageName: unknown @@ -21721,7 +21721,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": 1.52.0-in-development debug: ^4.3.4 influx: ^5.9.3 - timeline-state-resolver: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 + timeline-state-resolver: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 tslib: ^2.6.2 underscore: ^1.13.6 winston: ^3.11.0 @@ -26035,18 +26035,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240909-111856-517f0ee37.0": - version: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 - resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240909-111856-517f0ee37.0" +"timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0": + version: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + resolution: "timeline-state-resolver-types@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0" dependencies: tslib: ^2.6.3 - checksum: 6f9526e3e60021b722fd152272a7697a2b78517fb800a9e0879170388dcfeaaa2d386f80b9868b20bffe058033c9158077f93294cff7907c5d8a6d7b27e186f6 + checksum: c041363201bcfc0daac2ebca021b09fddc1f5b12fdeb932d9c19bfadc3ee308aa81f36c74c005edad2e756ed1c6465de779bfca5ed63ffd940878bf015497231 languageName: node linkType: hard -"timeline-state-resolver@npm:9.2.0-nightly-release52-20240909-111856-517f0ee37.0": - version: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 - resolution: "timeline-state-resolver@npm:9.2.0-nightly-release52-20240909-111856-517f0ee37.0" +"timeline-state-resolver@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0": + version: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 + resolution: "timeline-state-resolver@npm:9.2.0-nightly-release52-20240923-122840-58cfbb259.0" dependencies: "@tv2media/v-connection": ^7.3.4 atem-connection: 3.5.0 @@ -26071,7 +26071,7 @@ asn1@evs-broadcast/node-asn1: sprintf-js: ^1.1.3 superfly-timeline: ^9.0.1 threadedclass: ^1.2.2 - timeline-state-resolver-types: 9.2.0-nightly-release52-20240909-111856-517f0ee37.0 + timeline-state-resolver-types: 9.2.0-nightly-release52-20240923-122840-58cfbb259.0 tslib: ^2.6.3 tv-automation-quantel-gateway-client: ^3.1.7 type-fest: ^3.13.1 @@ -26079,7 +26079,7 @@ asn1@evs-broadcast/node-asn1: utf-8-validate: ^6.0.4 ws: ^8.18.0 xml-js: ^1.6.11 - checksum: 65e4b7ad24f414efd940fda3987900123aea8bd5ec41cadeca7763c13e98e24ceff4384f0103f8079d2e823986de56f3dd28bc2afeb3e6632d74c0e29f88ad6b + checksum: a127cd66d96f06bae3ff16291bc1be4cd1c6589c8843632c489e2432df2b34789adc62db0826d7069bb6ff1b4a8c56e0f37ffe5a17d9a92b8f3533963e0bdb71 languageName: node linkType: hard From 752a30a69ea4a864239a438b278c3b53d06a12a2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 8 Oct 2024 12:50:24 +0100 Subject: [PATCH 3/5] fix: review comments --- meteor/server/api/userActions.ts | 2 +- meteor/server/migration/1_42_0.ts | 51 ------------------- meteor/server/migration/X_X_X.ts | 41 ++++++++------- meteor/server/migration/migrations.ts | 3 +- .../src/context/adlibActionContext.ts | 2 +- .../blueprints-integration/src/triggers.ts | 2 +- .../src/blueprints/context/adlibActions.ts | 2 +- .../src/playout/model/PlayoutModel.ts | 2 +- .../model/implementation/PlayoutModelImpl.ts | 2 +- packages/job-worker/src/playout/upgrade.ts | 26 ++++++---- .../src/studio/model/StudioBaselineHelper.ts | 1 - .../meteor-lib/src/triggers/actionFactory.ts | 2 +- .../ui/Settings/ShowStyleBaseSettings.tsx | 1 - .../client/ui/Settings/SystemManagement.tsx | 7 +-- .../TriggeredActionsEditor.tsx | 15 ++---- .../actionSelector/ActionSelector.tsx | 1 - 16 files changed, 53 insertions(+), 107 deletions(-) delete mode 100644 meteor/server/migration/1_42_0.ts diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 8ccc6dda65..507ec96d35 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -1125,7 +1125,7 @@ class ServerUserActionAPI async () => { check(studioId, String) check(routeSetId, String) - check(state, Boolean) + check(state, Match.OneOf('toggle', Boolean)) const access = await StudioContentWriteAccess.routeSet(this, studioId) return ServerPlayoutAPI.switchRouteSet(access, routeSetId, state) diff --git a/meteor/server/migration/1_42_0.ts b/meteor/server/migration/1_42_0.ts deleted file mode 100644 index cdd49aab99..0000000000 --- a/meteor/server/migration/1_42_0.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { addMigrationSteps } from './databaseMigration' -import { StudioRouteSet, StudioRouteType } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { Studios } from '../collections' - -// Release 42 - -export const addSteps = addMigrationSteps('1.42.0', [ - { - id: 'Add new routeType property to routeSets where missing', - canBeRunAutomatically: true, - validate: async () => { - // If routeSets has been converted to ObjectWithOverrides, - // it will have a defaults property, and shouln't be migrated - if ( - (await Studios.countDocuments({ - routeSetsWithOverrides: { $exists: true }, - })) > 0 - ) { - return false - } - return ( - (await Studios.countDocuments({ - routeSets: { $exists: false }, - })) > 0 - ) - }, - migrate: async () => { - const studios = await Studios.findFetchAsync({}) - - for (const studio of studios) { - // If routeSets has been converted to ObjectWithOverrides, - // it will have a defaults property, and shouln't be migrated - if (studio.routeSetsWithOverrides) return - - //@ts-expect-error routeSets is not typed as ObjectWithOverrides - const routeSets = studio.routeSets as any as Record - Object.entries(routeSets).forEach(([routeSetId, routeSet]) => { - routeSet.routes.forEach((route) => { - if (!route.routeType) { - route.routeType = StudioRouteType.REROUTE - } - }) - - routeSets[routeSetId] = routeSet - }) - - await Studios.updateAsync(studio._id, { $set: { routeSets } }) - } - }, - }, -]) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 06d2547dbb..2d3cc1eff7 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -19,7 +19,10 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ id: `convert routesets to ObjectWithOverrides`, canBeRunAutomatically: true, validate: async () => { - const studios = await Studios.findFetchAsync({ routeSets: { $exists: true } }) + const studios = await Studios.findFetchAsync({ + routeSets: { $exists: true }, + routeSetsWithOverrides: { $exists: false }, + }) for (const studio of studios) { //@ts-expect-error routeSets is not typed as ObjectWithOverrides @@ -31,15 +34,16 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ return false }, migrate: async () => { - const studios = await Studios.findFetchAsync({ routeSets: { $exists: true } }) + const studios = await Studios.findFetchAsync({ + routeSets: { $exists: true }, + routeSetsWithOverrides: { $exists: false }, + }) for (const studio of studios) { - //@ts-expect-error routeSets is not typed as ObjectWithOverrides - if (!studio.routeSets) continue - //@ts-expect-error routeSets is not typed as ObjectWithOverrides - const oldRouteSets = studio.routeSets as any as Record + //@ts-expect-error routeSets is typed as Record + const oldRouteSets = studio.routeSets - const newRouteSets = convertObjectIntoOverrides(oldRouteSets) + const newRouteSets = convertObjectIntoOverrides(oldRouteSets || {}) await Studios.updateAsync(studio._id, { $set: { @@ -56,7 +60,10 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ id: `convert routeSetExclusivityGroups to ObjectWithOverrides`, canBeRunAutomatically: true, validate: async () => { - const studios = await Studios.findFetchAsync({ routeSetExclusivityGroups: { $exists: true } }) + const studios = await Studios.findFetchAsync({ + routeSetExclusivityGroups: { $exists: true }, + routeSetExclusivityGroupsWithOverrides: { $exists: false }, + }) for (const studio of studios) { //@ts-expect-error routeSetExclusivityGroups is not typed as ObjectWithOverrides @@ -68,18 +75,18 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ return false }, migrate: async () => { - const studios = await Studios.findFetchAsync({ routeSetExclusivityGroups: { $exists: true } }) + const studios = await Studios.findFetchAsync({ + routeSetExclusivityGroups: { $exists: true }, + routeSetExclusivityGroupsWithOverrides: { $exists: false }, + }) for (const studio of studios) { - //@ts-expect-error routeSetExclusivityGroups is not typed as ObjectWithOverrides - if (!studio.routeSetExclusivityGroups) return - //@ts-expect-error routeSetExclusivityGroups is not typed as ObjectWithOverrides - const oldRouteSetExclusivityGroups = studio.routeSetExclusivityGroups as any as Record< - string, - StudioRouteSetExclusivityGroup - > + //@ts-expect-error routeSets is typed as Record + const oldRouteSetExclusivityGroups = studio.routeSetExclusivityGroups - const newRouteSetExclusivityGroups = convertObjectIntoOverrides(oldRouteSetExclusivityGroups) + const newRouteSetExclusivityGroups = convertObjectIntoOverrides( + oldRouteSetExclusivityGroups || {} + ) await Studios.updateAsync(studio._id, { $set: { diff --git a/meteor/server/migration/migrations.ts b/meteor/server/migration/migrations.ts index 578fd6b955..7a7d45f2f5 100644 --- a/meteor/server/migration/migrations.ts +++ b/meteor/server/migration/migrations.ts @@ -12,8 +12,7 @@ addSteps1_40_0() import { addSteps as addSteps1_41_0 } from './1_41_0' addSteps1_41_0() -import { addSteps as addSteps1_42_0 } from './1_42_0' -addSteps1_42_0() +// Note: There where no migrations for Release 42 import { addSteps as addSteps1_44_0 } from './1_44_0' addSteps1_44_0() diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index a1115590b5..8dc91f4be3 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -39,7 +39,7 @@ export interface IActionExecutionContext queuePart(part: IBlueprintPart, pieces: IBlueprintPiece[]): Promise /** Switch RouteSet State*/ - switchRouteSet(routeSetId: string, state: boolean): Promise + switchRouteSet(routeSetId: string, state: boolean | 'toggle'): Promise /** Misc actions */ // updateAction(newManifest: Pick): void // only updates itself. to allow for the next one to do something different // executePeripheralDeviceAction(deviceId: string, functionName: string, args: any[]): Promise diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index 89600ecb69..3b7a54db85 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -198,7 +198,7 @@ export interface ISwitchRouteSetAction extends ITriggeredActionBase { action: PlayoutActions.switchRouteSet filterChain: (IRundownPlaylistFilterLink | IGUIContextFilterLink)[] routeSetId: string - state: boolean + state: boolean | 'toggle' } export interface ITakeAction extends ITriggeredActionBase { diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 20b94317b6..ec002435c4 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -184,7 +184,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct partInstance.blockTakeUntil(time) } - async switchRouteSet(routeSetId: string, state: boolean): Promise { + async switchRouteSet(routeSetId: string, state: boolean | 'toggle'): Promise { this._playoutModel.switchRouteSet(routeSetId, state) } diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 10caee9a47..19d770d04b 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -196,7 +196,7 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa * @param routeSetId * @param isActive */ - switchRouteSet(routeSetId: string, isActive: boolean): void + switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): void /** * Clear the currently selected PartInstances, so that nothing is selected for playback diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 6ee433b9dd..dd7861a0c2 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -480,7 +480,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou return partInstance } - switchRouteSet(routeSetId: string, isActive: boolean): void { + switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): void { this.#baselineHelper.updateRouteSetActive(routeSetId, isActive) } diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 25a38d4813..9daaaf0727 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -1,4 +1,10 @@ -import { BlueprintMapping, BlueprintMappings, JSONBlobParse, TSR } from '@sofie-automation/blueprints-integration' +import { + BlueprintMapping, + BlueprintMappings, + JSONBlobParse, + StudioRouteBehavior, + TSR, +} from '@sofie-automation/blueprints-integration' import { MappingsExt, StudioIngestDevice, @@ -70,23 +76,23 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data ]) ) const routeSets = Object.fromEntries( - Object.entries(result.routeSets ?? {}).map((dev) => [ + Object.entries>(result.routeSets ?? {}).map((dev) => [ dev[0], literal>({ - name: (dev[1] as StudioRouteSet).name ?? '', - active: (dev[1] as StudioRouteSet).active ?? false, - defaultActive: (dev[1] as StudioRouteSet).defaultActive ?? false, - behavior: (dev[1] as StudioRouteSet).behavior ?? {}, - exclusivityGroup: (dev[1] as StudioRouteSet).exclusivityGroup ?? undefined, - routes: (dev[1] as StudioRouteSet).routes, + name: dev[1].name ?? '', + active: dev[1].active ?? false, + defaultActive: dev[1].defaultActive ?? false, + behavior: dev[1].behavior ?? StudioRouteBehavior.TOGGLE, + exclusivityGroup: dev[1].exclusivityGroup ?? undefined, + routes: dev[1].routes ?? [], }), ]) ) const routeSetExclusivityGroups = Object.fromEntries( - Object.entries(result.routeSetExclusivityGroups ?? {}).map((dev) => [ + Object.entries>(result.routeSetExclusivityGroups ?? {}).map((dev) => [ dev[0], literal>({ - name: (dev[1] as StudioRouteSetExclusivityGroup).name, + name: dev[1].name ?? '', }), ]) ) diff --git a/packages/job-worker/src/studio/model/StudioBaselineHelper.ts b/packages/job-worker/src/studio/model/StudioBaselineHelper.ts index c7e9772938..6d2c0408c6 100644 --- a/packages/job-worker/src/studio/model/StudioBaselineHelper.ts +++ b/packages/job-worker/src/studio/model/StudioBaselineHelper.ts @@ -86,7 +86,6 @@ export class StudioBaselineHelper { updateRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): void { const studio = this.#context.studio const saveOverrides = (newOps: SomeObjectOverrideOp[]) => { - // this.#overridesRouteSetBuffer = { defaults: this.#overridesRouteSetBuffer.defaults, overrides: newOps } this.#overridesRouteSetBuffer.overrides = newOps this.#routeSetChanged = true } diff --git a/packages/meteor-lib/src/triggers/actionFactory.ts b/packages/meteor-lib/src/triggers/actionFactory.ts index 3e7d93b54d..c3ba6d97ba 100644 --- a/packages/meteor-lib/src/triggers/actionFactory.ts +++ b/packages/meteor-lib/src/triggers/actionFactory.ts @@ -604,7 +604,7 @@ export function createAction( ts, ctx.studioId.get(), action.routeSetId, - 'toggle' + action.state ) ) case ClientActions.showEntireCurrentSegment: diff --git a/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx b/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx index e6d6e40cab..df41483cb0 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx @@ -150,7 +150,6 @@ export default translateWithTracker((props: IProp
- +
diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index af9049b38d..1ff832302c 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -22,13 +22,7 @@ import classNames from 'classnames' import { catchError, fetchFrom } from '../../../../lib/lib' import { NotificationCenter, Notification, NoticeLevel } from '../../../../lib/notifications/notifications' import { doModalDialog } from '../../../../lib/ModalDialog' -import { - PartId, - RundownId, - ShowStyleBaseId, - StudioId, - TriggeredActionId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, RundownId, ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylists, Rundowns, TriggeredActions } from '../../../../collections' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { SourceLayers, OutputLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -46,7 +40,6 @@ export interface PreviewContext { } interface IProps { - studioId: StudioId | null showStyleBaseId: ShowStyleBaseId | null sourceLayers: SourceLayers outputLayers: OutputLayers @@ -81,7 +74,7 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction }, }) - const { studioId, showStyleBaseId, sourceLayers, outputLayers } = props + const { showStyleBaseId, sourceLayers, outputLayers } = props useSubscription(MeteorPubSub.triggeredActions, showStyleBaseId ? [showStyleBaseId] : null) useSubscription(CorelibPubSub.rundownsWithShowStyleBases, showStyleBaseId ? [showStyleBaseId] : []) @@ -392,12 +385,12 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction return (
- {sorensen && previewContext.rundownPlaylist && showStyleBaseId && studioId && ( + {sorensen && previewContext.rundownPlaylist && showStyleBaseId && (
-

ACT

Date: Tue, 8 Oct 2024 13:03:54 +0100 Subject: [PATCH 4/5] fix: implement editor ui --- .../actionSelector/ActionSelector.tsx | 7 +- .../actionEditors/SwitchRouteSetEditor.tsx | 85 +++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/SwitchRouteSetEditor.tsx diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index 4a739952a8..d9f150dcf3 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -15,6 +15,7 @@ import { preventOverflow } from '@popperjs/core' import { ToggleSwitchControl } from '../../../../../../lib/Components/ToggleSwitch' import { DropdownInputControl, DropdownInputOption } from '../../../../../../lib/Components/DropdownInput' import { IntInputControl } from '../../../../../../lib/Components/IntInput' +import { SwitchRouteSetEditor } from './actionEditors/SwitchRouteSetEditor' interface IProps { action: SomeAction @@ -75,9 +76,7 @@ function getArguments(t: TFunction, action: SomeAction): string[] { case PlayoutActions.resyncRundownPlaylist: break case PlayoutActions.switchRouteSet: - if (action.routeSetId) { - result.push(t('Route Set: {{routeSet}}', { routeSet: action.routeSetId })) - } + result.push(t('State "{{state}}"', { state: action.state })) break case ClientActions.shelf: if (action.state === true) { @@ -257,7 +256,7 @@ function getActionParametersEditor( case PlayoutActions.activateAdlibTestingMode: return null case PlayoutActions.switchRouteSet: - return null + return case PlayoutActions.disableNextPiece: return (
diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/SwitchRouteSetEditor.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/SwitchRouteSetEditor.tsx new file mode 100644 index 0000000000..2f7750e18e --- /dev/null +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/actionEditors/SwitchRouteSetEditor.tsx @@ -0,0 +1,85 @@ +import { useTranslation } from 'react-i18next' +import { useTracker } from '../../../../../../../lib/ReactMeteorData/ReactMeteorData' +import { Studios } from '../../../../../../../collections' +import { DropdownInputControl, DropdownInputOption } from '../../../../../../../lib/Components/DropdownInput' +import { SwitchRouteSetProps } from '@sofie-automation/corelib/dist/worker/studio' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { StudioRouteSet } from '@sofie-automation/blueprints-integration' + +export function SwitchRouteSetEditor({ + action, + onChange, +}: Readonly<{ + action: SwitchRouteSetProps + onChange: (newVal: Partial) => void +}>): JSX.Element | null { + const { t } = useTranslation() + const allRouteSetOptions = useTracker[]>( + () => { + const studios = Studios.find({}, { sort: { name: 1 } }).fetch() + const routeSetOptions = studios.flatMap((studio) => { + const routeSets = applyAndValidateOverrides(studio.routeSetsWithOverrides).obj + return Object.entries(routeSets).map( + ([id, routeSet]): Omit, 'i'> => ({ + value: id, + name: studios.length > 1 ? `${studio.name} - ${routeSet.name}` : routeSet.name, + }) + ) + }) + + return routeSetOptions.map((option, i): DropdownInputOption => ({ ...option, i })) + }, + [], + [] + ) + + return ( + <> +
+ + + classNames="input text-input input-m" + value={action.routeSetId} + options={allRouteSetOptions} + handleUpdate={(newVal) => { + onChange({ + ...action, + routeSetId: newVal, + }) + }} + /> +
+ +
+ + + classNames="input text-input input-m" + value={action.state} + options={[ + { + name: t('Enabled'), + value: true, + i: 0, + }, + { + name: t('Disabled'), + value: false, + i: 1, + }, + { + name: t('Toggle'), + value: 'toggle', + i: 2, + }, + ]} + handleUpdate={(newVal) => { + onChange({ + ...action, + state: newVal, + }) + }} + /> +
+ + ) +} From 0bd2d4d17e3f1242018b523f10cf6b87aaba04ff Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 8 Oct 2024 13:05:24 +0100 Subject: [PATCH 5/5] chore: lint --- packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx b/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx index df41483cb0..d4992cd48d 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyleBaseSettings.tsx @@ -21,7 +21,6 @@ import { Blueprints, ShowStyleBases, ShowStyleVariants, Studios } from '../../co import { JSONBlobParse } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { ShowStyleBaseBlueprintConfigurationSettings } from './ShowStyle/BlueprintConfiguration' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' interface IProps { match: {