UNPKG

timeline-state-resolver

Version:
876 lines • 39.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Conductor = exports.AbortError = exports.Device = exports.MINTIMEUNIT = exports.MINTRIGGERTIME = exports.PREPARETIME = exports.LOOKAHEADTIME = exports.DeviceContainer = void 0; const _ = require("underscore"); const superfly_timeline_1 = require("superfly-timeline"); const eventemitter3_1 = require("eventemitter3"); const threadedclass_1 = require("threadedclass"); const p_queue_1 = require("p-queue"); const PAll = require("p-all"); const p_timeout_1 = require("p-timeout"); const timeline_state_resolver_types_1 = require("timeline-state-resolver-types"); const doOnTime_1 = require("./devices/doOnTime"); const lib_1 = require("./lib"); const deviceContainer_1 = require("./devices/deviceContainer"); Object.defineProperty(exports, "DeviceContainer", { enumerable: true, get: function () { return deviceContainer_1.DeviceContainer; } }); const ConnectionManager_1 = require("./service/ConnectionManager"); exports.LOOKAHEADTIME = 5000; // Will look ahead this far into the future exports.PREPARETIME = 2000; // Will prepare commands this time before the event is to happen exports.MINTRIGGERTIME = 10; // Minimum time between triggers exports.MINTIMEUNIT = 1; // Minimum unit of time /** When resolving and the timeline has repeating objects, only resolve this far into the future */ const RESOLVE_LIMIT_TIME = 10 * 1000; var device_1 = require("./devices/device"); Object.defineProperty(exports, "Device", { enumerable: true, get: function () { return device_1.Device; } }); const CALLBACK_WAIT_TIME = 50; class AbortError extends Error { constructor() { super(...arguments); this.name = 'AbortError'; } } exports.AbortError = AbortError; /** * The Conductor class serves as the main class for interacting. It contains * methods for setting mappings, timelines and adding/removing devices. It keeps * track of when to resolve the timeline and updates the devices with new states. */ class Conductor extends eventemitter3_1.EventEmitter { constructor(options = {}) { super(); this._logDebug = false; this._timeline = []; this._timelineSize = undefined; this._mappings = {}; this._datastore = {}; this._deviceStates = {}; this.connectionManager = new ConnectionManager_1.ConnectionManager(); this._nextResolveTime = 0; this._resolved = { resolvedTimeline: null, resolveTime: 0, validTo: 0, }; this._isInitialized = false; this._multiThreadedResolver = false; this._useCacheWhenResolving = false; this._estimateResolveTimeMultiplier = 1; this._callbackInstances = new Map(); // key = instanceId this._triggerSendStartStopCallbacksTimeout = null; this._sentCallbacks = {}; this._actionQueue = new p_queue_1.default({ concurrency: 1, }); this._statMeasureStart = 0; this._statMeasureReason = ''; this._statReports = []; this._options = options; this._multiThreadedResolver = !!options.multiThreadedResolver; this._useCacheWhenResolving = !!options.useCacheWhenResolving; this._estimateResolveTimeMultiplier = options.estimateResolveTimeMultiplier || 1; if (options.getCurrentTime) this._getCurrentTime = options.getCurrentTime; this._interval = setInterval(() => { if (this.timeline) { this._resolveTimeline(); } }, 2500); this._doOnTime = new doOnTime_1.DoOnTime(() => { return this.getCurrentTime(); }); this._doOnTime.on('error', (e) => this.emit('error', e)); // this._doOnTime.on('callback', (...args) => { // this.emit('timelineCallback', ...args) // }) if (options.autoInit) { this.init().catch((e) => { this.emit('error', 'Error during auto-init: ', e); }); } this.connectionManager.on('error', (e) => this.emit('error', e)); this.connectionManager.on('connectionEvent:resyncStates', (deviceId) => this.resyncDeviceStates(deviceId)); } /** * Initializates the resolver, with optional multithreading */ async init() { this._resolver = await (0, threadedclass_1.threadedClass)('../dist/AsyncResolver.js', 'AsyncResolver', [ (r) => { this.emit('setTimelineTriggerTime', r); }, ], { threadUsage: this._multiThreadedResolver ? 1 : 0, autoRestart: true, disableMultithreading: !this._multiThreadedResolver, instanceName: 'resolver', }); await this._resolver.on('error', (e) => { this.emit('error', 'AsyncResolver error: ' + e); }); threadedclass_1.ThreadedClassManager.onEvent(this._resolver, 'thread_closed', () => { // This is called if a child crashes - we are using autoRestart, so we just log this.emit('warning', 'AsyncResolver thread closed'); }); threadedclass_1.ThreadedClassManager.onEvent(this._resolver, 'restarted', () => { this.emit('warning', 'AsyncResolver thread restarted'); }); threadedclass_1.ThreadedClassManager.onEvent(this._resolver, 'error', (error) => { this.emit('error', 'AsyncResolver threadedClass error', error); }); this._isInitialized = true; this.resetResolver(); } /** * Returns a nice, synchronized time. */ getCurrentTime() { if (this._getCurrentTime) { return this._getCurrentTime(); } else { return Date.now(); } } /** * Returns the mappings */ get mapping() { return this._mappings; } /** * Returns the current timeline */ get timeline() { return this._timeline; } /** * Sets a new timeline and resets the resolver. */ setTimelineAndMappings(timeline, mappings) { this.statStartMeasure('timeline received'); this._timeline = timeline; this._timelineSize = undefined; // reset the cache if (mappings) this._mappings = mappings; // We've got a new timeline, anything could've happened at this point // Highest priority right now is to determine if any commands have to be sent RIGHT NOW // After that, we'll move further ahead in time, creating commands ready for scheduling this.resetResolver(); } get timelineHash() { return this._timelineHash; } set timelineHash(hash) { this._timelineHash = hash; } get logDebug() { return this._logDebug; } set logDebug(val) { this._logDebug = val; threadedclass_1.ThreadedClassManager.debug = this._logDebug; } get estimateResolveTimeMultiplier() { return this._estimateResolveTimeMultiplier; } set estimateResolveTimeMultiplier(value) { this._estimateResolveTimeMultiplier = value; } /** * Remove all connections */ async destroy() { clearTimeout(this._interval); if (this._triggerSendStartStopCallbacksTimeout) clearTimeout(this._triggerSendStartStopCallbacksTimeout); // remove all connections: this.connectionManager.setConnections({}); } /** * Resets the resolve-time, so that the resolving will happen for the point-in time NOW * next time */ resetResolver() { // reset the resolver through the action queue to make sure it is reset after any currently running timelineResolves this._actionQueue .add(async () => { this._nextResolveTime = 0; // This will cause _resolveTimeline() to generate the state for NOW this._resolved = { resolvedTimeline: null, resolveTime: 0, validTo: 0, }; }) .catch(() => { this.emit('error', 'Failed to reset the ResolvedTimeline, timeline may not be updated appropriately!'); }); this._triggerResolveTimeline(); } /** * Send a makeReady-trigger to all devices * * @deprecated replace by TSR actions */ async devicesMakeReady(okToDestroyStuff, activationId) { this.activationId = activationId; this.emit('debug', `devicesMakeReady, ${okToDestroyStuff ? 'okToDestroyStuff' : 'undefined'}, ${activationId ? activationId : 'undefined'}`); await this._actionQueue.add(async () => { await this._mapAllConnections(false, async (d) => (0, p_timeout_1.default)((async () => { const trace = (0, lib_1.startTrace)('conductor:makeReady:' + d.deviceId); await d.device.makeReady(okToDestroyStuff, activationId); this.emit('timeTrace', (0, lib_1.endTrace)(trace)); })(), 10000, `makeReady for "${d.deviceId}" timed out`)); this._triggerResolveTimeline(); }); } /** * Send a standDown-trigger to all devices * * @deprecated replaced by TSR actions */ async devicesStandDown(okToDestroyStuff) { this.activationId = undefined; this.emit('debug', `devicesStandDown, ${okToDestroyStuff ? 'okToDestroyStuff' : 'undefined'}`); await this._actionQueue.add(async () => { await this._mapAllConnections(false, async (d) => (0, p_timeout_1.default)((async () => { const trace = (0, lib_1.startTrace)('conductor:standDown:' + d.deviceId); await d.device.standDown(okToDestroyStuff); this.emit('timeTrace', (0, lib_1.endTrace)(trace)); })(), 10000, `standDown for "${d.deviceId}" timed out`)); }); } async getThreadsMemoryUsage() { return threadedclass_1.ThreadedClassManager.getThreadsMemoryUsage(); } async _mapAllConnections(includeUninitialized, fcn) { return PAll(this.connectionManager.getConnections(includeUninitialized).map((d) => async () => fcn(d)), { stopOnError: false, }); } /** * This is the main resolve-loop. */ _triggerResolveTimeline(timeUntilTrigger) { // this.emit('info', '_triggerResolveTimeline', timeUntilTrigger) if (this._resolveTimelineTrigger) { clearTimeout(this._resolveTimelineTrigger); delete this._resolveTimelineTrigger; } if (timeUntilTrigger) { // resolve at a later stage this._resolveTimelineTrigger = setTimeout(() => { this._resolveTimeline(); }, timeUntilTrigger); } else { // resolve right away: this._resolveTimeline(); } } /** * Resolves the timeline for the next resolve-time, generates the commands and passes on the commands. */ _resolveTimeline() { // this adds it to a queue, make sure it never runs more than once at a time: this._actionQueue .add(async () => { return this._resolveTimelineInner() .then((nextResolveTime) => { this._nextResolveTime = nextResolveTime ?? 0; }) .catch((e) => { this.emit('error', 'Caught error in _resolveTimelineInner' + e); }); }) .catch((e) => { this.emit('error', 'Caught error in _resolveTimeline.then' + e); }); } async _resolveTimelineInner() { const trace = (0, lib_1.startTrace)('conductor:resolveTimeline'); if (!this._isInitialized) { this.emit('warning', 'TSR is not initialized yet'); return undefined; } let nextResolveTime = 0; let timeUntilNextResolve = exports.LOOKAHEADTIME; const startTime = Date.now(); const statMeasureStart = this._statMeasureStart; let statTimeStateHandled = -1; let statTimeTimelineStartResolve = -1; let statTimeTimelineResolved = -1; let estimatedResolveTime = -1; try { /** The point in time this function is run. ( ie "right now") */ const now = this.getCurrentTime(); /** The point in time we're targeting. (This can be in the future) */ let resolveTime = this._nextResolveTime; estimatedResolveTime = this.estimateResolveTime(); if (resolveTime === 0 || // About to be resolved ASAP resolveTime < now + estimatedResolveTime // We're late ) { // Set resolveTime to the earliest point in the future we can reasonable achieve it: resolveTime = now + estimatedResolveTime; this.emitWhenActive('debug', `resolveTimeline ${resolveTime} (${resolveTime - now} from now) (${estimatedResolveTime}) ---------`); } else { this.emitWhenActive('debug', `resolveTimeline ${resolveTime} (${resolveTime - now} from now) -----------------------------`); if (resolveTime > now + exports.LOOKAHEADTIME) { // If the resolveTime is too far ahead, we'd rather wait and resolve it later. this.emitWhenActive('debug', 'Too far ahead (' + resolveTime + ')'); this._triggerResolveTimeline(exports.LOOKAHEADTIME); return undefined; } } // Let all initialized devices know that a new state is about to come in. // This is done so that they can clear future commands a bit earlier, possibly avoiding double or conflicting commands // const pPrepareForHandleStates = this._mapAllDevices(async (device: DeviceContainer) => { // await device.device.prepareForHandleState(resolveTime) // }).catch(error => { // this.emit('error', error) // }) // TODO - the PAll way of doing this provokes https://github.com/Sofie-Automation/sofie-timeline-state-resolver/pull/139 // The doOnTime calls fire before this, meaning we cleanup the state for a time we have already sent commands for const pPrepareForHandleStates = Promise.all(this.connectionManager .getConnections(false) .map(async (device) => { await device.device.prepareForHandleState(resolveTime); })).catch((error) => { this.emit('error', error); }); const applyRecursively = (o, func) => { func(o); if (o.isGroup) { _.each(o.children || [], (child) => { applyRecursively(child, func); }); } }; statTimeTimelineStartResolve = Date.now(); const timeline = this.timeline; // To prevent trying to transfer circular references over IPC we remove // any references to the parent property: const deleteParent = (o) => { if ('parent' in o) { delete o['parent']; } }; _.each(timeline, (o) => applyRecursively(o, deleteParent)); // Determine if we can use the pre-resolved timeline: let resolvedTimeline; if (this._resolved.resolvedTimeline && resolveTime >= this._resolved.resolveTime && // if we have less than PREPARETIME left of the valid time, we should re-resolve so we are prepared for what comes after resolveTime < this._resolved.validTo - exports.PREPARETIME) { // Yes, we can use the previously resolved timeline: resolvedTimeline = this._resolved.resolvedTimeline; } else { // No, we need to resolve the timeline again: const o = await this._resolver.resolveTimeline(resolveTime, timeline, resolveTime + RESOLVE_LIMIT_TIME, this._useCacheWhenResolving); resolvedTimeline = o.resolvedTimeline; this._resolved.resolvedTimeline = resolvedTimeline; this._resolved.resolveTime = resolveTime; this._resolved.validTo = resolveTime + RESOLVE_LIMIT_TIME - 0; // Ensure we re-resolve the timeline before it expires // Apply changes to fixed objects (set "now" triggers to an actual time): // This gets persisted on this.timeline, so we only have to do this once const nowIdsTime = {}; _.each(o.objectsFixed, (o) => (nowIdsTime[o.id] = o.time)); const fixNow = (o) => { if (nowIdsTime[o.id]) { if (!_.isArray(o.enable)) { o.enable.start = nowIdsTime[o.id]; } } }; _.each(timeline, (o) => applyRecursively(o, fixNow)); } let tlState; try { tlState = (0, superfly_timeline_1.getResolvedState)(resolvedTimeline, resolveTime, 1); } catch (e) { // Trace some helpful information related to the error: this.emit('error', 'Error resolveTrace: ' + JSON.stringify(resolvedTimeline.statistics.resolveTrace)); throw e; } await pPrepareForHandleStates; statTimeTimelineResolved = Date.now(); if (this.getCurrentTime() > resolveTime) { this.emit('warning', `Resolver is ${this.getCurrentTime() - resolveTime} ms late (estimatedResolveTime was ${estimatedResolveTime})`); } const layersPerDevice = this.filterLayersPerDevice(tlState.layers, this.connectionManager.getConnections(false)); // Push state to the right device: await this._mapAllConnections(false, async (device) => { if (this._options.optimizeForProduction) { // Don't send any state to the abstract device, since it doesn't do anything anyway if (device.deviceType === timeline_state_resolver_types_1.DeviceType.ABSTRACT) return; } // The subState contains only the parts of the state relevant to that device: const subState = { time: tlState.time, layers: layersPerDevice[device.deviceId] || {}, nextEvents: [], }; // Pass along the state to the device, it will generate its commands and execute them: try { // await device.device.handleState(removeParentFromState(subState), this._mappings) await this._setDeviceState(device.deviceId, tlState.time, removeParentFromState(subState), this._mappings); } catch (e) { this.emit('error', 'Error in device "' + device.deviceId + '"' + e + ' ' + e.stack); } }); statTimeStateHandled = Date.now(); // Now that we've handled this point in time, it's time to determine what the next point in time is: const nextEventTime = tlState.nextEvents[0]?.time; if (!nextEventTime && tlState.time < this._resolved.validTo) { // There's nothing ahead in the timeline (as far as we can see, ref: this._resolved.validTo) // Tell the devices that the future is clear: await this._mapAllConnections(true, async (device) => { try { await device.device.clearFuture(tlState.time); } catch (e) { this.emit('error', 'Error in device "' + device.deviceId + '", clearFuture: ' + e + ' ' + e.stack); } }); } const nowPostExec = this.getCurrentTime(); /** Time left until the next event in the timeline */ const timeToNextEventTime = nextEventTime ? nextEventTime - nowPostExec : Infinity; /** Time left until the timeline needs to ge re-resolved */ const timeLeftValidTimeline = this._resolved.validTo ? this._resolved.validTo - nowPostExec : Infinity; timeUntilNextResolve = Math.max(exports.MINTRIGGERTIME, // At minimum, we should wait this time Math.min(exports.LOOKAHEADTIME, // We should wait maximum this time, because we might have deferred a resolving this far ahead timeLeftValidTimeline - exports.PREPARETIME, timeToNextEventTime - exports.PREPARETIME)); if (nextEventTime) { // resolve at nextEventTime next time: nextResolveTime = Math.min( // Resolve at next timeline event: nextEventTime, // Ensure that we don't resolve too far ahead: tlState.time + exports.LOOKAHEADTIME); } else { // resolve at this time then next time (or later): nextResolveTime = tlState.time; } // Special function: send callback to Core this._doOnTime.clearQueueNowAndAfter(tlState.time); const activeObjects = {}; _.each(tlState.layers, (instance) => { try { if (instance.content.callBack || instance.content.callBackStopped) { const callBackId = instance.id + instance.content.callBack + instance.content.callBackStopped + (instance.instance.originalStart ?? instance.instance.start) + JSON.stringify(instance.content.callBackData); activeObjects[callBackId] = { time: instance.instance.start || 0, id: instance.id, callBack: instance.content.callBack, callBackStopped: instance.content.callBackStopped, callBackData: instance.content.callBackData, startTime: instance.instance.start, }; } } catch (e) { this.emit('error', `callback to core, obj "${instance.id}"`, e); } }); this._doOnTime.queue(tlState.time, undefined, (sentCallbacksNew) => { this._diffStateForCallbacks(sentCallbacksNew, tlState.time); }, activeObjects); const resolveDuration = Date.now() - startTime; // Special / hack: report back, for latency statitics: if (this._timelineHash) { this.emit('resolveDone', this._timelineHash, resolveDuration); } this.emitWhenActive('debug', 'resolveTimeline at time ' + resolveTime + ' done in ' + resolveDuration + 'ms (size: ' + timeline.length + ')'); } catch (e) { this.emit('error', 'resolveTimeline' + e + '\nStack: ' + e.stack); } // Report time taken to resolve this.emit('timeTrace', (0, lib_1.endTrace)(trace)); this.statReport(statMeasureStart, { timelineStartResolve: statTimeTimelineStartResolve, timelineSize: this.getTimelineSize(), timelineSizeOld: this._timeline.length, timelineResolved: statTimeTimelineResolved, stateHandled: statTimeStateHandled, done: Date.now(), estimatedResolveTime: estimatedResolveTime, }); // Try to trigger the next resolval try { this._triggerResolveTimeline(timeUntilNextResolve); } catch (e) { this.emit('error', 'triggerResolveTimeline', e); } return nextResolveTime; } async _setDeviceState(deviceId, time, state, unfilteredMappings) { // only take mappings that are for this deviceId const mappings = Object.fromEntries(Object.entries(unfilteredMappings).filter(([_, mapping]) => mapping.deviceId === deviceId)); if (!this._deviceStates[deviceId]) this._deviceStates[deviceId] = []; // find all references to the datastore that are in this state const dependenciesSet = new Set(); for (const { content } of Object.values(state.layers)) { const dataStoreContent = content; for (const r of Object.values(dataStoreContent.$references || {})) { dependenciesSet.add(r.datastoreKey); } } const dependencies = Array.from(dependenciesSet); // store all states between the current state and the new state this._deviceStates[deviceId] = _.compact([ this._deviceStates[deviceId].reverse().find((s) => s.time <= this.getCurrentTime()), ...this._deviceStates[deviceId] .reverse() .filter((s) => s.time < time && s.time > this.getCurrentTime()) .reverse(), { time, state, dependencies, mappings, }, ]); // replace references to the timeline datastore with the actual values const filledState = (0, lib_1.fillStateFromDatastore)(state, this._datastore); // send the filled state to the device handler return this.connectionManager.getConnection(deviceId)?.device.handleState(filledState, mappings); } setDatastore(newStore) { this._actionQueue .add(() => { const allKeys = new Set([...Object.keys(newStore), ...Object.keys(this._datastore)]); const affectedDevices = []; for (const key of allKeys) { if (this._datastore[key]?.value !== newStore[key]?.value) { // it changed! let's sift through our dependencies to see if we need to do anything Object.entries(this._deviceStates).forEach(([deviceId, states]) => { if (states.find((state) => state.dependencies.find((dep) => dep === key))) { affectedDevices.push(deviceId); } }); } } this._datastore = newStore; for (const deviceId of affectedDevices) { const toBeFilled = _.compact([ // shallow clone so we don't reverse the array in place [...this._deviceStates[deviceId]].reverse().find((s) => s.time <= this.getCurrentTime()), ...this._deviceStates[deviceId].filter((s) => s.time > this.getCurrentTime()), // all states after now ]); for (const s of toBeFilled) { const filledState = (0, lib_1.fillStateFromDatastore)(s.state, this._datastore); this.connectionManager .getConnection(deviceId) ?.device.handleState(filledState, s.mappings) .catch((e) => this.emit('error', 'resolveTimeline' + e + '\nStack: ' + e.stack)); } } }) .catch((e) => { this.emit('error', 'Caught error in setDatastore' + e); }); } resyncDeviceStates(deviceId) { this._actionQueue .add(() => { const toBeFilled = _.compact([ // shallow clone so we don't reverse the array in place [...this._deviceStates[deviceId]].reverse().find((s) => s.time <= this.getCurrentTime()), ...this._deviceStates[deviceId].filter((s) => s.time > this.getCurrentTime()), // all states after now ]); for (const s of toBeFilled) { const filledState = (0, lib_1.fillStateFromDatastore)(s.state, this._datastore); this.connectionManager .getConnection(deviceId) ?.device.handleState(filledState, s.mappings) .catch((e) => this.emit('error', 'resolveTimeline' + e + '\nStack: ' + e.stack)); } }) .catch((e) => { this.emit('error', 'Caught error in resyncDeviceStates' + e); }); } getTimelineSize() { if (this._timelineSize === undefined) { // Update the cache: this._timelineSize = this.getTimelineSizeInner(this._timeline); } return this._timelineSize; } getTimelineSizeInner(timelineObjects) { let size = 0; size += timelineObjects.length; for (const obj of timelineObjects) { if (obj.children) { size += this.getTimelineSizeInner(obj.children); } if (obj.keyframes) { size += obj.keyframes.length; } } return size; } /** * Returns a time estimate for the resolval duration based on the amount of * objects on the timeline. If the proActiveResolve option is falsy this * returns 0. */ estimateResolveTime() { if (this._options.proActiveResolve) { const objectCount = this.getTimelineSize(); return Conductor.calculateResolveTime(objectCount, this._estimateResolveTimeMultiplier); } else { return 0; } } /** Calculates the estimated time it'll take to resolve a timeline of a certain size */ static calculateResolveTime(timelineSize, multiplier) { // Note: The LEVEL should really be a dynamic value, to reflect the actual performance of the hardware this is running on. const BASE_VALUE = 0; const LEVEL = 250; const EXPONENT = 0.7; const MIN_VALUE = 20; const MAX_VALUE = 200; const sizeFactor = Math.pow(timelineSize / LEVEL, EXPONENT) * LEVEL * 0.5; // a pretty nice-looking graph that levels out when objectCount is larger return (multiplier * Math.max(MIN_VALUE, Math.min(MAX_VALUE, Math.floor(BASE_VALUE + sizeFactor // add ms for every object (ish) in timeline )))); } _diffStateForCallbacks(activeObjects, tlTime) { // Clear callbacks scheduled after the current tlState for (const [callbackId, o] of Object.entries(this._sentCallbacks)) { if (o.time >= tlTime) { delete this._sentCallbacks[callbackId]; } } // Send callbacks for playing objects: for (const [callbackId, cb] of Object.entries(activeObjects)) { if (cb.callBack && cb.startTime) { if (!this._sentCallbacks[callbackId]) { // Object has started playing this._queueCallback(true, { type: 'start', time: cb.startTime, instanceId: cb.id, callBack: cb.callBack, callBackData: cb.callBackData, }); } else { // callback already sent, do nothing } } } // Send callbacks for stopped objects for (const [callbackId, cb] of Object.entries(this._sentCallbacks)) { if (cb.callBackStopped && !activeObjects[callbackId]) { // Object has stopped playing this._queueCallback(false, { type: 'stop', time: tlTime, instanceId: cb.id, callBack: cb.callBackStopped, callBackData: cb.callBackData, }); } } this._sentCallbacks = activeObjects; } _queueCallback(playing, cb) { let o; if (this._callbackInstances.has(cb.instanceId)) { o = this._callbackInstances.get(cb.instanceId); } else { o = { playing: undefined, playChanged: false, endChanged: false, }; this._callbackInstances.set(cb.instanceId, o); } if (o.playing !== playing) { this.emitWhenActive('debug', `_queueCallback ${playing ? 'playing' : 'stopping'} instance ${cb.instanceId}`); if (playing) { if (o.endChanged && o.endTime && Math.abs(cb.time - o.endTime) < CALLBACK_WAIT_TIME) { // Too little time has passed since last time. Annihilate that event instead: o.playing = playing; o.endTime = undefined; o.endCallback = undefined; o.endChanged = false; } else { o.playing = playing; o.playChanged = true; o.playTime = cb.time; o.playCallback = cb; } } else { if (o.playChanged && o.playTime && Math.abs(cb.time - o.playTime) < CALLBACK_WAIT_TIME) { // Too little time has passed since last time. Annihilate that event instead: o.playing = playing; o.playTime = undefined; o.playCallback = undefined; o.playChanged = false; } else { o.playing = playing; o.endChanged = true; o.endTime = cb.time; o.endCallback = cb; } } } else { this.emit('warning', `_queueCallback ${playing ? 'playing' : 'stopping'} instance ${cb.instanceId} already playing/stopped`); } this._triggerSendStartStopCallbacks(); } _triggerSendStartStopCallbacks() { if (!this._triggerSendStartStopCallbacksTimeout) { this._triggerSendStartStopCallbacksTimeout = setTimeout(() => { this._triggerSendStartStopCallbacksTimeout = null; this._sendStartStopCallbacks(); }, CALLBACK_WAIT_TIME); } } _sendStartStopCallbacks() { const now = this.getCurrentTime(); let haveThingsToSendLater = false; const callbacks = []; for (const [instanceId, o] of this._callbackInstances.entries()) { if (o.endChanged && o.endTime && o.endCallback) { if (o.endTime < now - CALLBACK_WAIT_TIME) { callbacks.push(o.endCallback); o.endChanged = false; } else { haveThingsToSendLater = true; } } if (o.playChanged && o.playTime && o.playCallback) { if (o.playTime < now - CALLBACK_WAIT_TIME) { callbacks.push(o.playCallback); o.playChanged = false; } else { haveThingsToSendLater = true; } } if (!haveThingsToSendLater && !o.playChanged && !o.endChanged) { this._callbackInstances.delete(instanceId); } } // Sort the callbacks: const callbacksArray = callbacks.sort((a, b) => { if (a.type === 'start' && b.type !== 'start') return 1; if (a.type !== 'start' && b.type === 'start') return -1; if ((a.time || 0) > (b.time || 0)) return 1; if ((a.time || 0) < (b.time || 0)) return -1; return 0; }); // emit callbacks _.each(callbacksArray, (cb) => { this.emit('timelineCallback', cb.time, cb.instanceId, cb.callBack, cb.callBackData); }); if (haveThingsToSendLater) { this._triggerSendStartStopCallbacks(); } } statStartMeasure(reason) { // Start a measure of response times if (!this._statMeasureStart) { this._statMeasureStart = Date.now(); this._statMeasureReason = reason; } } statReport(startTime, report) { // Check if the report is from the start of a measuring if (this._statMeasureStart && this._statMeasureStart === startTime) { // Save the report: const reportDuration = { reason: this._statMeasureReason, timelineStartResolve: report.timelineStartResolve - startTime, timelineResolved: report.timelineResolved - startTime, stateHandled: report.stateHandled - startTime, done: report.done - startTime, timelineSize: report.timelineSize, timelineSizeOld: report.timelineSizeOld, estimatedResolveTime: report.estimatedResolveTime, }; this._statReports.push(reportDuration); this._statMeasureStart = 0; this._statMeasureReason = ''; this.emit('debug', 'statReport', JSON.stringify(reportDuration)); this.emit('statReport', reportDuration); } } /** * Split the state into substates that are relevant for each device */ filterLayersPerDevice(layers, devices) { const filteredStates = {}; const deviceIdAndTypes = {}; _.each(devices, (device) => { deviceIdAndTypes[device.deviceId + '__' + device.deviceType] = device.deviceId; }); _.each(layers, (o, layerId) => { const oExt = o; let mapping = this._mappings[o.layer + '']; if (!mapping && oExt.isLookahead && oExt.lookaheadForLayer) { mapping = this._mappings[oExt.lookaheadForLayer]; } if (mapping) { const deviceIdAndType = mapping.deviceId + '__' + mapping.device; if (deviceIdAndTypes[deviceIdAndType]) { if (!filteredStates[mapping.deviceId]) { filteredStates[mapping.deviceId] = {}; } filteredStates[mapping.deviceId][layerId] = o; } } }); return filteredStates; } /** * Only emits the event when there is an active rundownPlaylist. * This is used to reduce unnesessary logging */ emitWhenActive(eventType, ...args) { if (this.activationId) { this.emit(eventType, ...args); } } } exports.Conductor = Conductor; function removeParentFromState(o) { for (const key in o) { if (key === 'parent') { delete o['parent']; } else if (typeof o[key] === 'object') { o[key] = removeParentFromState(o[key]); } } return o; } //# sourceMappingURL=conductor.js.map