From 53dde09345ff1df8415f670685889d37ac5c1b98 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 23 Aug 2024 16:35:47 +0100 Subject: [PATCH 1/6] wip: refactor --- .../src/integrations/vizMSE/convertState.ts | 190 +++++++ .../src/integrations/vizMSE/diffState.ts | 342 +++++++++++ .../src/integrations/vizMSE/index.ts | 535 +----------------- .../src/integrations/vizMSE/vizMSEManager.ts | 5 +- 4 files changed, 564 insertions(+), 508 deletions(-) create mode 100644 packages/timeline-state-resolver/src/integrations/vizMSE/convertState.ts create mode 100644 packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/convertState.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/convertState.ts new file mode 100644 index 000000000..d04a62ced --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/convertState.ts @@ -0,0 +1,190 @@ +import { literal } from '../../lib' +import { + Timeline, + TSRTimelineContent, + Mappings, + ResolvedTimelineObjectInstanceExtended, + Mapping, + SomeMappingVizMSE, + DeviceType, + TimelineContentVIZMSEAny, + TimelineContentTypeVizMSE, + TimelineContentVIZMSEElementInternal, + TimelineContentVIZMSEElementPilot, +} from 'timeline-state-resolver-types' +import _ = require('underscore') +import type { + VizMSEState, + VizMSEStateLayerLoadAllElements, + VizMSEStateLayerContinue, + VizMSEStateLayerInitializeShows, + VizMSEStateLayerCleanupShows, + VizMSEStateLayerConcept, + VizMSEStateLayer, + VizMSEStateLayerInternal, + VizMSEStateLayerPilot, +} from './types' +import type { DeviceContextAPI } from '../../service/device' +import type { VizMSEManager } from './vizMSEManager' + +export function convertStateToVizMSE( + logger: DeviceContextAPI['logger'], + vizmseManager: VizMSEManager | undefined, + timelineState: Timeline.TimelineState, + mappings: Mappings +): VizMSEState { + const state: VizMSEState = { + time: timelineState.time, + layer: {}, + } + + _.each(timelineState.layers, (layer, layerName: string) => { + const layerExt: ResolvedTimelineObjectInstanceExtended = layer + let foundMapping = mappings[layerName] as Mapping + + let isLookahead = false + if (!foundMapping && layerExt.isLookahead && layerExt.lookaheadForLayer) { + foundMapping = mappings[layerExt.lookaheadForLayer] as Mapping + isLookahead = true + } + if (foundMapping && foundMapping.device === DeviceType.VIZMSE) { + if (layer.content) { + const content = layer.content as TimelineContentVIZMSEAny + + switch (content.type) { + case TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS: + state.layer[layerName] = literal({ + timelineObjId: layer.id, + contentType: TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS, + }) + break + case TimelineContentTypeVizMSE.CLEAR_ALL_ELEMENTS: { + // Special case: clear all graphics: + const showId = vizmseManager?.resolveShowNameToId(content.showName) + if (!showId) { + logger.warning( + `convertStateToVizMSE: Unable to find Show Id for Clear-All template and Show Name "${content.showName}"` + ) + break + } + state.isClearAll = { + timelineObjId: layer.id, + showId, + channelsToSendCommands: content.channelsToSendCommands, + } + break + } + case TimelineContentTypeVizMSE.CONTINUE: + state.layer[layerName] = literal({ + timelineObjId: layer.id, + contentType: TimelineContentTypeVizMSE.CONTINUE, + direction: content.direction, + reference: content.reference, + }) + break + case TimelineContentTypeVizMSE.INITIALIZE_SHOWS: + state.layer[layerName] = literal({ + timelineObjId: layer.id, + contentType: TimelineContentTypeVizMSE.INITIALIZE_SHOWS, + showIds: _.compact(content.showNames.map((showName) => vizmseManager?.resolveShowNameToId(showName))), + }) + break + case TimelineContentTypeVizMSE.CLEANUP_SHOWS: + state.layer[layerName] = literal({ + timelineObjId: layer.id, + contentType: TimelineContentTypeVizMSE.CLEANUP_SHOWS, + showIds: _.compact(content.showNames.map((showName) => vizmseManager?.resolveShowNameToId(showName))), + }) + break + case TimelineContentTypeVizMSE.CONCEPT: + state.layer[layerName] = literal({ + timelineObjId: layer.id, + contentType: TimelineContentTypeVizMSE.CONCEPT, + concept: content.concept, + }) + break + default: { + const stateLayer = contentToStateLayer(logger, vizmseManager, layer.id, content) + if (stateLayer) { + if (isLookahead) stateLayer.lookahead = true + + state.layer[layerName] = stateLayer + } + break + } + } + } + } + }) + + if (state.isClearAll) { + // clear rest of state: + state.layer = {} + } + + // Fix references: + _.each(state.layer, (layer) => { + if (layer.contentType === TimelineContentTypeVizMSE.CONTINUE) { + const otherLayer = state.layer[layer.reference] + if (otherLayer) { + if ( + otherLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || + otherLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT + ) { + layer.referenceContent = otherLayer + } else { + // it's not possible to reference that kind of object + logger.warning( + `object "${layer.timelineObjId}" of contentType="${layer.contentType}", cannot reference object "${otherLayer.timelineObjId}" on layer "${layer.reference}" of contentType="${otherLayer.contentType}" ` + ) + } + } + } + }) + + return state +} +function contentToStateLayer( + logger: DeviceContextAPI['logger'], + vizmseManager: VizMSEManager | undefined, + timelineObjId: string, + content: TimelineContentVIZMSEElementInternal | TimelineContentVIZMSEElementPilot +): VizMSEStateLayer | undefined { + if (content.type === TimelineContentTypeVizMSE.ELEMENT_INTERNAL) { + const showId = vizmseManager?.resolveShowNameToId(content.showName) + if (!showId) { + logger.warning( + `contentToStateLayer: Unable to find Show Id for template "${content.templateName}" and Show Name "${content.showName}"` + ) + return undefined + } + const o: VizMSEStateLayerInternal = { + timelineObjId: timelineObjId, + contentType: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, + continueStep: content.continueStep, + cue: content.cue, + outTransition: content.outTransition, + + templateName: content.templateName, + templateData: content.templateData, + channelName: content.channelName, + delayTakeAfterOutTransition: content.delayTakeAfterOutTransition, + showId, + } + return o + } else if (content.type === TimelineContentTypeVizMSE.ELEMENT_PILOT) { + const o: VizMSEStateLayerPilot = { + timelineObjId: timelineObjId, + contentType: TimelineContentTypeVizMSE.ELEMENT_PILOT, + continueStep: content.continueStep, + cue: content.cue, + outTransition: content.outTransition, + + templateVcpId: content.templateVcpId, + channelName: content.channelName, + delayTakeAfterOutTransition: content.delayTakeAfterOutTransition, + } + return o + } + return undefined +} diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts new file mode 100644 index 000000000..0b289e502 --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts @@ -0,0 +1,342 @@ +import { literal } from '../../lib' +import { TimelineContentTypeVizMSE, VizMSEOptions, VIZMSETransitionType } from 'timeline-state-resolver-types' +import _ = require('underscore') +import { + VizMSECommand, + VizMSECommandCleanupShows, + VizMSECommandClearAllElements, + VizMSECommandClearAllEngines, + VizMSECommandContinue, + VizMSECommandContinueReverse, + VizMSECommandCue, + VizMSECommandElementBase, + VizMSECommandInitializeShows, + VizMSECommandLoadAllElements, + VizMSECommandPrepare, + VizMSECommandSetConcept, + VizMSECommandTake, + VizMSECommandTakeOut, + VizMSECommandType, + VizMSEState, + VizMSEStateLayer, +} from './types' +import { VizMSEManager } from './vizMSEManager' +import type { DeviceContextAPI } from '../../service/device' + +/** The ideal time to prepare elements before going on air */ +const IDEAL_PREPARE_TIME = 1000 +/** Minimum time to wait after preparing elements */ +const PREPARE_TIME_WAIT = 50 + +export function diffVizMSEStates( + oldState: VizMSEState | undefined, + newState: VizMSEState, + stateTime: number, + currentTime: number, + options: VizMSEOptions | undefined, + logger: DeviceContextAPI['logger'] +): Array { + const highPrioCommands: VizMSECommand[] = [] + const lowPrioCommands: VizMSECommand[] = [] + + const addCommand = (command: VizMSECommand, lowPriority?: boolean) => { + ;(lowPriority ? lowPrioCommands : highPrioCommands).push(command) + } + + /** The time of when to run "preparation" commands */ + let prepareTime = Math.min( + stateTime, + Math.max( + stateTime - IDEAL_PREPARE_TIME, + (oldState?.time ?? 0) + PREPARE_TIME_WAIT // earliset possible prepareTime + ) + ) + if (prepareTime < currentTime) { + // Only to not emit an unnessesary slowCommand event + prepareTime = currentTime + } + if (stateTime < prepareTime) { + prepareTime = stateTime - 10 + } + + _.each(newState.layer, (newLayer: VizMSEStateLayer, layerId: string) => { + const oldLayer: VizMSEStateLayer | undefined = oldState?.layer?.[layerId] + + if (newLayer.contentType === TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS) { + if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { + addCommand( + literal({ + timelineObjId: newLayer.timelineObjId, + fromLookahead: newLayer.lookahead, + layerId: layerId, + + type: VizMSECommandType.LOAD_ALL_ELEMENTS, + time: stateTime, + }), + newLayer.lookahead + ) + } + } else if (newLayer.contentType === TimelineContentTypeVizMSE.CONTINUE) { + if ((!oldLayer || !_.isEqual(newLayer, oldLayer)) && newLayer.referenceContent) { + const props: Omit = { + timelineObjId: newLayer.timelineObjId, + fromLookahead: newLayer.lookahead, + layerId: layerId, + + content: VizMSEManager.getPlayoutItemContentFromLayer(newLayer.referenceContent), + } + if ((newLayer.direction || 1) === 1) { + addCommand( + literal({ + ...props, + type: VizMSECommandType.CONTINUE_ELEMENT, + time: stateTime, + }), + newLayer.lookahead + ) + } else { + addCommand( + literal({ + ...props, + type: VizMSECommandType.CONTINUE_ELEMENT_REVERSE, + time: stateTime, + }), + newLayer.lookahead + ) + } + } + } else if (newLayer.contentType === TimelineContentTypeVizMSE.INITIALIZE_SHOWS) { + if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { + addCommand( + literal({ + type: VizMSECommandType.INITIALIZE_SHOWS, + timelineObjId: newLayer.timelineObjId, + showIds: newLayer.showIds, + time: stateTime, + }), + newLayer.lookahead + ) + } + } else if (newLayer.contentType === TimelineContentTypeVizMSE.CLEANUP_SHOWS) { + if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { + const command: VizMSECommandCleanupShows = literal({ + type: VizMSECommandType.CLEANUP_SHOWS, + timelineObjId: newLayer.timelineObjId, + showIds: newLayer.showIds, + time: stateTime, + }) + addCommand(command, newLayer.lookahead) + } + } else if (newLayer.contentType === TimelineContentTypeVizMSE.CONCEPT) { + if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { + addCommand( + literal({ + concept: newLayer.concept, + type: VizMSECommandType.SET_CONCEPT, + time: stateTime, + timelineObjId: newLayer.timelineObjId, + }) + ) + } + } else { + const props: Omit = { + timelineObjId: newLayer.timelineObjId, + fromLookahead: newLayer.lookahead, + layerId: layerId, + + content: VizMSEManager.getPlayoutItemContentFromLayer(newLayer), + } + if ( + !oldLayer || + !_.isEqual( + _.omit(newLayer, ['continueStep', 'timelineObjId', 'outTransition']), + _.omit(oldLayer, ['continueStep', 'timelineObjId', 'outTransition']) + ) + ) { + if ( + newLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || + newLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT + ) { + // Maybe prepare the element first: + addCommand( + literal({ + ...props, + type: VizMSECommandType.PREPARE_ELEMENT, + time: prepareTime, + }), + newLayer.lookahead + ) + + if (newLayer.cue) { + // Cue the element + addCommand( + literal({ + ...props, + type: VizMSECommandType.CUE_ELEMENT, + time: stateTime, + }), + newLayer.lookahead + ) + } else { + // Start playing element + addCommand( + literal({ + ...props, + type: VizMSECommandType.TAKE_ELEMENT, + time: stateTime, + }), + newLayer.lookahead + ) + } + } + } else if ( + (oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || + oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT) && + (newLayer.continueStep || 0) > (oldLayer.continueStep || 0) + ) { + // An increase in continueStep should result in triggering a continue: + addCommand( + literal({ + ...props, + type: VizMSECommandType.CONTINUE_ELEMENT, + time: stateTime, + }), + newLayer.lookahead + ) + } else if ( + (oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || + oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT) && + (newLayer.continueStep || 0) < (oldLayer.continueStep || 0) + ) { + // A decrease in continueStep should result in triggering a continue: + addCommand( + literal({ + ...props, + type: VizMSECommandType.CONTINUE_ELEMENT_REVERSE, + time: stateTime, + }), + newLayer.lookahead + ) + } + } + }) + + _.each(oldState?.layer ?? {}, (oldLayer: VizMSEStateLayer, layerId: string) => { + const newLayer = newState.layer[layerId] + if (!newLayer) { + if ( + oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || + oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT + ) { + // Stopped playing + addCommand( + literal({ + type: VizMSECommandType.TAKEOUT_ELEMENT, + time: stateTime, + timelineObjId: oldLayer.timelineObjId, + fromLookahead: oldLayer.lookahead, + layerId: layerId, + transition: oldLayer && oldLayer.outTransition, + content: VizMSEManager.getPlayoutItemContentFromLayer(oldLayer), + }), + oldLayer.lookahead + ) + } else if (oldLayer.contentType === TimelineContentTypeVizMSE.INITIALIZE_SHOWS) { + addCommand( + literal({ + type: VizMSECommandType.INITIALIZE_SHOWS, + timelineObjId: oldLayer.timelineObjId, + showIds: [], + time: stateTime, + }), + oldLayer.lookahead + ) + } + } + }) + + if (newState.isClearAll && !oldState?.isClearAll) { + // Special: clear all graphics + + const clearingCommands: VizMSECommand[] = [] + + const templateName = options && options.clearAllTemplateName + if (!templateName) { + logger.warning(`vizMSE: initOptions.clearAllTemplateName is not set!`) + } else { + // Start playing special element: + clearingCommands.push( + literal({ + timelineObjId: newState.isClearAll.timelineObjId, + time: stateTime, + type: VizMSECommandType.CLEAR_ALL_ELEMENTS, + templateName: templateName, + showId: newState.isClearAll.showId, + }) + ) + } + if ( + newState.isClearAll.channelsToSendCommands && + options && + options.clearAllCommands && + options.clearAllCommands.length + ) { + // Send special commands to the engines: + clearingCommands.push( + literal({ + timelineObjId: newState.isClearAll.timelineObjId, + time: stateTime, + type: VizMSECommandType.CLEAR_ALL_ENGINES, + channels: newState.isClearAll.channelsToSendCommands, + commands: options.clearAllCommands, + }) + ) + } + return clearingCommands + } + const sortCommands = (commands: VizMSECommand[]): VizMSECommand[] => { + // Sort the commands so that take out:s are run first + return commands.sort((a, b) => { + if (a.type === VizMSECommandType.TAKEOUT_ELEMENT && b.type !== VizMSECommandType.TAKEOUT_ELEMENT) return -1 + if (a.type !== VizMSECommandType.TAKEOUT_ELEMENT && b.type === VizMSECommandType.TAKEOUT_ELEMENT) return 1 + return 0 + }) + } + sortCommands(highPrioCommands) + sortCommands(lowPrioCommands) + + const concatCommands = sortCommands(highPrioCommands.concat(lowPrioCommands)) + + let highestDelay = 0 + concatCommands.forEach((command) => { + if (command.type === VizMSECommandType.TAKEOUT_ELEMENT) { + if (command.transition && command.transition.delay) { + if (command.transition.delay > highestDelay) { + highestDelay = command.transition.delay + } + } + } + }) + + if (highestDelay > 0) { + concatCommands.forEach((command, index) => { + if ( + command.type === VizMSECommandType.TAKE_ELEMENT && + command.layerId && + (newState.layer[command.layerId].contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || + !!newState.layer[command.layerId].delayTakeAfterOutTransition) + ) { + ;(concatCommands[index] as VizMSECommandTake).transition = { + type: VIZMSETransitionType.DELAY, + delay: highestDelay + 20, + } + } + }) + } + + if (concatCommands.length) { + logger.debug(`VIZMSE: COMMANDS: ${JSON.stringify(sortCommands(concatCommands))}`) + } + + return sortCommands(concatCommands) +} diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts index b965bb03c..2f4121998 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts @@ -1,69 +1,30 @@ import * as _ from 'underscore' import { CommandWithContext, DeviceStatus, DeviceWithState, StatusCode } from './../../devices/device' - import { ActionExecutionResult, ActionExecutionResultCode, DeviceOptionsVizMSE, DeviceType, - Mapping, Mappings, MediaObject, - ResolvedTimelineObjectInstanceExtended, Timeline, - TimelineContentTypeVizMSE, - TimelineContentVIZMSEAny, TSRTimelineContent, VizMSEOptions, - VIZMSETransitionType, VizMSEActions, - SomeMappingVizMSE, - TimelineContentVIZMSEElementPilot, - TimelineContentVIZMSEElementInternal, VizMSEActionExecutionPayload, VizMSEActionExecutionResult, VizResetPayload, } from 'timeline-state-resolver-types' - import { createMSE, MSE } from '@tv2media/v-connection' - import { DoOnTime, SendMode } from '../../devices/doOnTime' - import { ExpectedPlayoutItem } from '../../expectedPlayoutItems' -import { actionNotFoundMessage, endTrace, startTrace, t, literal } from '../../lib' +import { actionNotFoundMessage, endTrace, startTrace, t } from '../../lib' import { HTTPClientError, HTTPServerError } from '@tv2media/v-connection/dist/msehttp' import { VizMSEManager } from './vizMSEManager' -import { - VizMSECommand, - VizMSEState, - VizMSEStateLayerLoadAllElements, - VizMSEStateLayerContinue, - VizMSEStateLayerInitializeShows, - VizMSEStateLayerCleanupShows, - VizMSEStateLayerConcept, - VizMSEStateLayer, - VizMSEStateLayerInternal, - VizMSEStateLayerPilot, - VizMSECommandType, - VizMSECommandLoadAllElements, - VizMSECommandElementBase, - VizMSECommandContinue, - VizMSECommandContinueReverse, - VizMSECommandInitializeShows, - VizMSECommandCleanupShows, - VizMSECommandSetConcept, - VizMSECommandPrepare, - VizMSECommandCue, - VizMSECommandTake, - VizMSECommandTakeOut, - VizMSECommandClearAllElements, - VizMSECommandClearAllEngines, -} from './types' - -/** The ideal time to prepare elements before going on air */ -const IDEAL_PREPARE_TIME = 1000 -/** Minimum time to wait after preparing elements */ -const PREPARE_TIME_WAIT = 50 +import { VizMSECommand, VizMSEState, VizMSECommandType } from './types' +import { diffVizMSEStates } from './diffState' +import { convertStateToVizMSE } from './convertState' +import type { DeviceContextAPI } from '../../service/device' export interface DeviceOptionsVizMSEInternal extends DeviceOptionsVizMSE { commandReceiver?: CommandReceiver @@ -332,171 +293,30 @@ export class VizMSEDevice extends DeviceWithState['logger'] { + return { + debug: (...args) => { + this.emit('debug', ...args) + }, + info: (...args) => { + this.emit('info', ...args) + }, + warning: (...args) => { + this.emit('warning', ...args) + }, + error: (...args) => { + this.emit('error', ...args) + }, + } + } + /** * Takes a timeline state and returns a VizMSE State that will work with the state lib. * @param timelineState The timeline state to generate from. */ convertStateToVizMSE(timelineState: Timeline.TimelineState, mappings: Mappings): VizMSEState { - const state: VizMSEState = { - time: timelineState.time, - layer: {}, - } - - _.each(timelineState.layers, (layer, layerName: string) => { - const layerExt: ResolvedTimelineObjectInstanceExtended = layer - let foundMapping = mappings[layerName] as Mapping - - let isLookahead = false - if (!foundMapping && layerExt.isLookahead && layerExt.lookaheadForLayer) { - foundMapping = mappings[layerExt.lookaheadForLayer] as Mapping - isLookahead = true - } - if (foundMapping && foundMapping.device === DeviceType.VIZMSE && foundMapping.deviceId === this.deviceId) { - if (layer.content) { - const content = layer.content as TimelineContentVIZMSEAny - - switch (content.type) { - case TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS: - state.layer[layerName] = literal({ - timelineObjId: layer.id, - contentType: TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS, - }) - break - case TimelineContentTypeVizMSE.CLEAR_ALL_ELEMENTS: { - // Special case: clear all graphics: - const showId = this._vizmseManager?.resolveShowNameToId(content.showName) - if (!showId) { - this.emit( - 'warning', - `convertStateToVizMSE: Unable to find Show Id for Clear-All template and Show Name "${content.showName}"` - ) - break - } - state.isClearAll = { - timelineObjId: layer.id, - showId, - channelsToSendCommands: content.channelsToSendCommands, - } - break - } - case TimelineContentTypeVizMSE.CONTINUE: - state.layer[layerName] = literal({ - timelineObjId: layer.id, - contentType: TimelineContentTypeVizMSE.CONTINUE, - direction: content.direction, - reference: content.reference, - }) - break - case TimelineContentTypeVizMSE.INITIALIZE_SHOWS: - state.layer[layerName] = literal({ - timelineObjId: layer.id, - contentType: TimelineContentTypeVizMSE.INITIALIZE_SHOWS, - showIds: _.compact( - content.showNames.map((showName) => this._vizmseManager?.resolveShowNameToId(showName)) - ), - }) - break - case TimelineContentTypeVizMSE.CLEANUP_SHOWS: - state.layer[layerName] = literal({ - timelineObjId: layer.id, - contentType: TimelineContentTypeVizMSE.CLEANUP_SHOWS, - showIds: _.compact( - content.showNames.map((showName) => this._vizmseManager?.resolveShowNameToId(showName)) - ), - }) - break - case TimelineContentTypeVizMSE.CONCEPT: - state.layer[layerName] = literal({ - timelineObjId: layer.id, - contentType: TimelineContentTypeVizMSE.CONCEPT, - concept: content.concept, - }) - break - default: { - const stateLayer = this._contentToStateLayer(layer.id, content) - if (stateLayer) { - if (isLookahead) stateLayer.lookahead = true - - state.layer[layerName] = stateLayer - } - break - } - } - } - } - }) - - if (state.isClearAll) { - // clear rest of state: - state.layer = {} - } - - // Fix references: - _.each(state.layer, (layer) => { - if (layer.contentType === TimelineContentTypeVizMSE.CONTINUE) { - const otherLayer = state.layer[layer.reference] - if (otherLayer) { - if ( - otherLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || - otherLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT - ) { - layer.referenceContent = otherLayer - } else { - // it's not possible to reference that kind of object - this.emit( - 'warning', - `object "${layer.timelineObjId}" of contentType="${layer.contentType}", cannot reference object "${otherLayer.timelineObjId}" on layer "${layer.reference}" of contentType="${otherLayer.contentType}" ` - ) - } - } - } - }) - - return state - } - - private _contentToStateLayer( - timelineObjId: string, - content: TimelineContentVIZMSEElementInternal | TimelineContentVIZMSEElementPilot - ): VizMSEStateLayer | undefined { - if (content.type === TimelineContentTypeVizMSE.ELEMENT_INTERNAL) { - const showId = this._vizmseManager?.resolveShowNameToId(content.showName) - if (!showId) { - this.emit( - 'warning', - `_contentToStateLayer: Unable to find Show Id for template "${content.templateName}" and Show Name "${content.showName}"` - ) - return undefined - } - const o: VizMSEStateLayerInternal = { - timelineObjId: timelineObjId, - contentType: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, - continueStep: content.continueStep, - cue: content.cue, - outTransition: content.outTransition, - - templateName: content.templateName, - templateData: content.templateData, - channelName: content.channelName, - delayTakeAfterOutTransition: content.delayTakeAfterOutTransition, - showId, - } - return o - } else if (content.type === TimelineContentTypeVizMSE.ELEMENT_PILOT) { - const o: VizMSEStateLayerPilot = { - timelineObjId: timelineObjId, - contentType: TimelineContentTypeVizMSE.ELEMENT_PILOT, - continueStep: content.continueStep, - cue: content.cue, - outTransition: content.outTransition, - - templateVcpId: content.templateVcpId, - channelName: content.channelName, - delayTakeAfterOutTransition: content.delayTakeAfterOutTransition, - } - return o - } - return undefined + return convertStateToVizMSE(this._createFakeLogger(), this._vizmseManager, timelineState, mappings) } /** @@ -587,309 +407,14 @@ export class VizMSEDevice extends DeviceWithState { - const highPrioCommands: VizMSECommand[] = [] - const lowPrioCommands: VizMSECommand[] = [] - - const addCommand = (command: VizMSECommand, lowPriority?: boolean) => { - ;(lowPriority ? lowPrioCommands : highPrioCommands).push(command) - } - - /** The time of when to run "preparation" commands */ - let prepareTime = Math.min( + return diffVizMSEStates( + oldState, + newState, time, - Math.max( - time - IDEAL_PREPARE_TIME, - oldState.time + PREPARE_TIME_WAIT // earliset possible prepareTime - ) + this.getCurrentTime(), + this._initOptions, + this._createFakeLogger() ) - if (prepareTime < this.getCurrentTime()) { - // Only to not emit an unnessesary slowCommand event - prepareTime = this.getCurrentTime() - } - if (time < prepareTime) { - prepareTime = time - 10 - } - - _.each(newState.layer, (newLayer: VizMSEStateLayer, layerId: string) => { - const oldLayer: VizMSEStateLayer | undefined = oldState.layer[layerId] - - if (newLayer.contentType === TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS) { - if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { - addCommand( - literal({ - timelineObjId: newLayer.timelineObjId, - fromLookahead: newLayer.lookahead, - layerId: layerId, - - type: VizMSECommandType.LOAD_ALL_ELEMENTS, - time: time, - }), - newLayer.lookahead - ) - } - } else if (newLayer.contentType === TimelineContentTypeVizMSE.CONTINUE) { - if ((!oldLayer || !_.isEqual(newLayer, oldLayer)) && newLayer.referenceContent) { - const props: Omit = { - timelineObjId: newLayer.timelineObjId, - fromLookahead: newLayer.lookahead, - layerId: layerId, - - content: VizMSEManager.getPlayoutItemContentFromLayer(newLayer.referenceContent), - } - if ((newLayer.direction || 1) === 1) { - addCommand( - literal({ - ...props, - type: VizMSECommandType.CONTINUE_ELEMENT, - time: time, - }), - newLayer.lookahead - ) - } else { - addCommand( - literal({ - ...props, - type: VizMSECommandType.CONTINUE_ELEMENT_REVERSE, - time: time, - }), - newLayer.lookahead - ) - } - } - } else if (newLayer.contentType === TimelineContentTypeVizMSE.INITIALIZE_SHOWS) { - if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { - addCommand( - literal({ - type: VizMSECommandType.INITIALIZE_SHOWS, - timelineObjId: newLayer.timelineObjId, - showIds: newLayer.showIds, - time: time, - }), - newLayer.lookahead - ) - } - } else if (newLayer.contentType === TimelineContentTypeVizMSE.CLEANUP_SHOWS) { - if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { - const command: VizMSECommandCleanupShows = literal({ - type: VizMSECommandType.CLEANUP_SHOWS, - timelineObjId: newLayer.timelineObjId, - showIds: newLayer.showIds, - time: time, - }) - addCommand(command, newLayer.lookahead) - } - } else if (newLayer.contentType === TimelineContentTypeVizMSE.CONCEPT) { - if (!oldLayer || !_.isEqual(newLayer, oldLayer)) { - addCommand( - literal({ - concept: newLayer.concept, - type: VizMSECommandType.SET_CONCEPT, - time: time, - timelineObjId: newLayer.timelineObjId, - }) - ) - } - } else { - const props: Omit = { - timelineObjId: newLayer.timelineObjId, - fromLookahead: newLayer.lookahead, - layerId: layerId, - - content: VizMSEManager.getPlayoutItemContentFromLayer(newLayer), - } - if ( - !oldLayer || - !_.isEqual( - _.omit(newLayer, ['continueStep', 'timelineObjId', 'outTransition']), - _.omit(oldLayer, ['continueStep', 'timelineObjId', 'outTransition']) - ) - ) { - if ( - newLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || - newLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT - ) { - // Maybe prepare the element first: - addCommand( - literal({ - ...props, - type: VizMSECommandType.PREPARE_ELEMENT, - time: prepareTime, - }), - newLayer.lookahead - ) - - if (newLayer.cue) { - // Cue the element - addCommand( - literal({ - ...props, - type: VizMSECommandType.CUE_ELEMENT, - time: time, - }), - newLayer.lookahead - ) - } else { - // Start playing element - addCommand( - literal({ - ...props, - type: VizMSECommandType.TAKE_ELEMENT, - time: time, - }), - newLayer.lookahead - ) - } - } - } else if ( - (oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || - oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT) && - (newLayer.continueStep || 0) > (oldLayer.continueStep || 0) - ) { - // An increase in continueStep should result in triggering a continue: - addCommand( - literal({ - ...props, - type: VizMSECommandType.CONTINUE_ELEMENT, - time: time, - }), - newLayer.lookahead - ) - } else if ( - (oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || - oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT) && - (newLayer.continueStep || 0) < (oldLayer.continueStep || 0) - ) { - // A decrease in continueStep should result in triggering a continue: - addCommand( - literal({ - ...props, - type: VizMSECommandType.CONTINUE_ELEMENT_REVERSE, - time: time, - }), - newLayer.lookahead - ) - } - } - }) - - _.each(oldState.layer, (oldLayer: VizMSEStateLayer, layerId: string) => { - const newLayer = newState.layer[layerId] - if (!newLayer) { - if ( - oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || - oldLayer.contentType === TimelineContentTypeVizMSE.ELEMENT_PILOT - ) { - // Stopped playing - addCommand( - literal({ - type: VizMSECommandType.TAKEOUT_ELEMENT, - time: time, - timelineObjId: oldLayer.timelineObjId, - fromLookahead: oldLayer.lookahead, - layerId: layerId, - transition: oldLayer && oldLayer.outTransition, - content: VizMSEManager.getPlayoutItemContentFromLayer(oldLayer), - }), - oldLayer.lookahead - ) - } else if (oldLayer.contentType === TimelineContentTypeVizMSE.INITIALIZE_SHOWS) { - addCommand( - literal({ - type: VizMSECommandType.INITIALIZE_SHOWS, - timelineObjId: oldLayer.timelineObjId, - showIds: [], - time: time, - }), - oldLayer.lookahead - ) - } - } - }) - - if (newState.isClearAll && !oldState.isClearAll) { - // Special: clear all graphics - - const clearingCommands: VizMSECommand[] = [] - - const templateName = this._initOptions && this._initOptions.clearAllTemplateName - if (!templateName) { - this.emit('warning', `vizMSE: initOptions.clearAllTemplateName is not set!`) - } else { - // Start playing special element: - clearingCommands.push( - literal({ - timelineObjId: newState.isClearAll.timelineObjId, - time: time, - type: VizMSECommandType.CLEAR_ALL_ELEMENTS, - templateName: templateName, - showId: newState.isClearAll.showId, - }) - ) - } - if ( - newState.isClearAll.channelsToSendCommands && - this._initOptions && - this._initOptions.clearAllCommands && - this._initOptions.clearAllCommands.length - ) { - // Send special commands to the engines: - clearingCommands.push( - literal({ - timelineObjId: newState.isClearAll.timelineObjId, - time: time, - type: VizMSECommandType.CLEAR_ALL_ENGINES, - channels: newState.isClearAll.channelsToSendCommands, - commands: this._initOptions.clearAllCommands, - }) - ) - } - return clearingCommands - } - const sortCommands = (commands: VizMSECommand[]): VizMSECommand[] => { - // Sort the commands so that take out:s are run first - return commands.sort((a, b) => { - if (a.type === VizMSECommandType.TAKEOUT_ELEMENT && b.type !== VizMSECommandType.TAKEOUT_ELEMENT) return -1 - if (a.type !== VizMSECommandType.TAKEOUT_ELEMENT && b.type === VizMSECommandType.TAKEOUT_ELEMENT) return 1 - return 0 - }) - } - sortCommands(highPrioCommands) - sortCommands(lowPrioCommands) - - const concatCommands = sortCommands(highPrioCommands.concat(lowPrioCommands)) - - let highestDelay = 0 - concatCommands.forEach((command) => { - if (command.type === VizMSECommandType.TAKEOUT_ELEMENT) { - if (command.transition && command.transition.delay) { - if (command.transition.delay > highestDelay) { - highestDelay = command.transition.delay - } - } - } - }) - - if (highestDelay > 0) { - concatCommands.forEach((command, index) => { - if ( - command.type === VizMSECommandType.TAKE_ELEMENT && - command.layerId && - (newState.layer[command.layerId].contentType === TimelineContentTypeVizMSE.ELEMENT_INTERNAL || - !!newState.layer[command.layerId].delayTakeAfterOutTransition) - ) { - ;(concatCommands[index] as VizMSECommandTake).transition = { - type: VIZMSETransitionType.DELAY, - delay: highestDelay + 20, - } - } - }) - } - - if (concatCommands.length) { - this.emitDebug(`VIZMSE: COMMANDS: ${JSON.stringify(sortCommands(concatCommands))}`) - } - - return sortCommands(concatCommands) } private async _doCommand(command: VizMSECommand, context: string, timlineObjId: string): Promise { const time = this.getCurrentTime() diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/vizMSEManager.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/vizMSEManager.ts index d73f0b662..51090459b 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/vizMSEManager.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/vizMSEManager.ts @@ -1239,9 +1239,8 @@ export class VizMSEManager extends EventEmitter { this.emit('connectionChanged', this._mseConnected && this._msePingConnected) } - public clearAllWaitWithLayer(_portId: string) { - // HACK: Prior to #344 this was broken. This has been left in the broken state until it can be tested that the 'fix' doesn't cause other issues SOFIE-3419 - // this._waitWithLayers.clearAllForKey(portId) + public clearAllWaitWithLayer(portId: string) { + this._waitWithLayers.clearAllForKey(portId) } /** * Returns true if the wait was cleared from someone else From d11e777408208060750622e793406d078a240413 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 23 Aug 2024 17:01:32 +0100 Subject: [PATCH 2/6] wip: bulk of refactor --- .../timeline-state-resolver/src/conductor.ts | 16 +- .../src/integrations/vizMSE/diffState.ts | 11 +- .../src/integrations/vizMSE/index.ts | 285 ++++++------------ .../src/integrations/vizMSE/types.ts | 6 + .../src/service/DeviceInstance.ts | 8 +- .../src/service/device.ts | 6 +- .../src/service/devices.ts | 8 + 7 files changed, 122 insertions(+), 218 deletions(-) diff --git a/packages/timeline-state-resolver/src/conductor.ts b/packages/timeline-state-resolver/src/conductor.ts index ad04f35c5..14a03f789 100644 --- a/packages/timeline-state-resolver/src/conductor.ts +++ b/packages/timeline-state-resolver/src/conductor.ts @@ -37,6 +37,7 @@ import { DeviceOptionsPharos, DeviceOptionsTriCaster, DeviceOptionsSingularLive, + DeviceOptionsVizMSE, } from 'timeline-state-resolver-types' import { DoOnTime } from './devices/doOnTime' @@ -49,7 +50,6 @@ import { DeviceContainer } from './devices/deviceContainer' import { CasparCGDevice, DeviceOptionsCasparCGInternal } from './integrations/casparCG' import { SisyfosMessageDevice, DeviceOptionsSisyfosInternal } from './integrations/sisyfos' import { VMixDevice, DeviceOptionsVMixInternal } from './integrations/vmix' -import { VizMSEDevice, DeviceOptionsVizMSEInternal } from './integrations/vizMSE' import { BaseRemoteDeviceIntegration, RemoteDeviceInstance } from './service/remoteDeviceInstance' import type { ImplementedServiceDeviceTypes } from './service/devices' import { DeviceEvents } from './service/device' @@ -517,15 +517,6 @@ export class Conductor extends EventEmitter { getCurrentTime, threadedClassOptions ) - case DeviceType.VIZMSE: - return DeviceContainer.create( - '../../dist/integrations/vizMSE/index.js', - 'VizMSEDevice', - deviceId, - deviceOptions, - getCurrentTime, - threadedClassOptions - ) case DeviceType.VMIX: return DeviceContainer.create( '../../dist/integrations/vmix/index.js', @@ -552,7 +543,8 @@ export class Conductor extends EventEmitter { case DeviceType.TCPSEND: case DeviceType.TELEMETRICS: case DeviceType.TRICASTER: - case DeviceType.QUANTEL: { + case DeviceType.QUANTEL: + case DeviceType.VIZMSE: { ensureIsImplementedAsService(deviceOptions.type) // presumably this device is implemented in the new service handler @@ -1478,7 +1470,7 @@ export type DeviceOptionsAnyInternal = | DeviceOptionsSingularLive | DeviceOptionsVMixInternal | DeviceOptionsShotoku - | DeviceOptionsVizMSEInternal + | DeviceOptionsVizMSE | DeviceOptionsTelemetrics | DeviceOptionsTriCaster | DeviceOptionsMultiOSC diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts index 0b289e502..e04fb8d93 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/diffState.ts @@ -33,7 +33,7 @@ export function diffVizMSEStates( newState: VizMSEState, stateTime: number, currentTime: number, - options: VizMSEOptions | undefined, + options: VizMSEOptions, logger: DeviceContextAPI['logger'] ): Array { const highPrioCommands: VizMSECommand[] = [] @@ -260,7 +260,7 @@ export function diffVizMSEStates( const clearingCommands: VizMSECommand[] = [] - const templateName = options && options.clearAllTemplateName + const templateName = options.clearAllTemplateName if (!templateName) { logger.warning(`vizMSE: initOptions.clearAllTemplateName is not set!`) } else { @@ -275,12 +275,7 @@ export function diffVizMSEStates( }) ) } - if ( - newState.isClearAll.channelsToSendCommands && - options && - options.clearAllCommands && - options.clearAllCommands.length - ) { + if (newState.isClearAll.channelsToSendCommands && options.clearAllCommands && options.clearAllCommands.length) { // Send special commands to the engines: clearingCommands.push( literal({ diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts index 2f4121998..ae9272468 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts @@ -1,30 +1,28 @@ import * as _ from 'underscore' -import { CommandWithContext, DeviceStatus, DeviceWithState, StatusCode } from './../../devices/device' import { ActionExecutionResult, ActionExecutionResultCode, DeviceOptionsVizMSE, - DeviceType, Mappings, MediaObject, Timeline, TSRTimelineContent, VizMSEOptions, VizMSEActions, - VizMSEActionExecutionPayload, - VizMSEActionExecutionResult, VizResetPayload, + DeviceStatus, + StatusCode, } from 'timeline-state-resolver-types' import { createMSE, MSE } from '@tv2media/v-connection' import { DoOnTime, SendMode } from '../../devices/doOnTime' import { ExpectedPlayoutItem } from '../../expectedPlayoutItems' -import { actionNotFoundMessage, endTrace, startTrace, t } from '../../lib' +import { t } from '../../lib' import { HTTPClientError, HTTPServerError } from '@tv2media/v-connection/dist/msehttp' import { VizMSEManager } from './vizMSEManager' -import { VizMSECommand, VizMSEState, VizMSECommandType } from './types' +import { VizMSECommand, VizMSEState, VizMSECommandType, VizMSECommandWithContext } from './types' import { diffVizMSEStates } from './diffState' import { convertStateToVizMSE } from './convertState' -import type { DeviceContextAPI } from '../../service/device' +import { DeviceContextAPI, Device } from '../../service/device' export interface DeviceOptionsVizMSEInternal extends DeviceOptionsVizMSE { commandReceiver?: CommandReceiver @@ -35,40 +33,18 @@ export type CommandReceiver = (time: number, cmd: VizMSECommand, context: string * This class is used to interface with a vizRT Media Sequence Editor, through the v-connection library. * It features playing both "internal" graphics element and vizPilot elements. */ -export class VizMSEDevice extends DeviceWithState { +export class VizMSEDevice extends Device { private _vizMSE?: MSE private _vizmseManager?: VizMSEManager - private _commandReceiver: CommandReceiver = this._defaultCommandReceiver.bind(this) - - private _doOnTime: DoOnTime private _doOnTimeBurst: DoOnTime private _initOptions?: VizMSEOptions private _vizMSEConnected = false - constructor(deviceId: string, deviceOptions: DeviceOptionsVizMSEInternal, getCurrentTime: () => Promise) { - super(deviceId, deviceOptions, getCurrentTime) - - if (deviceOptions.options) { - if (deviceOptions.commandReceiver) this._commandReceiver = deviceOptions.commandReceiver - } + constructor(context: DeviceContextAPI) { + super(context) - this._doOnTime = new DoOnTime( - () => { - return this.getCurrentTime() - }, - SendMode.IN_ORDER, - this._deviceOptions - ) - this.handleDoOnTime(this._doOnTime, 'VizMSE') - - this._doOnTimeBurst = new DoOnTime( - () => { - return this.getCurrentTime() - }, - SendMode.BURST, - this._deviceOptions - ) + this._doOnTimeBurst = new DoOnTime(() => this.context.getCurrentTime(), SendMode.BURST, this._deviceOptions) this.handleDoOnTime(this._doOnTimeBurst, 'VizMSE.burst') } @@ -94,14 +70,16 @@ export class VizMSEDevice extends DeviceWithState this.connectionChanged(connected)) this._vizmseManager.on('updateMediaObject', (docId: string, doc: MediaObject | null) => - this.emit('updateMediaObject', this.deviceId, docId, doc) + this.context.updateMediaObject(docId, doc) ) - this._vizmseManager.on('clearMediaObjects', () => this.emit('clearMediaObjects', this.deviceId)) + this._vizmseManager.on('clearMediaObjects', () => this.context.clearMediaObjects()) - this._vizmseManager.on('info', (str) => this.emit('info', 'VizMSE: ' + str)) - this._vizmseManager.on('warning', (str) => this.emit('warning', 'VizMSE: ' + str)) - this._vizmseManager.on('error', (e) => this.emit('error', 'VizMSE', typeof e === 'string' ? new Error(e) : e)) - this._vizmseManager.on('debug', (...args) => this.emitDebug(...args)) + this._vizmseManager.on('info', (str) => this.context.logger.info('VizMSE: ' + str)) + this._vizmseManager.on('warning', (str) => this.context.logger.warning('VizMSE: ' + str)) + this._vizmseManager.on('error', (e) => + this.context.logger.error('VizMSE', typeof e === 'string' ? new Error(e) : e) + ) + this._vizmseManager.on('debug', (...args) => this.context.logger.debug(...args)) await this._vizmseManager.initializeRundown(activeRundownPlaylistId) @@ -117,58 +95,9 @@ export class VizMSEDevice extends DeviceWithState, newMappings: Mappings) { - super.onHandleState(newState, newMappings) - // check if initialized: - if (!this._vizmseManager || !this._vizmseManager.initialized) { - this.emit('warning', 'VizMSE.v-connection not initialized yet') - return - } - - const previousStateTime = Math.max(this.getCurrentTime(), newState.time) - const oldVizMSEState: VizMSEState = (this.getStateBefore(previousStateTime) || { state: { time: 0, layer: {} } }) - .state - - const convertTrace = startTrace(`device:convertState`, { deviceId: this.deviceId }) - const newVizMSEState = this.convertStateToVizMSE(newState, newMappings) - this.emit('timeTrace', endTrace(convertTrace)) - - const diffTrace = startTrace(`device:diffState`, { deviceId: this.deviceId }) - const commandsToAchieveState = this._diffStates(oldVizMSEState, newVizMSEState, newState.time) - this.emit('timeTrace', endTrace(diffTrace)) - - // clear any queued commands later than this time: - this._doOnTime.clearQueueNowAndAfter(previousStateTime) - - // add the new commands to the queue - this._addToQueue(commandsToAchieveState) - - // store the new state, for later use: - this.setState(newVizMSEState, newState.time) - } - - /** - * Clear any scheduled commands after this time - * @param clearAfterTime - */ - clearFuture(clearAfterTime: number) { - this._doOnTime.clearQueueAfter(clearAfterTime) - } - get canConnect(): boolean { - return true - } get connected(): boolean { return this._vizMSEConnected } @@ -198,13 +127,13 @@ export class VizMSEDevice extends DeviceWithState { await this._vizmseManager?.clearEngines({ type: VizMSECommandType.CLEAR_ALL_ENGINES, - time: this.getCurrentTime(), + time: this.context.getCurrentTime(), timelineObjId: 'clearAllEnginesAction', channels: 'all', commands: this._initOptions?.clearAllCommands || [], @@ -234,89 +163,61 @@ export class VizMSEDevice extends DeviceWithState( - actionId: A, - payload: VizMSEActionExecutionPayload - ): Promise> { - switch (actionId) { - case VizMSEActions.PurgeRundown: - await this.purgeRundown(true) - return { result: ActionExecutionResultCode.Ok } - case VizMSEActions.Activate: - return this.activate(payload) as Promise> - case VizMSEActions.StandDown: - return this.executeStandDown() as Promise> - case VizMSEActions.ClearAllEngines: - await this.clearEngines() - return { result: ActionExecutionResultCode.Ok } - case VizMSEActions.VizReset: - await this.resetViz(payload ?? {}) - return { result: ActionExecutionResultCode.Ok } - default: - return actionNotFoundMessage(actionId) - } + await this.context.resetState() } - get deviceType() { - return DeviceType.VIZMSE - } - get deviceName(): string { - return `VizMSE ${this._vizMSE ? this._vizMSE.hostname : 'Uninitialized'}` - } - - get queue() { - return this._doOnTime.getQueue() + readonly actions: Record< + VizMSEActions, + (id: string, payload?: Record) => Promise + > = { + [VizMSEActions.PurgeRundown]: async (_id: string, _payload?: Record) => { + await this.purgeRundown(true) + return { result: ActionExecutionResultCode.Ok } + }, + [VizMSEActions.Activate]: async (_id: string, payload?: Record) => { + return this.activate(payload) + }, + [VizMSEActions.StandDown]: async (_id: string, _payload?: Record) => { + return this.executeStandDown() + }, + [VizMSEActions.ClearAllEngines]: async (_id: string, _payload?: Record) => { + await this.clearEngines() + return { result: ActionExecutionResultCode.Ok } + }, + [VizMSEActions.VizReset]: async (_id: string, payload?: Record) => { + await this.resetViz(payload ?? {}) + return { result: ActionExecutionResultCode.Ok } + }, } get supportsExpectedPlayoutItems(): boolean { return true } public handleExpectedPlayoutItems(expectedPlayoutItems: Array): void { - this.emitDebug('VIZDEBUG: handleExpectedPlayoutItems called') + this.context.logger.debug('VIZDEBUG: handleExpectedPlayoutItems called') if (this._vizmseManager) { - this.emitDebug('VIZDEBUG: manager exists') + this.context.logger.debug('VIZDEBUG: manager exists') this._vizmseManager.setExpectedPlayoutItems(expectedPlayoutItems) } } - public getCurrentState(): VizMSEState | undefined { - return (this.getState() || { state: undefined }).state - } public connectionChanged(connected?: boolean) { if (connected === true || connected === false) this._vizMSEConnected = connected if (connected === false) { - this.emit('clearMediaObjects', this.deviceId) - } - this.emit('connectionChanged', this.getStatus()) - } - - private _createFakeLogger(): DeviceContextAPI['logger'] { - return { - debug: (...args) => { - this.emit('debug', ...args) - }, - info: (...args) => { - this.emit('info', ...args) - }, - warning: (...args) => { - this.emit('warning', ...args) - }, - error: (...args) => { - this.emit('error', ...args) - }, + this.context.clearMediaObjects() } + this.context.connectionChanged(this.getStatus()) } /** * Takes a timeline state and returns a VizMSE State that will work with the state lib. * @param timelineState The timeline state to generate from. */ - convertStateToVizMSE(timelineState: Timeline.TimelineState, mappings: Mappings): VizMSEState { - return convertStateToVizMSE(this._createFakeLogger(), this._vizmseManager, timelineState, mappings) + convertTimelineStateToDeviceState( + timelineState: Timeline.TimelineState, + mappings: Mappings + ): VizMSEState { + return convertStateToVizMSE(this.context.logger, this._vizmseManager, timelineState, mappings) } /** @@ -332,7 +233,7 @@ export class VizMSEDevice extends DeviceWithState undefined) } } - getStatus(): DeviceStatus { + getStatus(): Omit { let statusCode = StatusCode.GOOD const messages: Array = [] @@ -400,26 +301,33 @@ export class VizMSEDevice extends DeviceWithState { + public diffStates( + oldState: VizMSEState | undefined, + newState: VizMSEState, + _mappings: Mappings, + time: number + ): Array { + if (!this._initOptions) throw new Error('VizMSE not initialized yet') + return diffVizMSEStates( oldState, newState, time, - this.getCurrentTime(), + this.context.getCurrentTime(), this._initOptions, - this._createFakeLogger() - ) - } - private async _doCommand(command: VizMSECommand, context: string, timlineObjId: string): Promise { - const time = this.getCurrentTime() - return this._commandReceiver(time, command, context, timlineObjId) + this.context.logger + ).map((cmd) => ({ + command: cmd, + timelineObjId: cmd.timelineObjId, + context: '', // TODO + })) } + /** * Add commands to queue, to be executed at the right time */ @@ -429,7 +337,12 @@ export class VizMSEDevice extends DeviceWithState { - return this._doCommand(c.cmd, c.cmd.type + '_' + c.cmd.timelineObjId, c.cmd.timelineObjId) + return this._commandReceiver( + this.getCurrentTime(), + c.cmd, + c.cmd.type + '_' + c.cmd.timelineObjId, + c.cmd.timelineObjId + ) }, { cmd: cmd } ) @@ -454,59 +367,49 @@ export class VizMSEDevice extends DeviceWithState { - const cwc: CommandWithContext = { - context: context, - timelineObjId: timelineObjId, - command: cmd, - } - this.emitDebug(cwc) + public async sendCommand(cmd: VizMSECommandWithContext): Promise { + this.context.logger.debug(cmd) try { if (!this._vizmseManager) { throw new Error(`Not initialized yet`) } - switch (cmd.type) { + switch (cmd.command.type) { case VizMSECommandType.PREPARE_ELEMENT: - await this._vizmseManager.prepareElement(cmd) + await this._vizmseManager.prepareElement(cmd.command) break case VizMSECommandType.CUE_ELEMENT: - await this._vizmseManager.cueElement(cmd) + await this._vizmseManager.cueElement(cmd.command) break case VizMSECommandType.TAKE_ELEMENT: - await this._vizmseManager.takeElement(cmd) + await this._vizmseManager.takeElement(cmd.command) break case VizMSECommandType.TAKEOUT_ELEMENT: - await this._vizmseManager.takeoutElement(cmd) + await this._vizmseManager.takeoutElement(cmd.command) break case VizMSECommandType.CONTINUE_ELEMENT: - await this._vizmseManager.continueElement(cmd) + await this._vizmseManager.continueElement(cmd.command) break case VizMSECommandType.CONTINUE_ELEMENT_REVERSE: - await this._vizmseManager.continueElementReverse(cmd) + await this._vizmseManager.continueElementReverse(cmd.command) break case VizMSECommandType.LOAD_ALL_ELEMENTS: - await this._vizmseManager.loadAllElements(cmd) + await this._vizmseManager.loadAllElements(cmd.command) break case VizMSECommandType.CLEAR_ALL_ELEMENTS: - await this._vizmseManager.clearAll(cmd) + await this._vizmseManager.clearAll(cmd.command) break case VizMSECommandType.CLEAR_ALL_ENGINES: - await this._vizmseManager.clearEngines(cmd) + await this._vizmseManager.clearEngines(cmd.command) break case VizMSECommandType.SET_CONCEPT: - await this._vizmseManager.setConcept(cmd) + await this._vizmseManager.setConcept(cmd.command) break case VizMSECommandType.INITIALIZE_SHOWS: - await this._vizmseManager.initializeShows(cmd) + await this._vizmseManager.initializeShows(cmd.command) break case VizMSECommandType.CLEANUP_SHOWS: - await this._vizmseManager.cleanupShows(cmd) + await this._vizmseManager.cleanupShows(cmd.command) break default: // @ts-ignore never @@ -526,7 +429,7 @@ export class VizMSEDevice extends DeviceWithState { commandError: (error: Error, context: CommandWithContext) => { this.emit('commandError', error, context) }, - updateMediaObject: (collectionId: string, docId: string, doc: MediaObject | null) => { - this.emit('updateMediaObject', collectionId, docId, doc) + updateMediaObject: (docId: string, doc: MediaObject | null) => { + this.emit('updateMediaObject', this._deviceId, docId, doc) }, - clearMediaObjects: (collectionId: string) => { - this.emit('clearMediaObjects', collectionId) + clearMediaObjects: () => { + this.emit('clearMediaObjects', this._deviceId) }, timeTrace: (trace: FinishedTrace) => { diff --git a/packages/timeline-state-resolver/src/service/device.ts b/packages/timeline-state-resolver/src/service/device.ts index 50fe277f4..1ea73a5d0 100644 --- a/packages/timeline-state-resolver/src/service/device.ts +++ b/packages/timeline-state-resolver/src/service/device.ts @@ -126,7 +126,7 @@ export interface DeviceEvents { commandError: [error: Error, context: CommandWithContext] /** Update a MediaObject */ updateMediaObject: [collectionId: string, docId: string, doc: MediaObject | null] - /** Clear a MediaObjects collection */ + /** Clear MediaObjects from the device */ clearMediaObjects: [collectionId: string] timeTrace: [trace: FinishedTrace] @@ -162,9 +162,9 @@ export interface DeviceContextAPI { /** Something went wrong when executing a command */ commandError: (error: Error, context: CommandWithContext) => void /** Update a MediaObject */ - updateMediaObject: (collectionId: string, docId: string, doc: MediaObject | null) => void + updateMediaObject: (docId: string, doc: MediaObject | null) => void /** Clear a MediaObjects collection */ - clearMediaObjects: (collectionId: string) => void + clearMediaObjects: () => void timeTrace: (trace: FinishedTrace) => void diff --git a/packages/timeline-state-resolver/src/service/devices.ts b/packages/timeline-state-resolver/src/service/devices.ts index ac5eecff5..10b2e8b3f 100644 --- a/packages/timeline-state-resolver/src/service/devices.ts +++ b/packages/timeline-state-resolver/src/service/devices.ts @@ -18,6 +18,7 @@ import { TelemetricsDevice } from '../integrations/telemetrics' import { TriCasterDevice } from '../integrations/tricaster' import { SingularLiveDevice } from '../integrations/singularLive' import { MultiOSCMessageDevice } from '../integrations/multiOsc' +import { VizMSEDevice } from '../integrations/vizMSE' export interface DeviceEntry { deviceClass: new (context: DeviceContextAPI) => Device @@ -45,6 +46,7 @@ export type ImplementedServiceDeviceTypes = | DeviceType.TELEMETRICS | DeviceType.TRICASTER | DeviceType.QUANTEL + | DeviceType.VIZMSE // TODO - move all device implementations here and remove the old Device classes export const DevicesDict: Record = { @@ -156,4 +158,10 @@ export const DevicesDict: Record = { deviceName: (deviceId: string) => 'Quantel' + deviceId, executionMode: () => 'sequential', }, + [DeviceType.VIZMSE]: { + deviceClass: VizMSEDevice, + canConnect: true, + deviceName: (deviceId: string) => 'VizMSE' + deviceId, + executionMode: () => 'sequential', + }, } From 6379bda494bc72aefbf987ec37a304ed087cc4e0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 30 Aug 2024 16:11:25 +0100 Subject: [PATCH 3/6] wip: reimplement queue --- .../src/integrations/vizMSE/index.ts | 66 ++++++++----------- .../src/service/devices.ts | 2 +- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts index ae9272468..73328f639 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts @@ -14,7 +14,6 @@ import { StatusCode, } from 'timeline-state-resolver-types' import { createMSE, MSE } from '@tv2media/v-connection' -import { DoOnTime, SendMode } from '../../devices/doOnTime' import { ExpectedPlayoutItem } from '../../expectedPlayoutItems' import { t } from '../../lib' import { HTTPClientError, HTTPServerError } from '@tv2media/v-connection/dist/msehttp' @@ -23,6 +22,7 @@ import { VizMSECommand, VizMSEState, VizMSECommandType, VizMSECommandWithContext import { diffVizMSEStates } from './diffState' import { convertStateToVizMSE } from './convertState' import { DeviceContextAPI, Device } from '../../service/device' +import PQueue from 'p-queue' export interface DeviceOptionsVizMSEInternal extends DeviceOptionsVizMSE { commandReceiver?: CommandReceiver @@ -37,15 +37,12 @@ export class VizMSEDevice extends Device() constructor(context: DeviceContextAPI) { super(context) - - this._doOnTimeBurst = new DoOnTime(() => this.context.getCurrentTime(), SendMode.BURST, this._deviceOptions) - this.handleDoOnTime(this._doOnTimeBurst, 'VizMSE.burst') } async init(initOptions: VizMSEOptions, activeRundownPlaylistId?: string): Promise { @@ -95,7 +92,11 @@ export class VizMSEDevice extends Device) { - _.each(commandsToAchieveState, (cmd: VizMSECommand) => { - this._doOnTime.queue( - cmd.time, - cmd.layerId, - async (c: { cmd: VizMSECommand }) => { - return this._commandReceiver( - this.getCurrentTime(), - c.cmd, - c.cmd.type + '_' + c.cmd.timelineObjId, - c.cmd.timelineObjId - ) - }, - { cmd: cmd } - ) - - this._doOnTimeBurst.queue( - cmd.time, - undefined, - async (c: { cmd: VizMSECommand }) => { - if (c.cmd.type === VizMSECommandType.TAKE_ELEMENT && !c.cmd.fromLookahead) { - if (this._vizmseManager && c.cmd.layerId) { - this._vizmseManager.clearAllWaitWithLayer(c.cmd.layerId) - } - } - return Promise.resolve() - }, - { cmd: cmd } - ) - }) + public async sendCommand(c: VizMSECommandWithContext): Promise { + // Ensure command isn't blocked by a previous wait/delay + if (c.command.type === VizMSECommandType.TAKE_ELEMENT && !c.command.fromLookahead) { + if (this._vizmseManager && c.command.layerId) { + this._vizmseManager.clearAllWaitWithLayer(c.command.layerId) + } + } + + // Get or create the queue for the layer + let queue = this._commandQueues.get(c.command.layerId) + if (!queue) { + queue = new PQueue({ concurrency: 1 }) + this._commandQueues.set(c.command.layerId, queue) + } + + // Queue the commands in a sequential queue + return queue.add(async () => this.sendCommandInner(c)) } /** * Sends commands to the VizMSE server * @param time deprecated * @param cmd Command to execute */ - public async sendCommand(cmd: VizMSECommandWithContext): Promise { + private async sendCommandInner(cmd: VizMSECommandWithContext): Promise { this.context.logger.debug(cmd) try { diff --git a/packages/timeline-state-resolver/src/service/devices.ts b/packages/timeline-state-resolver/src/service/devices.ts index 10b2e8b3f..6ea8fbec0 100644 --- a/packages/timeline-state-resolver/src/service/devices.ts +++ b/packages/timeline-state-resolver/src/service/devices.ts @@ -162,6 +162,6 @@ export const DevicesDict: Record = { deviceClass: VizMSEDevice, canConnect: true, deviceName: (deviceId: string) => 'VizMSE' + deviceId, - executionMode: () => 'sequential', + executionMode: () => 'salvo', // Uses a queue internally to perform some sequential operations }, } From ae4c5c6034848b2c6668c55171405ac91437fbdf Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Sep 2024 12:37:21 +0100 Subject: [PATCH 4/6] wip --- .../src/integrations/vizMSE/index.ts | 30 ++++++------------- .../src/integrations/vizMSE/types.ts | 7 ++--- .../src/service/device.ts | 6 ++-- .../src/service/devices.ts | 2 +- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts index 73328f639..7cf24bf9a 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/index.ts @@ -1,4 +1,3 @@ -import * as _ from 'underscore' import { ActionExecutionResult, ActionExecutionResultCode, @@ -22,7 +21,6 @@ import { VizMSECommand, VizMSEState, VizMSECommandType, VizMSECommandWithContext import { diffVizMSEStates } from './diffState' import { convertStateToVizMSE } from './convertState' import { DeviceContextAPI, Device } from '../../service/device' -import PQueue from 'p-queue' export interface DeviceOptionsVizMSEInternal extends DeviceOptionsVizMSE { commandReceiver?: CommandReceiver @@ -39,7 +37,6 @@ export class VizMSEDevice extends Device() constructor(context: DeviceContextAPI) { super(context) @@ -92,11 +89,6 @@ export class VizMSEDevice extends Device ({ - command: cmd, - timelineObjId: cmd.timelineObjId, - context: '', // TODO - })) + ).map( + (cmd): VizMSECommandWithContext => ({ + command: cmd, + timelineObjId: cmd.timelineObjId, + queueId: cmd.layerId, + context: '', // TODO + }) + ) } /** @@ -342,15 +337,8 @@ export class VizMSEDevice extends Device this.sendCommandInner(c)) + return this.sendCommandInner(c) } /** * Sends commands to the VizMSE server diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/types.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/types.ts index fd4fe764f..5b393cb78 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/types.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/types.ts @@ -6,11 +6,10 @@ import { VIZMSEPlayoutItemContentInternal, } from 'timeline-state-resolver-types' import { VElement } from '@tv2media/v-connection' +import type { CommandWithContext } from '../../service/device' -export interface VizMSECommandWithContext { - command: VizMSECommand - context: string - timelineObjId: string +export interface VizMSECommandWithContext extends CommandWithContext { + queueId: string | undefined } export interface VizMSEState { diff --git a/packages/timeline-state-resolver/src/service/device.ts b/packages/timeline-state-resolver/src/service/device.ts index 1ea73a5d0..d2b9aa32a 100644 --- a/packages/timeline-state-resolver/src/service/device.ts +++ b/packages/timeline-state-resolver/src/service/device.ts @@ -11,9 +11,9 @@ import { type CommandContext = any -export type CommandWithContext = { - command: any - context: CommandContext +export type CommandWithContext = { + command: TCommand + context: TContext /** ID of the timeline-object that the command originated from */ timelineObjId: string /** this command is to be executed x ms _before_ the scheduled time */ diff --git a/packages/timeline-state-resolver/src/service/devices.ts b/packages/timeline-state-resolver/src/service/devices.ts index 6ea8fbec0..10b2e8b3f 100644 --- a/packages/timeline-state-resolver/src/service/devices.ts +++ b/packages/timeline-state-resolver/src/service/devices.ts @@ -162,6 +162,6 @@ export const DevicesDict: Record = { deviceClass: VizMSEDevice, canConnect: true, deviceName: (deviceId: string) => 'VizMSE' + deviceId, - executionMode: () => 'salvo', // Uses a queue internally to perform some sequential operations + executionMode: () => 'sequential', }, } From fa221656ceb8ce4a9869acf715b084c149c4f013 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Sep 2024 12:44:14 +0100 Subject: [PATCH 5/6] wip: propogate handleExpectedPlayoutItems --- .../timeline-state-resolver/src/service/DeviceInstance.ts | 4 ++-- packages/timeline-state-resolver/src/service/device.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/timeline-state-resolver/src/service/DeviceInstance.ts b/packages/timeline-state-resolver/src/service/DeviceInstance.ts index fd91db513..c1ae69c5f 100644 --- a/packages/timeline-state-resolver/src/service/DeviceInstance.ts +++ b/packages/timeline-state-resolver/src/service/DeviceInstance.ts @@ -185,8 +185,8 @@ export class DeviceInstanceWrapper extends EventEmitter { } } - handleExpectedPlayoutItems(_expectedPlayoutItems: Array): void { - // do nothing yet, as this isn't implemented. + handleExpectedPlayoutItems(expectedPlayoutItems: Array): void { + this._device.handleExpectedPlayoutItems(expectedPlayoutItems) } getStatus(): DeviceStatus { diff --git a/packages/timeline-state-resolver/src/service/device.ts b/packages/timeline-state-resolver/src/service/device.ts index d2b9aa32a..882e097da 100644 --- a/packages/timeline-state-resolver/src/service/device.ts +++ b/packages/timeline-state-resolver/src/service/device.ts @@ -1,4 +1,4 @@ -import { SlowSentCommandInfo, SlowFulfilledCommandInfo, CommandReport } from '../' +import { SlowSentCommandInfo, SlowFulfilledCommandInfo, CommandReport, ExpectedPlayoutItem } from '../' import { FinishedTrace } from '../lib' import { Timeline, @@ -55,7 +55,10 @@ export abstract class Device) => Promise> - // todo - add media objects + public handleExpectedPlayoutItems(_expectedPlayoutItems: Array): void { + // When receiving a new list of playoutItems. + // Do nothing by default + } // From BaseDeviceAPI: ----------------------------------------------- abstract convertTimelineStateToDeviceState( From e1619661a6ece1e774ecb53757fc81a016286b1f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Sep 2024 13:55:07 +0100 Subject: [PATCH 6/6] wip: couple of tests --- .../timeline-state-resolver/jest.config.js | 2 +- .../src/__mocks__/v-connection.ts | 7 +- .../vizMSE/__tests__/vizMSE.spec.ts | 1965 +++++++++-------- 3 files changed, 1001 insertions(+), 973 deletions(-) diff --git a/packages/timeline-state-resolver/jest.config.js b/packages/timeline-state-resolver/jest.config.js index 7d70639a0..af118e56f 100644 --- a/packages/timeline-state-resolver/jest.config.js +++ b/packages/timeline-state-resolver/jest.config.js @@ -5,7 +5,7 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', - diagnostics: { ignoreCodes: [6133, 6192] }, + diagnostics: { ignoreCodes: [6133, 6192, 6196] }, }, ], }, diff --git a/packages/timeline-state-resolver/src/__mocks__/v-connection.ts b/packages/timeline-state-resolver/src/__mocks__/v-connection.ts index df964b6f3..35a6f13b2 100644 --- a/packages/timeline-state-resolver/src/__mocks__/v-connection.ts +++ b/packages/timeline-state-resolver/src/__mocks__/v-connection.ts @@ -51,10 +51,15 @@ export const createMSE: typeof orgCreateMSE = function createMSE0( return mse } -export function getMockMSEs() { +export function getMockMSEs(): MSEMock[] { return mockMSEs } +export function getLastMockMSE(): MSEMock { + if (mockMSEs.length === 0) throw new Error('No MSE found') + return mockMSEs[mockMSEs.length - 1] +} + export class MSEMock extends EventEmitter implements MSE { public readonly hostname: string public readonly restPort: number diff --git a/packages/timeline-state-resolver/src/integrations/vizMSE/__tests__/vizMSE.spec.ts b/packages/timeline-state-resolver/src/integrations/vizMSE/__tests__/vizMSE.spec.ts index 19c095974..02b192efa 100644 --- a/packages/timeline-state-resolver/src/integrations/vizMSE/__tests__/vizMSE.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/vizMSE/__tests__/vizMSE.spec.ts @@ -25,21 +25,33 @@ import _ = require('underscore') import { StatusCode } from '../../../devices/device' import { MOCK_SHOWS } from '../../../__mocks__/v-connection' import { literal } from '../../../lib' +import { getDeviceContext } from '../../__tests__/testlib' +import { VizMSECommandType, VizMSEState } from '../types' +import { ExpectedPlayoutItemContentBase } from '../../../expectedPlayoutItems' + +jest.mock('@tv2media/v-connection') +jest.mock('net') + +// Some 'hacks' to get the mocks correctly registered +import * as vConnectionLib from '@tv2media/v-connection' +jest.spyOn(vConnectionLib, 'createMSE').mockImplementation(vConnection.createMSE) +import * as netLib from 'net' +// @ts-ignore +netLib.Socket = Socket const orgSetTimeout = setTimeout +type VIZMSEPlayoutItemContentInternalFull = VIZMSEPlayoutItemContentInternal & ExpectedPlayoutItemContentBase + async function wait(time = 1) { return new Promise((resolve) => { orgSetTimeout(resolve, time) }) } -async function setupDevice() { - let device: any = undefined - const commandReceiver0 = jest.fn((...args) => { - return device._defaultCommandReceiver(...args) - }) +const defaultPlaylistId = 'my-super-playlist-id' +async function setupDevice() { const myChannelMapping0: Mapping = { device: DeviceType.VIZMSE, deviceId: 'myViz', @@ -55,948 +67,977 @@ async function setupDevice() { viz_continue: myChannelMapping1, } - const myConductor = new Conductor({ - multiThreadedResolver: false, - getCurrentTime: mockTime.getCurrentTime, + // const myConductor = new Conductor({ + // multiThreadedResolver: false, + // getCurrentTime: mockTime.getCurrentTime, + // }) + // const onError = jest.fn() + // myConductor.on('error', onError) + + // myConductor.setTimelineAndMappings([], myChannelMapping) + // await myConductor.init() + // await myConductor.addDevice('myViz', { + // type: DeviceType.VIZMSE, + // options: { + // host: '127.0.0.1', + // preloadAllElements: true, + // playlistID: 'my-super-playlist-id', + // profile: 'profile9999', + // showDirectoryPath: 'SOFIE', + // }, + // commandReceiver: commandReceiver0, + // }) + + const device = new VizMSEDevice(getDeviceContext()) + await device.init({ + host: '127.0.0.1', + preloadAllElements: true, + playlistID: defaultPlaylistId, + profile: 'profile9999', + showDirectoryPath: 'SOFIE', }) - const onError = jest.fn() - myConductor.on('error', onError) - - myConductor.setTimelineAndMappings([], myChannelMapping) - await myConductor.init() - await myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: { - host: '127.0.0.1', - preloadAllElements: true, - playlistID: 'my-super-playlist-id', - profile: 'profile9999', - showDirectoryPath: 'SOFIE', - }, - commandReceiver: commandReceiver0, - }) - const deviceContainer = myConductor.getDevice('myViz') - device = deviceContainer!.device as ThreadedClass - return { device, myConductor, onError, commandReceiver0 } + return { device, myChannelMapping } } const mockTime = new MockTime() describe('vizMSE', () => { - jest.mock('@tv2media/v-connection', () => vConnection) - jest.mock('net', () => net) - // const orgSetTimeout = setTimeout beforeEach(() => { mockTime.init() }) - test('Internal element', async () => { - const { device, myConductor, commandReceiver0 } = await setupDevice() - await mockTime.advanceTimeToTicks(10100) - - await device.ignoreWaitsInTests() - - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) - - myConductor.setTimelineAndMappings([ - { - id: 'obj0', - enable: { - start: mockTime.now + 5000, // 15100 - duration: 5000, // 20100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, - // continueStep?: number - // channelName?: string - // cue?: boolean - // noAutoPreloading?: boolean - templateName: 'myInternalElement', - templateData: ['line1', 'line2'], - showName: MOCK_SHOWS[0].name, - }, - }, - { - id: 'obj1', - enable: { - start: mockTime.now + 7000, // 17100 - duration: 5000, // 22100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, - templateName: 'myInternalElement2', - templateData: ['line1'], - showName: MOCK_SHOWS[0].name, - }, - }, - { - id: 'obj2', - enable: { - start: mockTime.now + 9000, // 19100 - duration: 500, - }, - layer: 'viz_continue', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.CONTINUE, - reference: 'viz0', - }, - }, - ]) - - await mockTime.advanceTimeTicks(500) // 10500 - expect(commandReceiver0.mock.calls.length).toEqual(0) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(14500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - // fromLookahead?: boolean - // layerId?: string - time: 14100, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: ['line1', 'line2'], - showId: MOCK_SHOWS[0].id, - }, - type: 'prepare', - // channelName?: string - // noAutoPreloading?: boolean - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(15500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 15100, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: ['line1', 'line2'], - showId: MOCK_SHOWS[0].id, - }, - type: 'take', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(16500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 16100, - content: { - instanceName: expect.stringContaining('myInternalElement2'), - templateName: 'myInternalElement2', - templateData: ['line1'], - showId: MOCK_SHOWS[0].id, - }, - type: 'prepare', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(17500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 17100, - content: { - instanceName: expect.stringContaining('myInternalElement2'), - templateName: 'myInternalElement2', - templateData: ['line1'], - showId: MOCK_SHOWS[0].id, - }, - type: 'take', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(19500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj2', - time: 19100, - content: { - instanceName: expect.stringContaining('myInternalElement2'), - templateName: 'myInternalElement2', - templateData: ['line1'], - showId: MOCK_SHOWS[0].id, - }, - type: 'continue', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(22500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 22100, - content: { - instanceName: expect.stringContaining('myInternalElement2'), - templateName: 'myInternalElement2', - templateData: ['line1'], - showId: MOCK_SHOWS[0].id, - }, - type: 'out', - }) - }) - test('External/Pilot element', async () => { - const { device, myConductor, onError, commandReceiver0 } = await setupDevice() - await mockTime.advanceTimeToTicks(10100) - - await device.ignoreWaitsInTests() - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) - - myConductor.setTimelineAndMappings([ - { - id: 'obj0', - enable: { - start: mockTime.now + 5000, // 15100 - duration: 5000, // 20100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.ELEMENT_PILOT, - // continueStep?: number - // cue?: boolean - // noAutoPreloading?: boolean - channelName: 'FULL1', - templateVcpId: 1337, - }, - }, - { - id: 'obj1', - enable: { - start: mockTime.now + 7000, // 17100 - duration: 5000, // 22100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.ELEMENT_PILOT, - channelName: 'FULL1', - templateVcpId: 1338, - }, - }, - { - id: 'obj2', - enable: { - start: mockTime.now + 9000, // 19100 - duration: 500, - }, - layer: 'viz_continue', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.CONTINUE, - reference: 'viz0', - }, - }, - ]) - - await mockTime.advanceTimeTicks(500) // 10500 - expect(commandReceiver0.mock.calls.length).toEqual(0) - - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() - expect(mse.getMockRundowns()).toHaveLength(1) - const rundown = _.last(mse.getMockRundowns()) as VRundownMocked - expect(rundown).toBeTruthy() - - expect(await device.supportsExpectedPlayoutItems).toEqual(true) - const expectedItem1: VIZMSEPlayoutItemContentExternal = { - vcpid: 1337, - channel: 'FULL1', - } - const expectedItem2: VIZMSEPlayoutItemContentExternal = { - vcpid: 1336, - channel: 'FULL1', - } - await device.handleExpectedPlayoutItems([expectedItem1, expectedItem2]) - await mockTime.advanceTimeTicks(100) - - expect(rundown.createElement).toHaveBeenCalledTimes(2) - expect(rundown.createElement).toHaveBeenNthCalledWith(1, expectedItem1) - expect(rundown.createElement).toHaveBeenNthCalledWith(2, expectedItem2) - rundown.createElement.mockClear() - - await myConductor.devicesMakeReady(true) - await mockTime.advanceTimeTicks(10) - - expect(rundown.activate).toHaveBeenCalledTimes(2) - - expect(rundown.getElement).toHaveBeenCalledTimes(4) - expect(rundown.getElement).toHaveBeenNthCalledWith(1, expectedItem1) - expect(rundown.getElement).toHaveBeenNthCalledWith(2, expectedItem2) - - expect(rundown.createElement).toHaveBeenCalledTimes(2) - expect(rundown.createElement).toHaveBeenNthCalledWith(1, expectedItem1) - expect(rundown.createElement).toHaveBeenNthCalledWith(2, expectedItem2) - - expect(rundown.initialize).toHaveBeenCalledTimes(2) - expect(rundown.initialize).toHaveBeenNthCalledWith(1, expectedItem1) - expect(rundown.initialize).toHaveBeenNthCalledWith(2, expectedItem2) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(14500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - // fromLookahead?: boolean - // layerId?: string - time: 14100, - content: { - vcpid: 1337, - channel: 'FULL1', - }, - type: 'prepare', - // noAutoPreloading?: boolean - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(15500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 15100, - content: { - vcpid: 1337, - channel: 'FULL1', - }, - type: 'take', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(16500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 16100, - content: { - vcpid: 1338, - channel: 'FULL1', - }, - type: 'prepare', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(17500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 17100, - content: { - vcpid: 1338, - channel: 'FULL1', - }, - type: 'take', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(19500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj2', - time: 19100, - content: { - vcpid: 1338, - channel: 'FULL1', - }, - type: 'continue', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(22500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 22100, - content: { - vcpid: 1338, - channel: 'FULL1', - }, - type: 'out', - }) - - // manually load elements: - commandReceiver0.mockClear() - rundown.getElement.mockClear() - rundown.createElement.mockClear() - rundown.initialize.mockClear() - - const expectedItem3: VIZMSEPlayoutItemContentExternal = { - vcpid: 9999, - channel: 'FULL1', - } - - await device.handleExpectedPlayoutItems([expectedItem1, expectedItem2, expectedItem3]) - - myConductor.setTimelineAndMappings([ - { - id: 'loadAll', - enable: { - start: 25000, - duration: 500, // 25500 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS, - }, - }, - ]) - - await mockTime.advanceTimeToTicks(24900) - - expect(rundown.createElement).toHaveBeenCalledTimes(1) - expect(rundown.createElement).toHaveBeenNthCalledWith(1, expectedItem3) - expect(rundown.initialize).toHaveBeenCalledTimes(0) - - rundown.getElement.mockClear() - rundown.createElement.mockClear() - rundown.initialize.mockClear() - - await mockTime.advanceTimeToTicks(25500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'loadAll', - time: 25000, - type: 'load_all_elements', - }) - - expect(rundown.initialize).toHaveBeenCalledTimes(1) - expect(rundown.initialize).toHaveBeenNthCalledWith(1, expectedItem3) - - expect(rundown.deactivate).toHaveBeenCalledTimes(0) - await myConductor.devicesStandDown(true) - expect(rundown.deactivate).toHaveBeenCalledTimes(1) - - expect(onError).toHaveBeenCalledTimes(0) - }) - test('bad init options & basic functionality', async () => { - const myConductor = new Conductor({ - multiThreadedResolver: false, - getCurrentTime: mockTime.getCurrentTime, - }) - const onError = jest.fn() - myConductor.on('error', onError) - const onWarning = jest.fn() - myConductor.on('warning', onWarning) - - await myConductor.init() - - await expect( - myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: literal>({ - // host: '127.0.0.1', - profile: 'myProfile', - }) as any, - }) - ).rejects.toMatch(/bad option/) - await expect( - // @ts-ignore - myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: { - host: '127.0.0.1', - // profile: 'myProfile' - }, - }) - ).rejects.toMatch(/bad option/) - - expect(onError).toHaveBeenCalledTimes(2) - onError.mockClear() - - const deviceContainer = await myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: { - host: '127.0.0.1', - profile: 'myProfile', - }, - }) - const device = deviceContainer.device - const connectionChanged = jest.fn() - await device.on('connectionChanged', connectionChanged) - - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() - expect(mse.getMockRundowns()).toHaveLength(1) - - expect(connectionChanged).toHaveBeenCalledTimes(0) - expect(await device.getStatus()).toMatchObject({ - statusCode: StatusCode.GOOD, - }) - connectionChanged.mockClear() - - mse.mockSetDisconnected() - - await mockTime.advanceTimeTicks(100) - expect(connectionChanged).toHaveBeenCalledTimes(1) - expect(await device.getStatus()).toMatchObject({ - statusCode: StatusCode.BAD, - }) - connectionChanged.mockClear() - - mse.mockSetConnected() - - await mockTime.advanceTimeTicks(100) - expect(connectionChanged).toHaveBeenCalledTimes(1) - expect(await device.getStatus()).toMatchObject({ - statusCode: StatusCode.GOOD, - }) - - await device.terminate() - - await mockTime.advanceTimeTicks(1000) - - expect(onError).toHaveBeenCalledTimes(0) - expect(onWarning).toHaveBeenCalledTimes(0) - }) - test('clear all elements', async () => { - const commandReceiver0 = jest.fn(async () => { - return Promise.resolve() - }) - const myChannelMapping0: Mapping = { - device: DeviceType.VIZMSE, - deviceId: 'myViz', - options: {}, - } - const myChannelMapping1: Mapping = { - device: DeviceType.VIZMSE, - deviceId: 'myViz', - options: {}, - } - const myChannelMapping: Mappings = { - viz0: myChannelMapping0, - viz_continue: myChannelMapping1, - } - - const myConductor = new Conductor({ - multiThreadedResolver: false, - getCurrentTime: mockTime.getCurrentTime, - }) - myConductor.setTimelineAndMappings([], myChannelMapping) - await myConductor.init() - await myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: { - host: '127.0.0.1', - preloadAllElements: true, - playlistID: 'my-super-playlist-id', - profile: 'profile9999', - clearAllTemplateName: 'clear_all_of_them', - clearAllCommands: ['RENDERER*FRONT_LAYER SET_OBJECT ', 'RENDERER SET_OBJECT '], - showDirectoryPath: 'SOFIE', - }, - commandReceiver: commandReceiver0, - }) - await mockTime.advanceTimeToTicks(10100) - - const deviceContainer = myConductor.getDevice('myViz') - const device = deviceContainer!.device as ThreadedClass - await device.ignoreWaitsInTests() - - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) + // test('Internal element', async () => { + // const { device, myChannelMapping } = await setupDevice() + // await mockTime.advanceTimeToTicks(10100) + + // // // mock the sendCommand function: + // // const mockSendCommand = jest.fn, Parameters>( + // // async () => { + // // // No-op + // // } + // // ) + // // device.sendCommand = mockSendCommand + + // device.ignoreWaitsInTests() + + // // // Check that no commands has been scheduled: + // // expect(mockSendCommand).toHaveBeenCalledTimes(0) + + // const state0: VizMSEState = {} + + // myConductor.setTimelineAndMappings([ + // { + // id: 'obj0', + // enable: { + // start: mockTime.now + 5000, // 15100 + // duration: 5000, // 20100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, + // // continueStep?: number + // // channelName?: string + // // cue?: boolean + // // noAutoPreloading?: boolean + // templateName: 'myInternalElement', + // templateData: ['line1', 'line2'], + // showName: MOCK_SHOWS[0].name, + // }, + // }, + // { + // id: 'obj1', + // enable: { + // start: mockTime.now + 7000, // 17100 + // duration: 5000, // 22100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, + // templateName: 'myInternalElement2', + // templateData: ['line1'], + // showName: MOCK_SHOWS[0].name, + // }, + // }, + // { + // id: 'obj2', + // enable: { + // start: mockTime.now + 9000, // 19100 + // duration: 500, + // }, + // layer: 'viz_continue', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.CONTINUE, + // reference: 'viz0', + // }, + // }, + // ]) + + // await mockTime.advanceTimeTicks(500) // 10500 + // expect(commandReceiver0.mock.calls.length).toEqual(0) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(14500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // // fromLookahead?: boolean + // // layerId?: string + // time: 14100, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: ['line1', 'line2'], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'prepare', + // // channelName?: string + // // noAutoPreloading?: boolean + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(15500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 15100, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: ['line1', 'line2'], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'take', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(16500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 16100, + // content: { + // instanceName: expect.stringContaining('myInternalElement2'), + // templateName: 'myInternalElement2', + // templateData: ['line1'], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'prepare', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(17500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 17100, + // content: { + // instanceName: expect.stringContaining('myInternalElement2'), + // templateName: 'myInternalElement2', + // templateData: ['line1'], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'take', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(19500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj2', + // time: 19100, + // content: { + // instanceName: expect.stringContaining('myInternalElement2'), + // templateName: 'myInternalElement2', + // templateData: ['line1'], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'continue', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(22500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 22100, + // content: { + // instanceName: expect.stringContaining('myInternalElement2'), + // templateName: 'myInternalElement2', + // templateData: ['line1'], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'out', + // }) + // }) + // test('External/Pilot element', async () => { + // const { device, myConductor, onError, commandReceiver0 } = await setupDevice() + // await mockTime.advanceTimeToTicks(10100) + + // await device.ignoreWaitsInTests() + // // Check that no commands has been scheduled: + // expect(await device.queue).toHaveLength(0) + + // myConductor.setTimelineAndMappings([ + // { + // id: 'obj0', + // enable: { + // start: mockTime.now + 5000, // 15100 + // duration: 5000, // 20100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.ELEMENT_PILOT, + // // continueStep?: number + // // cue?: boolean + // // noAutoPreloading?: boolean + // channelName: 'FULL1', + // templateVcpId: 1337, + // }, + // }, + // { + // id: 'obj1', + // enable: { + // start: mockTime.now + 7000, // 17100 + // duration: 5000, // 22100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.ELEMENT_PILOT, + // channelName: 'FULL1', + // templateVcpId: 1338, + // }, + // }, + // { + // id: 'obj2', + // enable: { + // start: mockTime.now + 9000, // 19100 + // duration: 500, + // }, + // layer: 'viz_continue', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.CONTINUE, + // reference: 'viz0', + // }, + // }, + // ]) + + // await mockTime.advanceTimeTicks(500) // 10500 + // expect(commandReceiver0.mock.calls.length).toEqual(0) + + // const mse = vConnection.getLastMockMSE() + // expect(mse.getMockRundowns()).toHaveLength(1) + // const rundown = _.last(mse.getMockRundowns()) as VRundownMocked + // expect(rundown).toBeTruthy() + + // expect(await device.supportsExpectedPlayoutItems).toEqual(true) + // const expectedItem1: VIZMSEPlayoutItemContentExternal = { + // vcpid: 1337, + // channel: 'FULL1', + // } + // const expectedItem2: VIZMSEPlayoutItemContentExternal = { + // vcpid: 1336, + // channel: 'FULL1', + // } + // await device.handleExpectedPlayoutItems([expectedItem1, expectedItem2]) + // await mockTime.advanceTimeTicks(100) + + // expect(rundown.createElement).toHaveBeenCalledTimes(2) + // expect(rundown.createElement).toHaveBeenNthCalledWith(1, expectedItem1) + // expect(rundown.createElement).toHaveBeenNthCalledWith(2, expectedItem2) + // rundown.createElement.mockClear() + + // await myConductor.devicesMakeReady(true) + // await mockTime.advanceTimeTicks(10) + + // expect(rundown.activate).toHaveBeenCalledTimes(2) + + // expect(rundown.getElement).toHaveBeenCalledTimes(4) + // expect(rundown.getElement).toHaveBeenNthCalledWith(1, expectedItem1) + // expect(rundown.getElement).toHaveBeenNthCalledWith(2, expectedItem2) + + // expect(rundown.createElement).toHaveBeenCalledTimes(2) + // expect(rundown.createElement).toHaveBeenNthCalledWith(1, expectedItem1) + // expect(rundown.createElement).toHaveBeenNthCalledWith(2, expectedItem2) + + // expect(rundown.initialize).toHaveBeenCalledTimes(2) + // expect(rundown.initialize).toHaveBeenNthCalledWith(1, expectedItem1) + // expect(rundown.initialize).toHaveBeenNthCalledWith(2, expectedItem2) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(14500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // // fromLookahead?: boolean + // // layerId?: string + // time: 14100, + // content: { + // vcpid: 1337, + // channel: 'FULL1', + // }, + // type: 'prepare', + // // noAutoPreloading?: boolean + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(15500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 15100, + // content: { + // vcpid: 1337, + // channel: 'FULL1', + // }, + // type: 'take', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(16500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 16100, + // content: { + // vcpid: 1338, + // channel: 'FULL1', + // }, + // type: 'prepare', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(17500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 17100, + // content: { + // vcpid: 1338, + // channel: 'FULL1', + // }, + // type: 'take', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(19500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj2', + // time: 19100, + // content: { + // vcpid: 1338, + // channel: 'FULL1', + // }, + // type: 'continue', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(22500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 22100, + // content: { + // vcpid: 1338, + // channel: 'FULL1', + // }, + // type: 'out', + // }) + + // // manually load elements: + // commandReceiver0.mockClear() + // rundown.getElement.mockClear() + // rundown.createElement.mockClear() + // rundown.initialize.mockClear() + + // const expectedItem3: VIZMSEPlayoutItemContentExternal = { + // vcpid: 9999, + // channel: 'FULL1', + // } + + // await device.handleExpectedPlayoutItems([expectedItem1, expectedItem2, expectedItem3]) + + // myConductor.setTimelineAndMappings([ + // { + // id: 'loadAll', + // enable: { + // start: 25000, + // duration: 500, // 25500 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.LOAD_ALL_ELEMENTS, + // }, + // }, + // ]) + + // await mockTime.advanceTimeToTicks(24900) + + // expect(rundown.createElement).toHaveBeenCalledTimes(1) + // expect(rundown.createElement).toHaveBeenNthCalledWith(1, expectedItem3) + // expect(rundown.initialize).toHaveBeenCalledTimes(0) + + // rundown.getElement.mockClear() + // rundown.createElement.mockClear() + // rundown.initialize.mockClear() + + // await mockTime.advanceTimeToTicks(25500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'loadAll', + // time: 25000, + // type: 'load_all_elements', + // }) + + // expect(rundown.initialize).toHaveBeenCalledTimes(1) + // expect(rundown.initialize).toHaveBeenNthCalledWith(1, expectedItem3) + + // expect(rundown.deactivate).toHaveBeenCalledTimes(0) + // await myConductor.devicesStandDown(true) + // expect(rundown.deactivate).toHaveBeenCalledTimes(1) + + // expect(onError).toHaveBeenCalledTimes(0) + // }) + // test('bad init options & basic functionality', async () => { + // const myConductor = new Conductor({ + // multiThreadedResolver: false, + // getCurrentTime: mockTime.getCurrentTime, + // }) + // const onError = jest.fn() + // myConductor.on('error', onError) + // const onWarning = jest.fn() + // myConductor.on('warning', onWarning) + + // await myConductor.init() + + // await expect( + // myConductor.addDevice('myViz', { + // type: DeviceType.VIZMSE, + // options: literal>({ + // // host: '127.0.0.1', + // profile: 'myProfile', + // }) as any, + // }) + // ).rejects.toMatch(/bad option/) + // await expect( + // // @ts-ignore + // myConductor.addDevice('myViz', { + // type: DeviceType.VIZMSE, + // options: { + // host: '127.0.0.1', + // // profile: 'myProfile' + // }, + // }) + // ).rejects.toMatch(/bad option/) + + // expect(onError).toHaveBeenCalledTimes(2) + // onError.mockClear() + + // const deviceContainer = await myConductor.addDevice('myViz', { + // type: DeviceType.VIZMSE, + // options: { + // host: '127.0.0.1', + // profile: 'myProfile', + // }, + // }) + // const device = deviceContainer.device + // const connectionChanged = jest.fn() + // await device.on('connectionChanged', connectionChanged) + + // const mse = vConnection.getLastMockMSE() + // expect(mse.getMockRundowns()).toHaveLength(1) + + // expect(connectionChanged).toHaveBeenCalledTimes(0) + // expect(await device.getStatus()).toMatchObject({ + // statusCode: StatusCode.GOOD, + // }) + // connectionChanged.mockClear() + + // mse.mockSetDisconnected() + + // await mockTime.advanceTimeTicks(100) + // expect(connectionChanged).toHaveBeenCalledTimes(1) + // expect(await device.getStatus()).toMatchObject({ + // statusCode: StatusCode.BAD, + // }) + // connectionChanged.mockClear() + + // mse.mockSetConnected() + + // await mockTime.advanceTimeTicks(100) + // expect(connectionChanged).toHaveBeenCalledTimes(1) + // expect(await device.getStatus()).toMatchObject({ + // statusCode: StatusCode.GOOD, + // }) + + // await device.terminate() + + // await mockTime.advanceTimeTicks(1000) + + // expect(onError).toHaveBeenCalledTimes(0) + // expect(onWarning).toHaveBeenCalledTimes(0) + // }) + // test('clear all elements', async () => { + // const commandReceiver0 = jest.fn(async () => { + // return Promise.resolve() + // }) + // const myChannelMapping0: Mapping = { + // device: DeviceType.VIZMSE, + // deviceId: 'myViz', + // options: {}, + // } + // const myChannelMapping1: Mapping = { + // device: DeviceType.VIZMSE, + // deviceId: 'myViz', + // options: {}, + // } + // const myChannelMapping: Mappings = { + // viz0: myChannelMapping0, + // viz_continue: myChannelMapping1, + // } + + // const myConductor = new Conductor({ + // multiThreadedResolver: false, + // getCurrentTime: mockTime.getCurrentTime, + // }) + // myConductor.setTimelineAndMappings([], myChannelMapping) + // await myConductor.init() + // await myConductor.addDevice('myViz', { + // type: DeviceType.VIZMSE, + // options: { + // host: '127.0.0.1', + // preloadAllElements: true, + // playlistID: 'my-super-playlist-id', + // profile: 'profile9999', + // clearAllTemplateName: 'clear_all_of_them', + // clearAllCommands: ['RENDERER*FRONT_LAYER SET_OBJECT ', 'RENDERER SET_OBJECT '], + // showDirectoryPath: 'SOFIE', + // }, + // commandReceiver: commandReceiver0, + // }) + // await mockTime.advanceTimeToTicks(10100) + + // const deviceContainer = myConductor.getDevice('myViz') + // const device = deviceContainer!.device as ThreadedClass + // await device.ignoreWaitsInTests() + + // // Check that no commands has been scheduled: + // expect(await device.queue).toHaveLength(0) + + // myConductor.setTimelineAndMappings([ + // { + // id: 'obj0', + // enable: { + // start: mockTime.now, // 10100 + // duration: 10 * 1000, // 20100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, + // templateName: 'myInternalElement', + // templateData: [], + // showName: MOCK_SHOWS[0].name, + // }, + // }, + // { + // id: 'clearAll', + // enable: { + // start: mockTime.now + 5000, // 15100 + // duration: 1000, // 16100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.CLEAR_ALL_ELEMENTS, + // channelsToSendCommands: ['OVL', 'FULL'], + // showName: MOCK_SHOWS[0].name, + // }, + // }, + // ] as TSRTimeline) + + // // await mockTime.advanceTimeTicks(500) // 10500 + // // expect(commandReceiver0.mock.calls.length).toEqual(0) + + // // commandReceiver0.mockClear() + // // await mockTime.advanceTimeToTicks(14500) + // // expect(commandReceiver0.mock.calls.length).toEqual(1) + // // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // // timelineObjId: 'obj0', + // // time: 14100, + // // templateInstance: expect.stringContaining('myInternalElement'), + // // type: 'prepare', + // // templateName: 'myInternalElement', + // // templateData: [] + // // }) + + // // commandReceiver0.mockClear() + // // await mockTime.advanceTimeToTicks(100) + // await mockTime.advanceTimeTicks(500) // 10500 + // expect(commandReceiver0.mock.calls.length).toEqual(2) + + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 10105, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: [], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'prepare', + // }) + // expect(getMockCall(commandReceiver0, 1, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 10105, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: [], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'take', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(15500) + // expect(commandReceiver0.mock.calls.length).toEqual(3) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'clearAll', + // time: 15100, + // type: 'clear_all_elements', + // templateName: 'clear_all_of_them', + // }) + // expect(getMockCall(commandReceiver0, 1, 1)).toMatchObject({ + // timelineObjId: 'clearAll', + // time: 15100, + // type: 'clear_all_engines', + // channels: ['OVL', 'FULL'], + // commands: ['RENDERER*FRONT_LAYER SET_OBJECT ', 'RENDERER SET_OBJECT '], + // }) + // expect(getMockCall(commandReceiver0, 2, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 15150, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: [], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'prepare', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(16500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 16100, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: [], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'take', + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(20500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 20100, + // content: { + // instanceName: expect.stringContaining('myInternalElement'), + // templateName: 'myInternalElement', + // templateData: [], + // showId: MOCK_SHOWS[0].id, + // }, + // type: 'out', + // }) + // }) + // test('Delayed External/Pilot element', async () => { + // const { device, myConductor, onError, commandReceiver0 } = await setupDevice() + // await mockTime.advanceTimeToTicks(10100) + + // await device.ignoreWaitsInTests() + + // // Check that no commands has been scheduled: + // expect(await device.queue).toHaveLength(0) + + // const mse = vConnection.getLastMockMSE() + // expect(mse.getMockRundowns()).toHaveLength(1) + // const rundown = _.last(mse.getMockRundowns()) as VRundownMocked + // expect(rundown).toBeTruthy() + + // myConductor.setTimelineAndMappings([ + // { + // id: 'obj0', + // enable: { + // start: mockTime.now + 5000, // 15100 + // duration: 5000, // 20100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.ELEMENT_PILOT, + // channelName: 'FULL1', + // templateVcpId: 1337, + // outTransition: { + // type: VIZMSETransitionType.DELAY, + // delay: 1000, + // }, + // }, + // }, + // ]) + + // await mockTime.advanceTimeToTicks(14000) + // expect(commandReceiver0.mock.calls.length).toEqual(0) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(14500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 14100, + // content: { + // vcpid: 1337, + // channel: 'FULL1', + // }, + // type: 'prepare', + // }) + + // commandReceiver0.mockClear() + // rundown.take.mockClear() + // await mockTime.advanceTimeToTicks(15500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(rundown.take).toHaveBeenCalledTimes(1) + // expect(rundown.take).toHaveBeenNthCalledWith(1, { + // vcpid: 1337, + // channel: 'FULL1', + // }) + // expect(rundown.out).toHaveBeenCalledTimes(0) + + // commandReceiver0.mockClear() + // rundown.out.mockClear() + // rundown.take.mockClear() + // await mockTime.advanceTimeToTicks(20500) + // expect(rundown.out).toHaveBeenCalledTimes(0) // because it's delayed! + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(21200) + // expect(rundown.out).toHaveBeenCalledTimes(1) + // expect(rundown.out).toHaveBeenNthCalledWith(1, { + // vcpid: 1337, + // channel: 'FULL1', + // }) + // expect(rundown.take).toHaveBeenCalledTimes(0) + + // expect(onError).toHaveBeenCalledTimes(0) + // }) + + // test('produces initialization and cleanup commands', async () => { + // const { device, myChannelMapping } = await setupDevice() + // await mockTime.advanceTimeToTicks(10100) + // device.ignoreWaitsInTests() + // await device.makeReady(true) + + // const mse = vConnection.getLastMockMSE() + // expect(mse.getMockRundowns()).toHaveLength(1) + // const rundown = _.last(mse.getMockRundowns()) as VRundownMocked + // expect(rundown).toBeTruthy() + + // myConductor.setTimelineAndMappings([ + // { + // id: 'obj0', + // enable: { + // start: mockTime.now + 5000, // 15100 + // duration: 5000, // 20100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.INITIALIZE_SHOWS, + // showNames: [MOCK_SHOWS[0].name, MOCK_SHOWS[1].name], + // }, + // }, + // { + // id: 'obj1', + // enable: { + // start: mockTime.now + 11000, // 21100 + // duration: 5000, // 26100 + // }, + // layer: 'viz0', + // content: { + // deviceType: DeviceType.VIZMSE, + // type: TimelineContentTypeVizMSE.CLEANUP_SHOWS, + // showNames: [MOCK_SHOWS[2].name, MOCK_SHOWS[3].name], + // }, + // }, + // ]) + + // await mockTime.advanceTimeToTicks(15000) + // expect(commandReceiver0.mock.calls.length).toEqual(0) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(15500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 15100, + // type: 'initialize_shows', + // showIds: [MOCK_SHOWS[0].id, MOCK_SHOWS[1].id], + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(20500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj0', + // time: 20100, + // type: 'initialize_shows', + // showIds: [], + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(21500) + // expect(commandReceiver0.mock.calls.length).toEqual(1) + // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ + // timelineObjId: 'obj1', + // time: 21100, + // type: 'cleanup_shows', + // showIds: [MOCK_SHOWS[2].id, MOCK_SHOWS[3].id], + // }) + + // commandReceiver0.mockClear() + // await mockTime.advanceTimeToTicks(26500) + // expect(commandReceiver0.mock.calls.length).toEqual(0) + + // expect(onError).toHaveBeenCalledTimes(0) + // }) + + // test.only('re-initializes show for incoming elements during TimelineObjVIZMSEInitializeShows', async () => { + // const { device, myChannelMapping } = await setupDevice() + // device.ignoreWaitsInTests() + + // await device.makeReady(true) + + // const mse = vConnection.getLastMockMSE() + // expect(mse.getMockRundowns()).toHaveLength(1) + // const rundown = _.last(mse.getMockRundowns()) as VRundownMocked + // expect(rundown).toBeTruthy() + + // // myConductor.setTimelineAndMappings([ + // // { + // // id: 'obj0', + // // enable: { + // // start: mockTime.now + 5000, // 15100 + // // duration: 5000, // 20100 + // // }, + // // layer: 'viz0', + // // content: { + // // deviceType: DeviceType.VIZMSE, + // // type: TimelineContentTypeVizMSE.INITIALIZE_SHOWS, + // // showNames: [MOCK_SHOWS[0].name, MOCK_SHOWS[1].name], + // // }, + // // }, + // // ]) + + // device.handleExpectedPlayoutItems( + // literal([ + // { + // rundownId: '', + // playlistId: defaultPlaylistId, + // templateName: 'bund', + // showName: MOCK_SHOWS[0].name, + // }, + // ]) + // ) + + // await mockTime.advanceTimeToTicks(15100) + + // await device.sendCommand({ + // command: { + // time: mockTime.now + 5000, // 15100 + // type: VizMSECommandType.INITIALIZE_SHOWS, + // showIds: [MOCK_SHOWS[0].name, MOCK_SHOWS[1].name], + // timelineObjId: 'obj0', + // }, + // context: 'MOCK', + // timelineObjId: 'obj0', + // queueId: undefined, + // }) + + // await mockTime.advanceTimeToTicks(15500) + + // rundown.initializeShow.mockClear() + // device.handleExpectedPlayoutItems( + // literal([ + // { + // rundownId: '', + // playlistId: defaultPlaylistId, + // templateName: 'bund', + // showName: MOCK_SHOWS[0].name, + // }, + // { + // rundownId: '', + // playlistId: defaultPlaylistId, + // templateName: 'bund', + // showName: MOCK_SHOWS[1].name, + // }, + // { + // rundownId: '', + // playlistId: defaultPlaylistId, + // templateName: 'ident', + // showName: MOCK_SHOWS[2].name, + // }, + // { + // rundownId: '', + // playlistId: defaultPlaylistId, + // templateName: 'tlf', + // showName: MOCK_SHOWS[1].name, + // }, + // ]) + // ) + // console.log('tick') + // await mockTime.advanceTimeToTicks(16500) + // console.log('tock') + // expect(rundown.initializeShow).toHaveBeenCalledTimes(1) + // expect(rundown.initializeShow).toHaveBeenNthCalledWith(1, MOCK_SHOWS[1].id) + + // rundown.initializeShow.mockClear() + // await mockTime.advanceTimeToTicks(25500) + // expect(rundown.initializeShow).toHaveBeenCalledTimes(0) + // }) - myConductor.setTimelineAndMappings([ - { - id: 'obj0', - enable: { - start: mockTime.now, // 10100 - duration: 10 * 1000, // 20100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.ELEMENT_INTERNAL, - templateName: 'myInternalElement', - templateData: [], - showName: MOCK_SHOWS[0].name, - }, - }, - { - id: 'clearAll', - enable: { - start: mockTime.now + 5000, // 15100 - duration: 1000, // 16100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.CLEAR_ALL_ELEMENTS, - channelsToSendCommands: ['OVL', 'FULL'], - showName: MOCK_SHOWS[0].name, - }, - }, - ] as TSRTimeline) - - // await mockTime.advanceTimeTicks(500) // 10500 - // expect(commandReceiver0.mock.calls.length).toEqual(0) - - // commandReceiver0.mockClear() - // await mockTime.advanceTimeToTicks(14500) - // expect(commandReceiver0.mock.calls.length).toEqual(1) - // expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - // timelineObjId: 'obj0', - // time: 14100, - // templateInstance: expect.stringContaining('myInternalElement'), - // type: 'prepare', - // templateName: 'myInternalElement', - // templateData: [] - // }) - - // commandReceiver0.mockClear() - // await mockTime.advanceTimeToTicks(100) - await mockTime.advanceTimeTicks(500) // 10500 - expect(commandReceiver0.mock.calls.length).toEqual(2) - - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 10105, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: [], - showId: MOCK_SHOWS[0].id, - }, - type: 'prepare', - }) - expect(getMockCall(commandReceiver0, 1, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 10105, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: [], - showId: MOCK_SHOWS[0].id, - }, - type: 'take', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(15500) - expect(commandReceiver0.mock.calls.length).toEqual(3) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'clearAll', - time: 15100, - type: 'clear_all_elements', - templateName: 'clear_all_of_them', - }) - expect(getMockCall(commandReceiver0, 1, 1)).toMatchObject({ - timelineObjId: 'clearAll', - time: 15100, - type: 'clear_all_engines', - channels: ['OVL', 'FULL'], - commands: ['RENDERER*FRONT_LAYER SET_OBJECT ', 'RENDERER SET_OBJECT '], - }) - expect(getMockCall(commandReceiver0, 2, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 15150, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: [], - showId: MOCK_SHOWS[0].id, - }, - type: 'prepare', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(16500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 16100, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: [], - showId: MOCK_SHOWS[0].id, - }, - type: 'take', - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(20500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 20100, - content: { - instanceName: expect.stringContaining('myInternalElement'), - templateName: 'myInternalElement', - templateData: [], - showId: MOCK_SHOWS[0].id, - }, - type: 'out', - }) - }) - test('Delayed External/Pilot element', async () => { - const { device, myConductor, onError, commandReceiver0 } = await setupDevice() - await mockTime.advanceTimeToTicks(10100) - - await device.ignoreWaitsInTests() - - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) - - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() - expect(mse.getMockRundowns()).toHaveLength(1) - const rundown = _.last(mse.getMockRundowns()) as VRundownMocked - expect(rundown).toBeTruthy() - - myConductor.setTimelineAndMappings([ - { - id: 'obj0', - enable: { - start: mockTime.now + 5000, // 15100 - duration: 5000, // 20100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.ELEMENT_PILOT, - channelName: 'FULL1', - templateVcpId: 1337, - outTransition: { - type: VIZMSETransitionType.DELAY, - delay: 1000, - }, - }, - }, - ]) - - await mockTime.advanceTimeToTicks(14000) - expect(commandReceiver0.mock.calls.length).toEqual(0) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(14500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 14100, - content: { - vcpid: 1337, - channel: 'FULL1', - }, - type: 'prepare', - }) - - commandReceiver0.mockClear() - rundown.take.mockClear() - await mockTime.advanceTimeToTicks(15500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(rundown.take).toHaveBeenCalledTimes(1) - expect(rundown.take).toHaveBeenNthCalledWith(1, { - vcpid: 1337, - channel: 'FULL1', - }) - expect(rundown.out).toHaveBeenCalledTimes(0) - - commandReceiver0.mockClear() - rundown.out.mockClear() - rundown.take.mockClear() - await mockTime.advanceTimeToTicks(20500) - expect(rundown.out).toHaveBeenCalledTimes(0) // because it's delayed! - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(21200) - expect(rundown.out).toHaveBeenCalledTimes(1) - expect(rundown.out).toHaveBeenNthCalledWith(1, { - vcpid: 1337, - channel: 'FULL1', - }) - expect(rundown.take).toHaveBeenCalledTimes(0) - - expect(onError).toHaveBeenCalledTimes(0) - }) - test('produces initialization and cleanup commands', async () => { - const { device, myConductor, onError, commandReceiver0 } = await setupDevice() - await mockTime.advanceTimeToTicks(10100) - await device.ignoreWaitsInTests() - await myConductor.devicesMakeReady(true) - - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) - - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() - expect(mse.getMockRundowns()).toHaveLength(1) - const rundown = _.last(mse.getMockRundowns()) as VRundownMocked - expect(rundown).toBeTruthy() - - myConductor.setTimelineAndMappings([ - { - id: 'obj0', - enable: { - start: mockTime.now + 5000, // 15100 - duration: 5000, // 20100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.INITIALIZE_SHOWS, - showNames: [MOCK_SHOWS[0].name, MOCK_SHOWS[1].name], - }, - }, - { - id: 'obj1', - enable: { - start: mockTime.now + 11000, // 21100 - duration: 5000, // 26100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.CLEANUP_SHOWS, - showNames: [MOCK_SHOWS[2].name, MOCK_SHOWS[3].name], - }, - }, - ]) - - await mockTime.advanceTimeToTicks(15000) - expect(commandReceiver0.mock.calls.length).toEqual(0) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(15500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 15100, - type: 'initialize_shows', - showIds: [MOCK_SHOWS[0].id, MOCK_SHOWS[1].id], - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(20500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj0', - time: 20100, - type: 'initialize_shows', - showIds: [], - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(21500) - expect(commandReceiver0.mock.calls.length).toEqual(1) - expect(getMockCall(commandReceiver0, 0, 1)).toMatchObject({ - timelineObjId: 'obj1', - time: 21100, - type: 'cleanup_shows', - showIds: [MOCK_SHOWS[2].id, MOCK_SHOWS[3].id], - }) - - commandReceiver0.mockClear() - await mockTime.advanceTimeToTicks(26500) - expect(commandReceiver0.mock.calls.length).toEqual(0) - - expect(onError).toHaveBeenCalledTimes(0) - }) - test('re-initializes show for incoming elements during TimelineObjVIZMSEInitializeShows', async () => { - const { device, myConductor, onError } = await setupDevice() - await device.ignoreWaitsInTests() - await myConductor.devicesMakeReady(true) - - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) - - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() - expect(mse.getMockRundowns()).toHaveLength(1) - const rundown = _.last(mse.getMockRundowns()) as VRundownMocked - expect(rundown).toBeTruthy() - - myConductor.setTimelineAndMappings([ - { - id: 'obj0', - enable: { - start: mockTime.now + 5000, // 15100 - duration: 5000, // 20100 - }, - layer: 'viz0', - content: { - deviceType: DeviceType.VIZMSE, - type: TimelineContentTypeVizMSE.INITIALIZE_SHOWS, - showNames: [MOCK_SHOWS[0].name, MOCK_SHOWS[1].name], - }, - }, - ]) - - await device.handleExpectedPlayoutItems( - literal([ - { - templateName: 'bund', - showName: MOCK_SHOWS[0].name, - }, - ]) - ) - - await mockTime.advanceTimeToTicks(15500) - - rundown.initializeShow.mockClear() - await device.handleExpectedPlayoutItems( - literal([ - { - templateName: 'bund', - showName: MOCK_SHOWS[0].name, - }, - { - templateName: 'bund', - showName: MOCK_SHOWS[1].name, - }, - { - templateName: 'ident', - showName: MOCK_SHOWS[2].name, - }, - { - templateName: 'tlf', - showName: MOCK_SHOWS[1].name, - }, - ]) - ) - await mockTime.advanceTimeToTicks(16500) - expect(rundown.initializeShow).toHaveBeenCalledTimes(1) - expect(rundown.initializeShow).toHaveBeenNthCalledWith(1, MOCK_SHOWS[1].id) - - rundown.initializeShow.mockClear() - await mockTime.advanceTimeToTicks(25500) - expect(rundown.initializeShow).toHaveBeenCalledTimes(0) - - expect(onError).toHaveBeenCalledTimes(0) - }) test('creates and deletes internal elements', async () => { - const { device, myConductor, onError } = await setupDevice() + const { device } = await setupDevice() await mockTime.advanceTimeToTicks(10100) - await device.ignoreWaitsInTests() - await myConductor.devicesMakeReady(true) - - // Check that no commands has been scheduled: - expect(await device.queue).toHaveLength(0) + device.ignoreWaitsInTests() + await device.makeReady(true) - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() + const mse = vConnection.getLastMockMSE() expect(mse.getMockRundowns()).toHaveLength(1) const rundown = _.last(mse.getMockRundowns()) as VRundownMocked expect(rundown).toBeTruthy() await mockTime.advanceTimeToTicks(20500) - await device.handleExpectedPlayoutItems( - literal([ + device.handleExpectedPlayoutItems( + literal([ { + rundownId: '', + playlistId: '', templateName: 'bund', showName: MOCK_SHOWS[1].name, templateData: ['foo', 'bar'], @@ -1004,6 +1045,10 @@ describe('vizMSE', () => { }, ]) ) + + // let some time pass + await mockTime.advanceTimeToTicks(21500) + expect(rundown.createElement).toHaveBeenCalledTimes(1) expect(rundown.createElement).toHaveBeenNthCalledWith( 1, @@ -1016,7 +1061,7 @@ describe('vizMSE', () => { 'my_channel' ) - await device.handleExpectedPlayoutItems(literal([])) + device.handleExpectedPlayoutItems([]) await mockTime.advanceTimeToTicks(39500) expect(rundown.deleteElement).toHaveBeenCalledTimes(0) @@ -1029,36 +1074,24 @@ describe('vizMSE', () => { showId: MOCK_SHOWS[1].id, }) ) - - expect(onError).toHaveBeenCalledTimes(0) }) test('vizMSE: clear all elements on makeReady when clearAllOnMakeReady is true', async () => { const CLEAR_COMMAND = 'RENDERER*FRONT_LAYER SET_OBJECT' const PROFILE_NAME = 'mockProfile' - const myConductor = new Conductor({ - multiThreadedResolver: false, - getCurrentTime: mockTime.getCurrentTime, - }) - await myConductor.init() - await myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: { - host: '127.0.0.1', - preloadAllElements: true, - playlistID: 'my-super-playlist-id', - profile: PROFILE_NAME, - clearAllOnMakeReady: true, - clearAllTemplateName: 'clear_all_of_them', - clearAllCommands: [CLEAR_COMMAND], - }, - }) - const deviceContainer = myConductor.getDevice('myViz') - const device = deviceContainer!.device as ThreadedClass - await device.ignoreWaitsInTests() + const device = new VizMSEDevice(getDeviceContext()) + await device.init({ + host: '127.0.0.1', + preloadAllElements: true, + playlistID: 'my-super-playlist-id', + profile: PROFILE_NAME, + clearAllOnMakeReady: true, + clearAllTemplateName: 'clear_all_of_them', + clearAllCommands: [CLEAR_COMMAND], + }) + device.ignoreWaitsInTests() - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() + const mse = vConnection.getLastMockMSE() mse.mockCreateProfile(PROFILE_NAME, { Channel1: { entry: { @@ -1106,30 +1139,20 @@ describe('vizMSE', () => { test("vizMSE: don't clear engines when clearAllOnMakeReady is set to false", async () => { const CLEAR_COMMAND = 'RENDERER*FRONT_LAYER SET_OBJECT' const PROFILE_NAME = 'mockProfile' - const myConductor = new Conductor({ - multiThreadedResolver: false, - getCurrentTime: mockTime.getCurrentTime, - }) - await myConductor.init() - await myConductor.addDevice('myViz', { - type: DeviceType.VIZMSE, - options: { - host: '127.0.0.1', - preloadAllElements: true, - playlistID: 'my-super-playlist-id', - profile: PROFILE_NAME, - clearAllOnMakeReady: false, - clearAllTemplateName: 'clear_all_of_them', - clearAllCommands: [CLEAR_COMMAND], - }, - }) - const deviceContainer = myConductor.getDevice('myViz') - const device = deviceContainer!.device as ThreadedClass - await device.ignoreWaitsInTests() + const device = new VizMSEDevice(getDeviceContext()) + await device.init({ + host: '127.0.0.1', + preloadAllElements: true, + playlistID: 'my-super-playlist-id', + profile: PROFILE_NAME, + clearAllOnMakeReady: false, + clearAllTemplateName: 'clear_all_of_them', + clearAllCommands: [CLEAR_COMMAND], + }) + device.ignoreWaitsInTests() - const mse = _.last(getMockMSEs()) as MSEMock - expect(mse).toBeTruthy() + const mse = vConnection.getLastMockMSE() mse.mockCreateProfile(PROFILE_NAME, { Channel1: { entry: {