import { Conductor , DeviceType , ConductorOptions , Device , TimelineTriggerTimeResult , DeviceOptionsAny , Mappings , DeviceContainer , Timeline as TimelineTypes , TSRTimelineObj , TSRTimeline , TSRTimelineObjBase , CommandReport , DeviceOptionsAtem , AtemMediaPoolAsset , MediaObject , ExpectedPlayoutItem , ExpectedPlayoutItemContent , SlowSentCommandInfo , SlowFulfilledCommandInfo , } from 'timeline-state-resolver' ; import { CoreHandler , CoreTSRDeviceHandler } from './coreHandler' ; import clone = require ( 'fast-clone' ) ; import * as crypto from 'crypto' ; import * as cp from 'child_process' ; import * as _ from 'underscore' ; import { CoreConnection , PeripheralDeviceAPI as P } from '@sofie-automation/server-core-integration' ; import { TimelineObjectCoreExt } from '@sofie-automation/blueprints-integration' ; import { LoggerInstance } from './index' ; import { disableAtemUpload } from './config' ; import Debug from 'debug' ; const debug = Debug ( 'playout-gateway' ) ; export interface TSRConfig { } export interface TSRSettings { devices : { [ deviceId ] : DeviceOptionsAny ; } ; initializeAsClear : boolean ; mappings : Mappings ; errorReporting ? : boolean ; multiThreading ? : boolean ; multiThreadedResolver ? : boolean ; useCacheWhenResolving ? : boolean ; } export interface TSRDevice { coreConnection : CoreConnection ; device : Device < DeviceOptionsAny > ; } export interface TimelineObjGeneric extends TimelineObjectCoreExt { /** Unique _id (generally obj.studioId + '_' + obj.id) */ _id : string ; /** Unique within a timeline (ie within a studio) */ id : string ; /** Studio installation Id */ studioId : string ; objectType : TimelineObjType ; enable : TimelineTypes . TimelineEnable & { setFromNow ? : boolean ; } ; inGroup ? : string ; metadata ? : { [ key ] : any ; } ; /** Only set to true when an object is inserted by lookahead */ isLookahead ? : boolean ; /** Set when an object is on a virtual layer for lookahead, so that it can be routed correctly */ originalLLayer ? : string | number ; } export enum TimelineObjType { /** Objects played in a rundown */ RUNDOWN = 'rundown' , /** Objects controlling recording */ RECORDING = 'record' , /** Objects controlling manual playback */ MANUAL = 'manual' , /** "Magic object", used to calculate a hash of the timeline */ STAT = 'stat' } export interface TimelineComplete { _id : string ; timeline : Array < TimelineObjGeneric > ; } export interface TimelineContentObjectTmp extends TSRTimelineObjBase { inGroup ? : string ; } /** Max time for initializing devices */ const INIT_TIMEOUT = 10000 ; /**
 * Represents a connection between Gateway and TSR
 */ export class TSRHandler { logger : LoggerInstance ; tsr ! : Conductor ; private _coreHandler ! : CoreHandler ; private _triggerupdateExpectedPlayoutItemsTimeout : any = null ; private _coreTsrHandlers : { [ deviceId ] : CoreTSRDeviceHandler ; } = { } ; private _observers : Array < any > = [ ] ; private _cachedStudioId = '' ; private _initialized = false ; private _multiThreaded : boolean | null = null ; private _reportAllCommands : boolean | null = null ; private _errorReporting : boolean | null = null ; private _updateDevicesIsRunning = false ; private _lastReportedObjHashes : string [ ] = [ ] ; private _triggerUpdateDevicesCheckAgain = false ; private _triggerUpdateDevicesTimeout : NodeJS . Timeout | undefined ; constructor ( logger ) { this . logger = logger ; } public async init ( _config , coreHandler ) { this . _coreHandler = coreHandler ; this . _coreHandler . setTSR ( this ) ; this . logger . info ( 'TSRHandler init' ) ; const peripheralDevice = await coreHandler . core . getPeripheralDevice ( ) ; const settings = peripheralDevice . settings || { } ; this . logger . info ( 'Devices' , settings . devices ) ; const c = { getCurrentTime : ( ) : number => { return this . _coreHandler . core . getCurrentTime ( ) ; } , initializeAsClear : settings . initializeAsClear !== false , multiThreadedResolver : settings . multiThreadedResolver === true , useCacheWhenResolving : settings . useCacheWhenResolving === true , proActiveResolve : true } ; this . tsr = new Conductor ( c ) ; this . _triggerupdateTimelineAndMappings ( ) ; coreHandler . onConnected ( ( ) => { this . setupObservers ( ) ; this . resendStatuses ( ) ; } ) ; this . setupObservers ( ) ; this . tsr . on ( 'error' , ( e , ... args ) => { const msg = e + '' ; const cmdReply = args [ 0 ] ; if ( msg . match ( /casparcg/i ) && ( msg . match ( /PlayCommand/i ) || msg . match ( /LoadbgCommand/i ) ) && cmdReply && _ . isObject ( cmdReply ) && cmdReply . response && cmdReply . response . code === 404 ) { this . logger . warn ( 'TSR' , e , ... args ) ; } else { this . logger . error ( 'TSR' , e , ... args ) ; } } ) ; this . tsr . on ( 'info' , ( msg , ... args ) => { this . logger . info ( 'TSR' , msg , ... args ) ; } ) ; this . tsr . on ( 'warning' , ( msg , ... args ) => { this . logger . warn ( 'TSR' , msg , ... args ) ; } ) ; this . tsr . on ( 'debug' , ( ... args ) => { if ( this . _coreHandler . logDebug ) { const msg = { message : 'TSR debug message (' + args . length + ')' , data : [ ] } ; if ( args . length ) { _ . each ( args , ( arg ) => { if ( _ . isObject ( arg ) ) { msg . data . push ( JSON . stringify ( arg ) ) ; } else { msg . data . push ( arg ) ; } } ) ; } else { msg . data . push ( '>empty message<' ) ; } this . logger . debug ( msg ) ; } } ) ; this . tsr . on ( 'setTimelineTriggerTime' , ( r ) => { this . _coreHandler . core . callMethod ( P . methods . timelineTriggerTime , [ r ] ) . catch ( ( e ) => { this . logger . error ( 'Error in setTimelineTriggerTime' , e ) ; } ) ; } ) ; this . tsr . on ( 'timelineCallback' , ( time , objId , callbackName , data ) => { const method = P . methods [ callbackName ] ; if ( method ) { this . _coreHandler . core . callMethod ( method , [ Object . assign ( { } , data , { objId : objId , time : time } ) , ] ) . catch ( ( e ) => { this . logger . error ( 'Error in timelineCallback' , e ) ; } ) ; } else { this . logger . error ( `Unknown callback method "${ callbackName }"` ) ; } } ) ; this . tsr . on ( 'resolveDone' , ( timelineHash , resolveDuration ) => { if ( this . _lastReportedObjHashes . includes ( timelineHash ) ) return ; this . _lastReportedObjHashes . unshift ( timelineHash ) ; if ( this . _lastReportedObjHashes . length > 10 ) { this . _lastReportedObjHashes . length = 10 ; } this . _coreHandler . core . callMethod ( 'peripheralDevice.reportResolveDone' , [ timelineHash , resolveDuration ] ) . catch ( ( e ) => { this . logger . error ( 'Error in reportResolveDone' , e ) ; } ) ; } ) ; this . logger . debug ( 'tsr init' ) ; await this . tsr . init ( ) ; this . _initialized = true ; this . _triggerupdateTimelineAndMappings ( ) ; this . onSettingsChanged ( ) ; this . _triggerUpdateDevices ( ) ; this . logger . debug ( 'tsr init done' ) ; } private setupObservers ( ) { if ( this . _observers . length ) { this . logger . debug ( 'Clearing observers..' ) ; this . _observers . forEach ( ( obs ) => { obs . stop ( ) ; } ) ; this . _observers = [ ] ; } this . logger . debug ( 'Renewing observers' ) ; const timelineObserver = this . _coreHandler . core . observe ( 'studioTimeline' ) ; timelineObserver . added = ( ) => { this . _triggerupdateTimelineAndMappings ( ) ; } ; timelineObserver . changed = ( ) => { this . _triggerupdateTimelineAndMappings ( ) ; } ; timelineObserver . removed = ( ) => { this . _triggerupdateTimelineAndMappings ( ) ; } ; this . _observers . push ( timelineObserver ) ; const mappingsObserver = this . _coreHandler . core . observe ( 'studioMappings' ) ; mappingsObserver . added = ( ) => { this . _triggerupdateTimelineAndMappings ( ) ; } ; mappingsObserver . changed = ( ) => { this . _triggerupdateTimelineAndMappings ( ) ; } ; mappingsObserver . removed = ( ) => { this . _triggerupdateTimelineAndMappings ( ) ; } ; this . _observers . push ( mappingsObserver ) ; const deviceObserver = this . _coreHandler . core . observe ( 'peripheralDevices' ) ; deviceObserver . added = ( ) => { debug ( 'triggerUpdateDevices from deviceObserver added' ) ; this . _triggerUpdateDevices ( ) ; } ; deviceObserver . changed = ( ) => { debug ( 'triggerUpdateDevices from deviceObserver changed' ) ; this . _triggerUpdateDevices ( ) ; } ; deviceObserver . removed = ( ) => { debug ( 'triggerUpdateDevices from deviceObserver removed' ) ; this . _triggerUpdateDevices ( ) ; } ; this . _observers . push ( deviceObserver ) ; const expectedPlayoutItemsObserver = this . _coreHandler . core . observe ( 'expectedPlayoutItems' ) ; expectedPlayoutItemsObserver . added = ( ) => { this . _triggerupdateExpectedPlayoutItems ( ) ; } ; expectedPlayoutItemsObserver . changed = ( ) => { this . _triggerupdateExpectedPlayoutItems ( ) ; } ; expectedPlayoutItemsObserver . removed = ( ) => { this . _triggerupdateExpectedPlayoutItems ( ) ; } ; this . _observers . push ( expectedPlayoutItemsObserver ) ; } private resendStatuses ( ) { _ . each ( this . _coreTsrHandlers , ( tsrHandler ) => { tsrHandler . sendStatus ( ) ; } ) ; } destroy ( ) { return this . tsr . destroy ( ) ; } getTimeline ( ) { const studioId = this . _getStudioId ( ) ; if ( ! studioId ) { this . logger . warn ( 'no studioId' ) ; return undefined ; } const timeline = this . _coreHandler . core . getCollection ( 'studioTimeline' ) . findOne ( ( o ) => { return o . _id === studioId ; } ) ; return timeline as any ; } getMappings ( ) { const studioId = this . _getStudioId ( ) ; if ( ! studioId ) { return undefined ; } const mappingsObject = this . _coreHandler . core . getCollection ( 'studioMappings' ) . findOne ( studioId ) ; return mappingsObject as any ; } onSettingsChanged ( ) { if ( ! this . _initialized ) return ; if ( this . tsr . logDebug !== this . _coreHandler . logDebug ) { this . logger . info ( `Log settings: ${ this . _coreHandler . logDebug }` ) ; this . tsr . logDebug = this . _coreHandler . logDebug ; } if ( this . _errorReporting !== this . _coreHandler . errorReporting ) { this . _errorReporting = this . _coreHandler . errorReporting ; this . logger . info ( 'ErrorReporting: ' + this . _multiThreaded ) ; } if ( this . _multiThreaded !== this . _coreHandler . multithreading ) { this . _multiThreaded = this . _coreHandler . multithreading ; this . logger . info ( 'Multithreading: ' + this . _multiThreaded ) ; debug ( 'triggerUpdateDevices from onSettingsChanged' ) ; this . _triggerUpdateDevices ( ) ; } if ( this . _reportAllCommands !== this . _coreHandler . reportAllCommands ) { this . _reportAllCommands = this . _coreHandler . reportAllCommands ; this . logger . info ( 'ReportAllCommands: ' + this . _reportAllCommands ) ; debug ( 'triggerUpdateDevices from onSettingsChanged' ) ; this . _triggerUpdateDevices ( ) ; } } private _triggerupdateTimelineAndMappings ( ) { if ( ! this . _initialized ) return ; this . _updateTimelineAndMappings ( ) ; } private _updateTimelineAndMappings ( ) { const timeline = this . getTimeline ( ) ; const mappingsObject = this . getMappings ( ) ; if ( ! timeline ) { this . logger . debug ( `Cancel resolving: No timeline` ) ; return ; } if ( ! mappingsObject ) { this . logger . debug ( `Cancel resolving: No mappings` ) ; return ; } if ( timeline . mappingsHash !== mappingsObject . mappingsHash ) { this . logger . info ( `Cancel resolving: mappingsHash differ: "${ timeline . mappingsHash }" vs "${ mappingsObject . mappingsHash }"` ) ; return ; } this . logger . debug ( `Trigger new resolving` ) ; const transformedTimeline = this . _transformTimeline ( timeline . timeline ) ; this . tsr . timelineHash = timeline . timelineHash ; this . tsr . setTimelineAndMappings ( transformedTimeline , mappingsObject . mappings ) ; } private _getPeripheralDevice ( ) { const peripheralDevices = this . _coreHandler . core . getCollection ( 'peripheralDevices' ) ; return peripheralDevices . findOne ( this . _coreHandler . core . deviceId ) ; } private _getStudio ( ) { const peripheralDevice = this . _getPeripheralDevice ( ) ; if ( peripheralDevice ) { const studios = this . _coreHandler . core . getCollection ( 'studios' ) ; return studios . findOne ( peripheralDevice . studioId ) ; } return null ; } private _getStudioId ( ) { if ( this . _cachedStudioId ) return this . _cachedStudioId ; const studio = this . _getStudio ( ) ; if ( studio ) { this . _cachedStudioId = studio . _id ; return studio . _id ; } return null ; } private _triggerUpdateDevices ( ) { if ( ! this . _initialized ) return ; if ( this . _triggerUpdateDevicesTimeout ) clearTimeout ( this . _triggerUpdateDevicesTimeout ) ; this . _triggerUpdateDevicesTimeout = undefined ; if ( ! this . _updateDevicesIsRunning ) { this . _updateDevicesIsRunning = true ; debug ( 'triggerUpdateDevices now' ) ; setTimeout ( ( ) => { this . _updateDevices ( ) . then ( ( ) => { this . _updateDevicesIsRunning = false ; if ( this . _triggerUpdateDevicesCheckAgain ) { debug ( 'triggerUpdateDevices from updateDevices promise resolved' ) ; if ( this . _triggerUpdateDevicesTimeout ) clearTimeout ( this . _triggerUpdateDevicesTimeout ) ; this . _triggerUpdateDevicesTimeout = setTimeout ( ( ) => this . _triggerUpdateDevices ( ) , 1000 ) ; } this . _triggerUpdateDevicesCheckAgain = false ; } , ( ) => { this . _updateDevicesIsRunning = false ; if ( this . _triggerUpdateDevicesCheckAgain ) { debug ( 'triggerUpdateDevices from updateDevices promise rejected' ) ; setTimeout ( ( ) => this . _triggerUpdateDevices ( ) , 1000 ) ; if ( this . _triggerUpdateDevicesTimeout ) clearTimeout ( this . _triggerUpdateDevicesTimeout ) ; this . _triggerUpdateDevicesTimeout = setTimeout ( ( ) => this . _triggerUpdateDevices ( ) , 1000 ) ; } this . _triggerUpdateDevicesCheckAgain = false ; } ) ; } , 10 ) ; } else { debug ( 'triggerUpdateDevices already running, cue a check again later' ) ; this . _triggerUpdateDevicesCheckAgain = true ; } } private async _updateDevices ( ) { this . logger . info ( 'updateDevices start' ) ; const peripheralDevices = this . _coreHandler . core . getCollection ( 'peripheralDevices' ) ; const peripheralDevice = peripheralDevices . findOne ( this . _coreHandler . core . deviceId ) ; let ps = [ ] ; const promiseOperations = { } ; const keepTrack = < T > ( p , name ) => { promiseOperations [ name ] = true ; return p . then ( ( result ) => { delete promiseOperations [ name ] ; return result ; } ) ; } ; const devices = new Map < string , DeviceOptionsAny > ( ) ; if ( peripheralDevice ) { const settings = peripheralDevice . settings || { } ; for ( const [ deviceId , device ] of Object . entries ( settings . devices ) ) { if ( ! device . disable ) { devices . set ( deviceId , device ) ; } } for ( const [ deviceId , orgDeviceOptions ] of devices . entries ( ) ) { const oldDevice = this . tsr . getDevice ( deviceId , true ) ; const deviceOptions = _ . extend ( { limitSlowSentCommand : 40 , limitSlowFulfilledCommand : 100 , options : { } } , orgDeviceOptions ) ; if ( this . _multiThreaded !== null && deviceOptions . isMultiThreaded === undefined ) { deviceOptions . isMultiThreaded = this . _multiThreaded ; } if ( this . _reportAllCommands !== null && deviceOptions . reportAllCommands === undefined ) { deviceOptions . reportAllCommands = this . _reportAllCommands ; } if ( ! oldDevice ) { if ( deviceOptions . options ) { this . logger . info ( 'Initializing device: ' + deviceId ) ; this . logger . info ( 'new' , deviceOptions ) ; ps . push ( keepTrack ( this . _addDevice ( deviceId , deviceOptions ) , 'add_' + deviceId ) ) ; } } else { if ( deviceOptions . options ) { let anyChanged = false ; if ( ! _ . isEqual ( _ . omit ( oldDevice . deviceOptions , 'debug' ) , _ . omit ( deviceOptions , 'debug' ) ) ) { anyChanged = true ; } if ( anyChanged ) { deviceOptions . debug = this . getDeviceDebug ( orgDeviceOptions ) ; this . logger . info ( 'Re-initializing device: ' + deviceId ) ; this . logger . info ( 'old' , oldDevice . deviceOptions ) ; this . logger . info ( 'new' , deviceOptions ) ; ps . push ( keepTrack ( this . _removeDevice ( deviceId ) , 'remove_' + deviceId ) . then ( ( ) => { return keepTrack ( this . _addDevice ( deviceId , deviceOptions ) , 're-add_' + deviceId ) ; } ) ) ; } } } } for ( const oldDevice of this . tsr . getDevices ( ) ) { const deviceId = oldDevice . deviceId ; if ( ! devices . has ( deviceId ) ) { this . logger . info ( 'Un-initializing device: ' + deviceId ) ; ps . push ( keepTrack ( this . _removeDevice ( deviceId ) , 'remove_' + deviceId ) ) ; } } } await Promise . race ( [ Promise . all ( ps ) , new Promise < void > ( ( resolve ) => setTimeout ( ( ) => { const keys = _ . keys ( promiseOperations ) ; if ( keys . length ) { this . logger . warn ( `Timeout in _updateDevices: ${ keys . join ( ',' ) }` ) ; } resolve ( ) ; } , INIT_TIMEOUT ) ) , ] ) ; ps = [ ] ; for ( const device of this . tsr . getDevices ( ) ) { const deviceOptions = devices . get ( device . deviceId ) ; if ( deviceOptions ) { const debug = this . getDeviceDebug ( deviceOptions ) ; if ( device . debugLogging !== debug ) { this . logger . info ( `Setting logDebug of device ${ device . deviceId } to ${ debug }` ) ; ps . push ( device . setDebugLogging ( debug ) ) ; } } } await Promise . all ( ps ) ; this . _triggerupdateExpectedPlayoutItems ( ) ; this . logger . info ( 'updateDevices end' ) ; } private getDeviceDebug ( deviceOptions ) { return deviceOptions . debug || this . _coreHandler . logDebug || false ; } private async _addDevice ( deviceId , options ) { this . logger . debug ( 'Adding device ' + deviceId ) ; try { if ( this . _coreTsrHandlers [ deviceId ] ) { throw new Error ( `There is already a _coreTsrHandlers for deviceId "${ deviceId }"!` ) ; } const devicePr = this . tsr . createDevice ( deviceId , options ) ; const coreTsrHandler = new CoreTSRDeviceHandler ( this . _coreHandler , devicePr , deviceId , this ) ; this . _coreTsrHandlers [ deviceId ] = coreTsrHandler ; coreTsrHandler . statusChanged ( { statusCode : P . StatusCode . BAD , messages : [ 'Device initialising...' ] } ) ; const device = await devicePr ; const deviceType = device . deviceType ; const onDeviceStatusChanged = ( connectedOrStatus ) => { let deviceStatus ; if ( _ . isBoolean ( connectedOrStatus ) ) { if ( connectedOrStatus ) { deviceStatus = { statusCode : P . StatusCode . GOOD } ; } else { deviceStatus = { statusCode : P . StatusCode . BAD , messages : [ 'Disconnected' ] } ; } } else { deviceStatus = connectedOrStatus ; } coreTsrHandler . statusChanged ( deviceStatus ) ; if ( ( deviceStatus . statusCode === P . StatusCode . GOOD || deviceStatus . statusCode === P . StatusCode . WARNING_MINOR || deviceStatus . statusCode === P . StatusCode . WARNING_MAJOR ) && deviceType === DeviceType . ATEM && ! disableAtemUpload ) { const assets = ( options as DeviceOptionsAtem ) . options ?. mediaPoolAssets ; if ( assets && assets . length > 0 ) { try { this . uploadFilesToAtem ( device , assets . filter ( ( asset ) => _ . isNumber ( asset . position ) && asset . path ) ) ; } catch ( e ) { } } } } ; const onSlowSentCommand = ( info ) => { if ( info . internalDelay > 100 ) { this . logger . error ( 'slowSentCommand' , { deviceName : device . deviceName , ... info } ) ; } else { this . logger . warn ( 'slowSentCommand' , { deviceName : device . deviceName , ... info } ) ; } } ; const onSlowFulfilledCommand = ( info ) => { this . logger . warn ( 'slowFulfilledCommand' , { deviceName : device . deviceName , ... info } ) ; } ; const onCommandReport = ( commandReport ) => { if ( this . _reportAllCommands ) { this . logger . info ( 'commandReport' , { commandReport : commandReport } ) ; } } ; const onCommandError = ( error , context ) => { this . logger . error ( error ) ; this . logger . debug ( context ) ; } ; const onUpdateMediaObject = ( collectionId , docId , doc ) => { coreTsrHandler . onUpdateMediaObject ( collectionId , docId , doc ) ; } ; const onClearMediaObjectCollection = ( collectionId ) => { coreTsrHandler . onClearMediaObjectCollection ( collectionId ) ; } ; let deviceName = device . deviceName ; const deviceInstanceId = device . instanceId ; const fixError = ( e ) : string => { const name = `Device "${ deviceName || deviceId }" (${ deviceInstanceId })` ; if ( e . reason ) e . reason = name + ': ' + e . reason ; if ( e . message ) e . message = name + ': ' + e . message ; if ( e . stack ) { e . stack += '\nAt device' + name ; } if ( _ . isString ( e ) ) e = name + ': ' + e ; return e ; } ; await coreTsrHandler . init ( ) ; deviceName = device . deviceName ; device . onChildClose = ( ) => { this . logger . warn ( `Child of device ${ deviceId } closed/crashed` ) ; debug ( `Trigger update devices because "${ deviceId }" process closed` ) ; onDeviceStatusChanged ( { statusCode : P . StatusCode . BAD , messages : [ 'Child process closed' ] } ) ; this . _removeDevice ( deviceId ) . then ( ( ) => { this . _triggerUpdateDevices ( ) ; } , ( ) => { this . _triggerUpdateDevices ( ) ; } ) ; } ; await device . device . on ( 'connectionChanged' , onDeviceStatusChanged ) ; await device . device . on ( 'slowSentCommand' , onSlowSentCommand ) ; await device . device . on ( 'slowFulfilledCommand' , onSlowFulfilledCommand ) ; await device . device . on ( 'commandError' , onCommandError ) ; await device . device . on ( 'commandReport' , onCommandReport ) ; await device . device . on ( 'updateMediaObject' , onUpdateMediaObject ) ; await device . device . on ( 'clearMediaObjects' , onClearMediaObjectCollection ) ; await device . device . on ( 'info' , ( e , ... args ) => this . logger . info ( fixError ( e ) , ... args ) ) ; await device . device . on ( 'warning' , ( e , ... args ) => this . logger . warn ( fixError ( e ) , ... args ) ) ; await device . device . on ( 'error' , ( e , ... args ) => this . logger . error ( fixError ( e ) , ... args ) ) ; await device . device . on ( 'debug' , ( e , ... args ) => { if ( device . debugLogging && ! this . _coreHandler . logDebug ) { this . logger . info ( 'debug: ' + fixError ( e ) , ... args ) ; } } ) ; await this . tsr . initDevice ( deviceId , options ) ; onDeviceStatusChanged ( await device . device . getStatus ( ) ) ; } catch ( e ) { this . logger . error ( `Error when adding device "${ deviceId }"` , e ) ; debug ( `Error when adding device "${ deviceId }"` ) ; try { await this . _removeDevice ( deviceId ) ; } catch ( e ) { this . logger . error ( `Error when cleaning up after adding device "${ deviceId }" error...` , e ) ; } if ( ! this . _triggerUpdateDevicesTimeout ) { this . _triggerUpdateDevicesTimeout = setTimeout ( ( ) => { debug ( `Trigger updateDevices from failure "${ deviceId }"` ) ; this . _triggerUpdateDevices ( ) ; } , 10 * 1000 ) ; } } } /**
     * This function is a quick and dirty solution to load a still to the atem mixers.
     * This does not serve as a proper implementation! And need to be refactor
     * // @todo: proper atem media management
     * /Balte - 22-08
     */ private uploadFilesToAtem ( device , files ) { if ( device && device . deviceType === DeviceType . ATEM ) { this . logger . info ( 'try to load ' + JSON . stringify ( files . map ( ( f ) => f . path ) . join ( ', ' ) ) + ' to atem' ) ; const options = device . deviceOptions . options as { host : string ; } ; this . logger . info ( 'options ' + JSON . stringify ( options ) ) ; if ( options && options . host ) { this . logger . info ( 'uploading files to ' + options . host ) ; const process = cp . spawn ( `node` , [ `./dist/atemUploader.js` , options . host , JSON . stringify ( files ) ] ) ; process . stdout . on ( 'data' , ( data ) => this . logger . info ( data . toString ( ) ) ) ; process . stderr . on ( 'data' , ( data ) => this . logger . info ( data . toString ( ) ) ) ; process . on ( 'close' , ( ) => { process . removeAllListeners ( ) ; } ) ; } else { throw Error ( 'ATEM host option not set' ) ; } } } private async _removeDevice ( deviceId ) { if ( this . _coreTsrHandlers [ deviceId ] ) { try { await this . _coreTsrHandlers [ deviceId ] . dispose ( ) ; this . logger . debug ( 'Disposed device ' + deviceId ) ; } catch ( e ) { this . logger . error ( `Error when removing device "${ deviceId }"` , e ) ; } } delete this . _coreTsrHandlers [ deviceId ] ; } private _triggerupdateExpectedPlayoutItems ( ) { if ( ! this . _initialized ) return ; if ( this . _triggerupdateExpectedPlayoutItemsTimeout ) { clearTimeout ( this . _triggerupdateExpectedPlayoutItemsTimeout ) ; } this . _triggerupdateExpectedPlayoutItemsTimeout = setTimeout ( ( ) => { this . _updateExpectedPlayoutItems ( ) . catch ( ( e ) => this . logger . error ( 'Error in _updateExpectedPlayoutItems' , e ) ) ; } , 200 ) ; } private async _updateExpectedPlayoutItems ( ) { const expectedPlayoutItems = this . _coreHandler . core . getCollection ( 'expectedPlayoutItems' ) ; const peripheralDevice = this . _getPeripheralDevice ( ) ; const expectedItems = expectedPlayoutItems . find ( { studioId : peripheralDevice . studioId } ) ; const rundowns = _ . indexBy ( this . _coreHandler . core . getCollection ( 'rundowns' ) . find ( { studioId : peripheralDevice . studioId } ) , '_id' ) ; await Promise . all ( _ . map ( this . tsr . getDevices ( ) , async ( container ) => { if ( await container . device . supportsExpectedPlayoutItems ) { await container . device . handleExpectedPlayoutItems ( _ . map ( _ . filter ( expectedItems , ( item ) => { return ( item . deviceSubType === container . deviceType ) ; } ) , ( item ) => { const itemContent = item . content ; const newItem = { ... itemContent , rundownId : item . rundownId , playlistId : item . rundownId && rundowns [ item . rundownId ] ?. playlistId , baseline : item . baseline } ; return newItem ; } ) ) ; } } ) ) ; } param timeline private _transformTimeline ( timeline ) { const transformObject = ( obj ) : TimelineContentObjectTmp => { const transformedObj = clone ( _ . omit ( obj , [ '_id' , 'studioId' ] ) ) ; transformedObj . id = obj . id || obj . _id ; if ( ! transformedObj . content ) transformedObj . content = { } ; if ( transformedObj . isGroup ) { if ( ! transformedObj . content . objects ) transformedObj . content . objects = [ ] ; } return transformedObj ; } ; const objects = { } ; _ . each ( timeline , ( obj ) => { const transformedObj = transformObject ( obj ) ; objects [ transformedObj . id ] = transformedObj ; } ) ; const transformedTimeline = [ ] ; _ . each ( objects , ( obj ) => { if ( obj . inGroup ) { const groupObj = objects [ obj . inGroup ] ; if ( groupObj ) { if ( ! groupObj . children ) groupObj . children = [ ] ; if ( groupObj . children ) { delete obj . inGroup ; groupObj . children . push ( obj ) ; } } else { this . logger . error ( 'Referenced group "' + obj . inGroup + '" not found! Referenced by "' + obj . id + '"' ) ; } } else { delete obj . inGroup ; transformedTimeline . push ( obj as TSRTimelineObj ) ; } } ) ; return transformedTimeline ; } } export function getHash ( str ) { const hash = crypto . createHash ( 'sha1' ) ; return hash . update ( str ) . digest ( 'base64' ) . replace ( /[+/=]/g , '_' ) ; }