timeline-state-resolver
Version:
Have timeline, control stuff
1,122 lines • 48.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.VizMSEManager = exports.getHash = void 0;
const _ = require("underscore");
const events_1 = require("events");
const timeline_state_resolver_types_1 = require("timeline-state-resolver-types");
const got_1 = require("got");
const lib_1 = require("../../lib");
const types_1 = require("./types");
const vizEngineTcpSender_1 = require("./vizEngineTcpSender");
const crypto = require("crypto");
const path = require("path");
const waitGroup_1 = require("../../waitGroup");
/** Minimum time to wait before removing an element after an expectedPlayoutItem has been removed */
const DELETE_TIME_WAIT = 20 * 1000;
// How often to check / preload elements
const MONITOR_INTERVAL = 5 * 1000;
// How long to wait after any action (takes, cues, etc) before trying to cue for preloading
const SAFE_PRELOAD_TIME = 2000;
// How long to wait before retrying to ping the MSE when initializing the rundown, after a failed attempt
const INIT_RETRY_INTERVAL = 3000;
// Appears at the end of show names in the directory
const SHOW_EXTENSION = '.show';
function getHash(str) {
const hash = crypto.createHash('sha1');
return hash.update(str).digest('base64').replace(/[+/=]/g, '_'); // remove +/= from strings, because they cause troubles
}
exports.getHash = getHash;
class VizMSEManager extends events_1.EventEmitter {
get activeRundownPlaylistId() {
return this._activeRundownPlaylistId;
}
constructor(_parentVizMSEDevice, _vizMSE, preloadAllElements, onlyPreloadActivePlaylist, purgeUnknownElements, autoLoadInternalElements, engineRestPort, _showDirectoryPath, _profile, _playlistID) {
super();
this._parentVizMSEDevice = _parentVizMSEDevice;
this._vizMSE = _vizMSE;
this.preloadAllElements = preloadAllElements;
this.onlyPreloadActivePlaylist = onlyPreloadActivePlaylist;
this.purgeUnknownElements = purgeUnknownElements;
this.autoLoadInternalElements = autoLoadInternalElements;
this.engineRestPort = engineRestPort;
this._showDirectoryPath = _showDirectoryPath;
this._profile = _profile;
this._playlistID = _playlistID;
this.initialized = false;
this.notLoadedCount = 0;
this.loadingCount = 0;
this.enginesDisconnected = [];
this._elementCache = {};
this._expectedPlayoutItems = [];
this._lastTimeCommandSent = 0;
this._hasActiveRundown = false;
this._mseConnected = undefined; // undefined: first connection not established yet
this._msePingConnected = false;
this._loadingAllElements = false;
this._waitWithLayers = new waitGroup_1.WaitGroup();
this.ignoreAllWaits = false; // Only to be used in tests
this._terminated = false;
this._updateAfterReconnect = false;
this._initializedShows = new Set();
}
/**
* Initialize the Rundown in MSE.
* Our approach is to create a single rundown on initialization, and then use only that for later control.
*/
async initializeRundown(activeRundownPlaylistId) {
this._vizMSE.on('connected', () => this.mseConnectionChanged(true));
this._vizMSE.on('disconnected', () => this.mseConnectionChanged(false));
this._vizMSE.on('warning', (message) => this.emit('warning', 'v-connection: ' + message));
this._activeRundownPlaylistId = activeRundownPlaylistId;
this._preloadedRundownPlaylistId = this.onlyPreloadActivePlaylist ? activeRundownPlaylistId : undefined;
if (activeRundownPlaylistId) {
this.emit('debug', `VizMSE: already active playlist: ${this._preloadedRundownPlaylistId}`);
}
const initializeRundownInner = async () => {
try {
// Perform a ping, to ensure we are connected properly
await this._vizMSE.ping();
this._msePingConnected = true;
this.mseConnectionChanged(true);
// Setup the rundown used by this device:
const rundown = await this._getRundown();
if (!rundown)
throw new Error(`VizMSEManager: Unable to create rundown!`);
this._showToIdMap = await this._vizMSE.listShowsFromDirectory();
}
catch (e) {
this.emit('debug', `VizMSE: initializeRundownInner ${e}`);
setTimeout(() => {
(0, lib_1.deferAsync)(async () => initializeRundownInner(), (_e) => {
// ignore error
});
}, INIT_RETRY_INTERVAL);
return;
}
// const profile = await this._vizMSE.getProfile('sofie') // TODO: Figure out if this is needed
this._setMonitorLoadedElementsTimeout();
this._setMonitorConnectionTimeout();
this.initialized = true;
};
await initializeRundownInner();
}
/**
* Close connections and die
*/
async terminate() {
this._terminated = true;
if (this._monitorAndLoadElementsTimeout) {
clearTimeout(this._monitorAndLoadElementsTimeout);
}
if (this._monitorMSEConnectionTimeout) {
clearTimeout(this._monitorMSEConnectionTimeout);
}
if (this._vizMSE) {
await this._vizMSE.close();
}
}
/**
* Set the collection of expectedPlayoutItems.
* These will be monitored and can be triggered to pre-load.
*/
setExpectedPlayoutItems(expectedPlayoutItems) {
this.emit('debug', 'VIZDEBUG: setExpectedPlayoutItems called');
if (this.preloadAllElements) {
this.emit('debug', 'VIZDEBUG: preload elements allowed');
this._expectedPlayoutItems = expectedPlayoutItems;
this._prepareAndGetExpectedPlayoutItems() // Calling this in order to trigger creation of all elements
.then(async (hashesAndItems) => {
if (this._rundown && this._hasActiveRundown) {
this.emit('debug', 'VIZDEBUG: auto load internal elements...');
await this.updateElementsLoadedStatus();
const elementHashesToDelete = [];
// When a new element is added, we'll trigger a show init:
const showIdsToInitialize = new Set();
_.each(this._elementCache, (element) => {
if ((0, types_1.isVizMSEPlayoutItemContentInternalInstance)(element.content)) {
if (!element.isLoaded && !element.requestedLoading) {
this.emit('debug', `Element "${this._getElementReference(element.element)}" is not loaded`);
if (this.autoLoadInternalElements || this._initializedShows.has(element.content.showId)) {
showIdsToInitialize.add(element.content.showId);
element.requestedLoading = true;
}
}
}
if (!hashesAndItems[element.hash] && !element.toDelete) {
elementHashesToDelete.push(element.hash);
this._elementCache[element.hash].toDelete = true;
}
});
const uniqueShowIds = Array.from(showIdsToInitialize);
await this._initializeShows(uniqueShowIds);
setTimeout(() => {
Promise.all(elementHashesToDelete.map(async (elementHash) => {
const element = this._elementCache[elementHash];
if (element?.toDelete) {
await this._deleteElement(element.content);
delete this._elementCache[elementHash];
}
})).catch((error) => this.emit('error', error));
}, DELETE_TIME_WAIT);
}
})
.catch((error) => this.emit('error', error));
}
}
async purgeRundown(clearAll) {
this.emit('debug', `VizMSE: purging rundown (manually)`);
const rundown = await this._getRundown();
const elementsToKeep = clearAll ? undefined : this.getElementsToKeep();
await rundown.purgeExternalElements(elementsToKeep || []);
}
/**
* Activate the rundown.
* This causes the MSE rundown to activate, which must be done before using it.
* Doing this will make MSE start loading things onto the vizEngine etc.
*/
async activate(rundownPlaylistId) {
this._preloadedRundownPlaylistId = this.onlyPreloadActivePlaylist ? rundownPlaylistId : undefined;
let loadTwice = false;
if (!rundownPlaylistId || this._activeRundownPlaylistId !== rundownPlaylistId) {
this._triggerCommandSent();
const rundown = await this._getRundown();
// clear any existing elements from the existing rundown
try {
this.emit('debug', `VizMSE: purging rundown`);
const elementsToKeep = this.getElementsToKeep();
await rundown.purgeExternalElements(elementsToKeep);
}
catch (error) {
this.emit('error', error);
}
this._clearCache();
this._clearMediaObjects();
loadTwice = true;
}
this._triggerCommandSent();
this._triggerLoadAllElements(loadTwice)
.then(async () => {
this._triggerCommandSent();
this._activeRundownPlaylistId = rundownPlaylistId;
this._hasActiveRundown = true;
if (this.purgeUnknownElements) {
const rundown = await this._getRundown();
const elementsInRundown = await rundown.listExternalElements();
const hashesAndItems = await this._prepareAndGetExpectedPlayoutItems();
for (const element of elementsInRundown) {
// Check if that element is in our expectedPlayoutItems list
if (!hashesAndItems[VizMSEManager._getElementHash(element)]) {
// The element in the Viz-rundown seems to be unknown to us
await rundown.deleteElement(element);
}
}
}
})
.catch((e) => {
this.emit('error', e);
});
}
/**
* Deactivate the MSE rundown.
* This causes the MSE to stand down and clear the vizEngines of any loaded graphics.
*/
async deactivate() {
const rundown = await this._getRundown();
this._triggerCommandSent();
await rundown.deactivate();
this._triggerCommandSent();
this.standDownActiveRundown();
this._clearMediaObjects();
}
standDownActiveRundown() {
this._hasActiveRundown = false;
this._activeRundownPlaylistId = undefined;
}
_clearMediaObjects() {
this.emit('clearMediaObjects');
}
/**
* Prepare an element
* This creates the element and is intended to be called a little time ahead of Takeing the element.
*/
async prepareElement(cmd) {
this.logCommand(cmd, 'prepare');
this._triggerCommandSent();
await this._checkPrepareElement(cmd.content, true);
this._triggerCommandSent();
}
/**
* Cue:ing an element: Load and play the first frame of a graphic
*/
async cueElement(cmd) {
const rundown = await this._getRundown();
await this._checkPrepareElement(cmd.content);
await this._checkElementExists(cmd);
await this._handleRetry(async () => {
this.logCommand(cmd, 'cue');
return rundown.cue(cmd.content);
});
}
logCommand(cmd, commandName) {
const content = cmd.content;
if ((0, types_1.isVizMSEPlayoutItemContentInternalInstance)(content)) {
this.emit('debug', `VizMSE: ${commandName} "${content.instanceName}" in show "${content.showId}"`);
}
else {
this.emit('debug', `VizMSE: ${commandName} "${content.vcpid}" on channel "${content.channel}"`);
}
}
/**
* Take an element: Load and Play a graphic element, run in-animatinos etc
*/
async takeElement(cmd) {
const rundown = await this._getRundown();
await this._checkPrepareElement(cmd.content);
if (cmd.transition) {
if (cmd.transition.type === timeline_state_resolver_types_1.VIZMSETransitionType.DELAY) {
if (await this.waitWithLayer(cmd.layerId || '__default', cmd.transition.delay)) {
// at this point, the wait aws aborted by someone else. Do nothing then.
return;
}
}
}
await this._checkElementExists(cmd);
await this._handleRetry(async () => {
this.logCommand(cmd, 'take');
return rundown.take(cmd.content);
});
}
/**
* Take out: Animate out a graphic element
*/
async takeoutElement(cmd) {
const rundown = await this._getRundown();
if (cmd.transition) {
if (cmd.transition.type === timeline_state_resolver_types_1.VIZMSETransitionType.DELAY) {
if (await this.waitWithLayer(cmd.layerId || '__default', cmd.transition.delay)) {
// at this point, the wait aws aborted by someone else. Do nothing then.
return;
}
}
}
await this._checkPrepareElement(cmd.content);
await this._checkElementExists(cmd);
await this._handleRetry(async () => {
this.logCommand(cmd, 'out');
return rundown.out(cmd.content);
});
}
/**
* Continue: Cause the graphic element to step forward, if it has multiple states
*/
async continueElement(cmd) {
const rundown = await this._getRundown();
await this._checkPrepareElement(cmd.content);
await this._checkElementExists(cmd);
await this._handleRetry(async () => {
this.logCommand(cmd, 'continue');
return rundown.continue(cmd.content);
});
}
/**
* Continue-reverse: Cause the graphic element to step backwards, if it has multiple states
*/
async continueElementReverse(cmd) {
const rundown = await this._getRundown();
await this._checkPrepareElement(cmd.content);
await this._checkElementExists(cmd);
await this._handleRetry(async () => {
this.logCommand(cmd, 'continue reverse');
return rundown.continueReverse(cmd.content);
});
}
/**
* Special: trigger a template which clears all templates on the output
*/
async clearAll(cmd) {
const rundown = await this._getRundown();
const template = {
timelineObjId: cmd.timelineObjId,
contentType: timeline_state_resolver_types_1.TimelineContentTypeVizMSE.ELEMENT_INTERNAL,
templateName: cmd.templateName,
templateData: [],
showId: cmd.showId,
};
// Start playing special element:
const cmdTake = {
time: cmd.time,
type: types_1.VizMSECommandType.TAKE_ELEMENT,
timelineObjId: template.timelineObjId,
content: VizMSEManager.getPlayoutItemContentFromLayer(template),
};
await this._checkPrepareElement(cmdTake.content);
await this._checkElementExists(cmdTake);
await this._handleRetry(async () => {
this.logCommand(cmdTake, 'clearAll take');
return rundown.take(cmdTake.content);
});
}
/**
* Special: send commands to Viz Engines in order to clear them
*/
async clearEngines(cmd) {
try {
const engines = await this._getEngines();
const enginesToClear = this._filterEnginesToClear(engines, cmd.channels);
enginesToClear.forEach((engine) => {
const sender = new vizEngineTcpSender_1.VizEngineTcpSender(engine.port, engine.host);
sender.on('warning', (w) => this.emit('warning', `clearEngines: ${w}`));
sender.on('error', (e) => this.emit('error', `clearEngines: ${e}`));
sender.send(cmd.commands);
});
}
catch (e) {
this.emit('warning', `Sending Clear-all command failed ${e}`);
}
}
async _getEngines() {
const profile = await this._vizMSE.getProfile(this._profile);
const engines = await this._vizMSE.getEngines();
const result = [];
const outputs = new Map(); // engine name : channel name
_.each(profile.execution_groups, (group, groupName) => {
_.each(group, (entry) => {
if (typeof entry === 'object' && entry.viz) {
if (typeof entry.viz === 'object' && entry.viz.value) {
outputs.set(entry.viz.value, groupName);
}
}
});
});
const outputEngines = engines.filter((engine) => {
return outputs.has(engine.name);
});
outputEngines.forEach((engine) => {
_.each(_.keys(engine.renderer), (fullHost) => {
const channelName = outputs.get(engine.name);
const match = fullHost.match(/([^:]+):?(\d*)?/);
const port = match && match[2] ? parseInt(match[2], 10) : 6100;
const host = match && match[1] ? match[1] : fullHost;
result.push({ name: engine.name, channel: channelName, host, port });
});
});
return result;
}
_filterEnginesToClear(engines, channels) {
return engines.filter((engine) => channels === 'all' || (engine.channel && channels.includes(engine.channel)));
}
async setConcept(cmd) {
const rundown = await this._getRundown();
await rundown.setAlternativeConcept(cmd.concept);
}
/**
* Load all elements: Trigger a loading of all pilot elements onto the vizEngine.
* This might cause the vizEngine to freeze during load, so do not to it while on air!
*/
async loadAllElements(_cmd) {
this._triggerCommandSent();
await this._triggerLoadAllElements();
this._triggerCommandSent();
}
async _initializeShows(showIds) {
const rundown = await this._getRundown();
this.emit('debug', `Triggering show ${showIds} init `);
for (const showId of showIds) {
try {
await rundown.initializeShow(showId);
}
catch (e) {
this.emit('error', `Error in _initializeShows : ${e instanceof Error ? e.toString() : e}`);
}
}
}
async initializeShows(cmd) {
const rundown = await this._getRundown();
this._initializedShows = new Set(cmd.showIds);
const expectedPlayoutItems = await this._prepareAndGetExpectedPlayoutItems();
if (this.purgeUnknownElements) {
this.emit('debug', `Purging shows ${cmd.showIds} `);
const elementsToKeep = Object.values(expectedPlayoutItems).filter(types_1.isVizMSEPlayoutItemContentInternalInstance);
await rundown.purgeInternalElements(cmd.showIds, true, elementsToKeep);
}
this._triggerCommandSent();
await this._initializeShows(cmd.showIds);
this._triggerCommandSent();
}
async cleanupShows(cmd) {
this._triggerCommandSent();
await this._cleanupShows(cmd.showIds);
this._triggerCommandSent();
}
async _cleanupShows(showIds) {
const rundown = await this._getRundown();
this.emit('debug', `Triggering show ${showIds} cleanup `);
await rundown.purgeInternalElements(showIds, true);
for (const showId of showIds) {
try {
await rundown.cleanupShow(showId);
}
catch (e) {
this.emit('error', `Error in _cleanupShows : ${e instanceof Error ? e.toString() : e}`);
}
}
}
async cleanupAllShows() {
this._triggerCommandSent();
const rundown = await this._getRundown();
try {
await rundown.cleanupAllSofieShows();
}
catch (error) {
this.emit('error', `Error in cleanupAllShows : ${error instanceof Error ? error.toString() : error}`);
}
this._triggerCommandSent();
}
resolveShowNameToId(showName) {
const showNameWithExtension = path.extname(showName) === SHOW_EXTENSION ? showName : `${showName}${SHOW_EXTENSION}`;
return this._showToIdMap?.get(path.posix.join(this._showDirectoryPath, showNameWithExtension));
}
/** Convenience function to get the data for an element */
static getTemplateData(layer) {
if (layer.contentType === timeline_state_resolver_types_1.TimelineContentTypeVizMSE.ELEMENT_INTERNAL)
return layer.templateData;
return [];
}
/** Convenience function to get the "instance-id" of an element. This is intended to be unique for each usage/instance of the elemenet */
static getInternalElementInstanceName(layer) {
return `sofieInt_${layer.templateName}_${getHash((layer.templateData ?? []).join(','))}`;
}
getPlayoutItemContent(playoutItem) {
if ((0, types_1.isVIZMSEPlayoutItemContentExternal)(playoutItem)) {
return playoutItem;
}
const showId = this.resolveShowNameToId(playoutItem.showName);
if (!showId) {
this.emit('warning', `getPlayoutItemContent: Unable to find Show Id for template "${playoutItem.templateName}" and Show Name "${playoutItem.showName}"`);
return undefined;
}
return {
...playoutItem,
instanceName: VizMSEManager.getInternalElementInstanceName(playoutItem),
showId,
};
}
static getPlayoutItemContentFromLayer(layer) {
if (layer.contentType === timeline_state_resolver_types_1.TimelineContentTypeVizMSE.ELEMENT_INTERNAL) {
return {
templateName: layer.templateName,
templateData: this.getTemplateData(layer).map((data) => _.escape(data)),
instanceName: this.getInternalElementInstanceName(layer),
showId: layer.showId,
};
}
if (layer.contentType === timeline_state_resolver_types_1.TimelineContentTypeVizMSE.ELEMENT_PILOT) {
return (0, lib_1.literal)({
vcpid: layer.templateVcpId,
channel: layer.channelName,
});
}
throw new Error(`Unknown layer.contentType "${layer['contentType']}"`);
}
static _getElementHash(content) {
if ((0, types_1.isVizMSEPlayoutItemContentInternalInstance)(content)) {
return `${content.showId}_${content.instanceName}`;
}
else {
return `pilot_${content.vcpid}_${content.channel}`;
}
}
_getCachedElement(hashOrContent) {
if (typeof hashOrContent !== 'string') {
hashOrContent = VizMSEManager._getElementHash(hashOrContent);
return this._elementCache[hashOrContent];
}
else {
return this._elementCache[hashOrContent];
}
}
_cacheElement(content, element) {
const hash = VizMSEManager._getElementHash(content);
if (!element)
throw new Error('_cacheElement: element not set (with hash ' + hash + ')');
if (this._elementCache[hash]) {
this.emit('warning', `There is already an element with hash "${hash}" in cache`);
}
this._elementCache[hash] = {
hash,
element,
content,
isLoaded: this._isElementLoaded(element),
isLoading: this._isElementLoading(element),
};
}
_clearCache() {
_.each(_.keys(this._elementCache), (hash) => {
delete this._elementCache[hash];
});
}
_getElementReference(el) {
if (this._isInternalElement(el))
return el.name;
if (this._isExternalElement(el))
return Number(el.vcpid); // TMP!!
throw Error('Unknown element type, neither internal nor external');
}
_isInternalElement(element) {
const el = element;
return el && el.name && !el.vcpid;
}
_isExternalElement(element) {
const el = element;
return el && el.vcpid;
}
/**
* Check if element is already created, otherwise create it and return it.
*/
async _checkPrepareElement(content, fromPrepare) {
const cachedElement = this._getCachedElement(content);
let vElement = cachedElement ? cachedElement.element : undefined;
if (cachedElement) {
cachedElement.toDelete = false;
}
if (!vElement) {
const elementHash = VizMSEManager._getElementHash(content);
if (!fromPrepare) {
this.emit('warning', `Late preparation of element "${elementHash}"`);
}
else {
this.emit('debug', `VizMSE: preparing new "${elementHash}"`);
}
vElement = await this._prepareNewElement(content);
if (!fromPrepare)
await this._wait(100); // wait a bit, because taking isn't possible right away anyway at this point
}
}
/** Check that the element exists and if not, throw error */
async _checkElementExists(cmd) {
const rundown = await this._getRundown();
const cachedElement = this._getCachedElement(cmd.content);
if (!cachedElement)
throw new Error(`_checkElementExists: cachedElement falsy`);
const elementRef = this._getElementReference(cachedElement.element);
const elementIsExternal = cachedElement && this._isExternalElement(cachedElement.element);
if (elementIsExternal) {
const element = await rundown.getElement(cmd.content);
if (this._isExternalElement(element) && element.exists === 'no') {
throw new Error(`Can't take the element "${elementRef}" while it has the property exists="no"`);
}
}
}
/**
* Create a new element in MSE
*/
async _prepareNewElement(content) {
const rundown = await this._getRundown();
try {
if ((0, types_1.isVizMSEPlayoutItemContentExternalInstance)(content)) {
// Prepare a pilot element
const pilotEl = await rundown.createElement(content);
this._cacheElement(content, pilotEl);
return pilotEl;
}
else {
// Prepare an internal element
const internalEl = await rundown.createElement(content, content.templateName, content.templateData || [], content.channel);
this._cacheElement(content, internalEl);
return internalEl;
}
}
catch (e) {
if (e.toString().match(/already exist/i)) {
// "An internal/external graphics element with name 'xxxxxxxxxxxxxxx' already exists."
// If the object already exists, it's not an error, fetch and use the element instead
const element = await rundown.getElement(content);
this._cacheElement(content, element);
return element;
}
else {
throw e;
}
}
}
async _deleteElement(content) {
const rundown = await this._getRundown();
this._triggerCommandSent();
await rundown.deleteElement(content);
this._triggerCommandSent();
}
async _prepareAndGetExpectedPlayoutItems() {
this.emit('debug', `VISMSE: _prepareAndGetExpectedPlayoutItems (${this._expectedPlayoutItems.length})`);
const hashesAndItems = {};
const expectedPlayoutItems = _.uniq(_.filter(this._expectedPlayoutItems, (expectedPlayoutItem) => {
return ((!this._preloadedRundownPlaylistId ||
!expectedPlayoutItem.playlistId ||
this._preloadedRundownPlaylistId === expectedPlayoutItem.playlistId) &&
((0, types_1.isVIZMSEPlayoutItemContentInternal)(expectedPlayoutItem) ||
(0, types_1.isVIZMSEPlayoutItemContentExternal)(expectedPlayoutItem)));
}), false, (a) => JSON.stringify(_.pick(a, 'templateName', 'templateData', 'vcpid', 'showId')));
await Promise.all(_.map(expectedPlayoutItems, async (expectedPlayoutItem) => {
const content = this.getPlayoutItemContent(expectedPlayoutItem);
if (!content) {
return;
}
const hash = VizMSEManager._getElementHash(content);
try {
await this._checkPrepareElement(content, true);
hashesAndItems[hash] = content;
}
catch (e) {
this.emit('error', `Error in _prepareAndGetExpectedPlayoutItems for "${hash}": ${e.toString()}`);
}
}));
return hashesAndItems;
}
/**
* Update the load-statuses of the expectedPlayoutItems -elements from MSE, where needed
*/
async updateElementsLoadedStatus(forceReloadAll) {
const hashesAndItems = await this._prepareAndGetExpectedPlayoutItems();
let someUnloaded = false;
const elementsToLoad = _.compact(_.map(hashesAndItems, (item, hash) => {
const el = this._getCachedElement(hash);
if (!item.noAutoPreloading && el) {
if (el.wasLoaded && !el.isLoaded && !el.isLoading) {
someUnloaded = true;
}
return el;
}
return undefined;
}));
if (this._rundown) {
this.emit('debug', `Updating status of elements starting, activePlaylistId="${this._preloadedRundownPlaylistId}", elementsToLoad.length=${elementsToLoad.length} (${_.keys(hashesAndItems).length})`);
const rundown = await this._getRundown();
if (forceReloadAll) {
elementsToLoad.forEach((element) => {
element.isLoaded = false;
element.isLoading = false;
element.requestedLoading = false;
element.wasLoaded = false;
});
}
if (someUnloaded) {
await this._triggerRundownActivate(rundown);
}
await Promise.all(_.map(elementsToLoad, async (cachedEl) => {
try {
await this._checkPrepareElement(cachedEl.content);
this.emit('debug', `Updating status of element ${cachedEl.hash}`);
// Update cached status of the element:
const newEl = await rundown.getElement(cachedEl.content);
const newLoadedEl = {
...cachedEl,
isExpected: true,
isLoaded: this._isElementLoaded(newEl),
isLoading: this._isElementLoading(newEl),
};
this._elementCache[cachedEl.hash] = newLoadedEl;
this.emit('debug', `Element ${cachedEl.hash}: ${JSON.stringify(newEl)}`);
if ((0, types_1.isVizMSEPlayoutItemContentExternalInstance)(cachedEl.content)) {
if (this._updateAfterReconnect || cachedEl?.isLoaded !== newLoadedEl.isLoaded) {
if (cachedEl?.isLoaded && !newLoadedEl.isLoaded) {
newLoadedEl.wasLoaded = true;
}
else if (!cachedEl?.isLoaded && newLoadedEl.isLoaded) {
newLoadedEl.wasLoaded = false;
}
const vcpid = cachedEl.content.vcpid;
if (newLoadedEl.isLoaded) {
const mediaObject = {
_id: cachedEl.hash,
mediaId: 'PILOT_' + vcpid,
mediaPath: vcpid.toString(),
mediaSize: 0,
mediaTime: 0,
thumbSize: 0,
thumbTime: 0,
cinf: '',
tinf: '',
_rev: '',
};
this.emit('updateMediaObject', cachedEl.hash, mediaObject);
}
else {
this.emit('updateMediaObject', cachedEl.hash, null);
}
}
if (newLoadedEl.wasLoaded && !newLoadedEl.isLoaded && !newLoadedEl.isLoading) {
this.emit('debug', `Element "${this._getElementReference(newEl)}" went from loaded to not loaded, initializing`);
await rundown.initialize(cachedEl.content);
}
}
}
catch (e) {
this.emit('error', `Error in updateElementsLoadedStatus: ${e.toString()}`);
}
}));
this._updateAfterReconnect = false;
this.emit('debug', `Updating status of elements done`);
}
else {
throw Error('VizMSE.v-connection not initialized yet');
}
}
async _triggerRundownActivate(rundown) {
try {
this.emit('debug', 'rundown.activate triggered');
await rundown.activate();
}
catch (error) {
this.emit('warning', `Ignored error for rundown.activate(): ${error}`);
}
this._triggerCommandSent();
await this._wait(1000);
this._triggerCommandSent();
}
/**
* Trigger a load of all elements that are not yet loaded onto the vizEngine.
*/
async _triggerLoadAllElements(loadTwice = false) {
if (this._loadingAllElements) {
this.emit('warning', '_triggerLoadAllElements already running');
return;
}
this._loadingAllElements = true;
try {
const rundown = await this._getRundown();
this.emit('debug', '_triggerLoadAllElements starting');
// First, update the loading-status of all elements:
await this.updateElementsLoadedStatus(true);
// if (this._initializeRundownOnLoadAll) {
// Then, load all elements that needs loading:
const loadAllElementsThatNeedsLoading = async () => {
const showIdsToInitialize = new Set();
this._triggerCommandSent();
await this._triggerRundownActivate(rundown);
await Promise.all(_.map(this._elementCache, async (e) => {
if ((0, types_1.isVizMSEPlayoutItemContentInternalInstance)(e.content)) {
showIdsToInitialize.add(e.content.showId);
e.requestedLoading = true;
}
else if ((0, types_1.isVizMSEPlayoutItemContentExternalInstance)(e.content)) {
if (e.isLoaded) {
// The element is loaded fine, no need to do anything
this.emit('debug', `Element "${VizMSEManager._getElementHash(e.content)}" is loaded`);
}
else if (e.isLoading) {
// The element is currently loading, do nothing
this.emit('debug', `Element "${VizMSEManager._getElementHash(e.content)}" is loading`);
}
else if (e.isExpected) {
// The element has not started loading, load it:
this.emit('debug', `Element "${VizMSEManager._getElementHash(e.content)}" is not loaded, initializing`);
await rundown.initialize(e.content);
}
}
else {
this.emit('error', `Element "${VizMSEManager._getElementHash(e.content)}" type `);
}
}));
await this._initializeShows(Array.from(showIdsToInitialize));
};
// He's making a list:
await loadAllElementsThatNeedsLoading();
await this._wait(2000);
if (loadTwice) {
// He's checking it twice:
await this.updateElementsLoadedStatus();
// Gonna find out what's loaded and nice:
await loadAllElementsThatNeedsLoading();
}
this.emit('debug', '_triggerLoadAllElements done');
}
finally {
this._loadingAllElements = false;
}
}
_setMonitorLoadedElementsTimeout() {
if (this._monitorAndLoadElementsTimeout) {
clearTimeout(this._monitorAndLoadElementsTimeout);
}
if (!this._terminated) {
this._monitorAndLoadElementsTimeout = setTimeout(() => {
this._monitorLoadedElements()
.catch((...args) => {
this.emit('error', ...args);
})
.finally(() => {
this._setMonitorLoadedElementsTimeout();
});
}, MONITOR_INTERVAL);
}
}
_setMonitorConnectionTimeout() {
if (this._monitorMSEConnectionTimeout) {
clearTimeout(this._monitorMSEConnectionTimeout);
}
if (!this._terminated) {
this._monitorMSEConnectionTimeout = setTimeout(() => {
this._monitorConnection()
.catch((...args) => {
this.emit('error', ...args);
})
.finally(() => {
this._setMonitorConnectionTimeout();
});
}, MONITOR_INTERVAL);
}
}
async _monitorConnection() {
if (this.initialized) {
// (the ping will throw on a timeout if ping doesn't return in time)
return this._vizMSE
.ping()
.then(() => {
// ok!
if (!this._msePingConnected) {
this._msePingConnected = true;
this.onConnectionChanged();
}
})
.catch(() => {
// not ok!
if (this._msePingConnected) {
this._msePingConnected = false;
this.onConnectionChanged();
}
})
.then(async () => {
return this._msePingConnected ? this._monitorEngines() : Promise.resolve();
});
}
return Promise.reject();
}
async _monitorEngines() {
if (!this.engineRestPort) {
return;
}
const engines = await this._getEngines();
const ps = [];
engines.forEach((engine) => {
return ps.push(this._pingEngine(engine));
});
const statuses = await Promise.all(ps);
const enginesDisconnected = [];
statuses.forEach((status) => {
if (!status.alive) {
enginesDisconnected.push(`${status.channel || status.name} (${status.host})`);
}
});
if (!_.isEqual(enginesDisconnected, this.enginesDisconnected)) {
this.enginesDisconnected = enginesDisconnected;
this.onConnectionChanged();
}
}
async _pingEngine(engine) {
return new Promise((resolve) => {
const url = `http://${engine.host}:${this.engineRestPort}/#/status`;
got_1.default
.get(url, { timeout: 2000 })
.then((response) => {
const alive = response !== undefined && response?.statusCode < 400;
if (!alive) {
this.emit('debug', `VizMSE: _pingEngine at "${url}", code ${response?.statusCode}`);
}
resolve({ ...engine, alive });
})
.catch((error) => {
this.emit('debug', `VizMSE: _pingEngine at "${url}", error ${error}`);
resolve({ ...engine, alive: false });
});
});
}
/** Monitor loading status of expected elements */
async _monitorLoadedElements() {
try {
if (this._rundown &&
this._hasActiveRundown &&
this.preloadAllElements &&
this._timeSinceLastCommandSent() > SAFE_PRELOAD_TIME) {
await this.updateElementsLoadedStatus(false);
let notLoaded = 0;
let loading = 0;
let loaded = 0;
_.each(this._elementCache, (e) => {
if (e.isLoaded)
loaded++;
else if (e.isLoading)
loading++;
else
notLoaded++;
});
if (notLoaded > 0 || loading > 0) {
// emit debug data
this.emit('debug', `Items on queue: notLoaded: ${notLoaded} loading: ${loading}, loaded: ${loaded}`);
this.emit('debug', `_elementsLoaded: ${_.map(_.filter(this._elementCache, (e) => !e.isLoaded).slice(0, 10), (e) => {
return JSON.stringify(e.element);
})}`);
}
this._setLoadedStatus(notLoaded, loading);
}
else
this._setLoadedStatus(0, 0);
}
catch (e) {
this.emit('error', e);
}
}
async _wait(time) {
if (this.ignoreAllWaits)
return Promise.resolve();
return new Promise((resolve) => setTimeout(resolve, time));
}
/** Execute fcn an retry a couple of times until it succeeds */
async _handleRetry(fcn) {
let i = 0;
const maxNumberOfTries = 5;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
this._triggerCommandSent();
const result = fcn();
this._triggerCommandSent();
return result;
}
catch (e) {
if (i++ < maxNumberOfTries) {
if (e?.toString && e?.toString().match(/inexistent/i)) {
// "PepTalk inexistent error"
this.emit('debug', `VizMSE: _handleRetry got "inexistent" error, trying again...`);
// Wait and try again:
await this._wait(300);
}
else {
// Unhandled error, give up:
throw e;
}
}
else {
// Give up, we've tried enough times already
throw e;
}
}
}
}
_triggerCommandSent() {
this._lastTimeCommandSent = Date.now();
}
_timeSinceLastCommandSent() {
return Date.now() - this._lastTimeCommandSent;
}
_setLoadedStatus(notLoaded, loading) {
if (notLoaded !== this.notLoadedCount || loading !== this.loadingCount) {
this.notLoadedCount = notLoaded;
this.loadingCount = loading;
this._parentVizMSEDevice.connectionChanged();
}
}
/**
* Returns true if the element is successfully loaded (as opposed to "not-loaded" or "loading")
*/
_isElementLoaded(el) {
if (this._isInternalElement(el)) {
return ((el.available === '1.00' || el.available === '1' || el.available === undefined) &&
(el.loaded === '1.00' || el.loaded === '1') &&
el.is_loading !== 'yes');
}
else if (this._isExternalElement(el)) {
return ((el.available === '1.00' || el.available === '1') &&
(el.loaded === '1.00' || el.loaded === '1') &&
el.is_loading !== 'yes');
}
else {
throw new Error(`vizMSE: _isLoaded: unknown element type: ${el && JSON.stringify(el)}`);
}
}
/**
* Returns true if the element has NOT started loading (is currently not loading, or finished loaded)
*/
_isElementLoading(el) {
if (this._isInternalElement(el)) {
return el.loaded !== '1.00' && el.loaded !== '1' && el.is_loading === 'yes';
}
else if (this._isExternalElement(el)) {
return el.loaded !== '1.00' && el.loaded !== '1' && el.is_loading === 'yes';
}
else {
throw new Error(`vizMSE: _isLoaded: unknown element type: ${el && JSON.stringify(el)}`);
}
}
/**
* Return the current MSE rundown, create it if it doesn't exists
*/
async _getRundown() {
if (!this._rundown) {
// Only allow for one rundown fetch at the same time:
if (this._getRundownPromise) {
return this._getRundownPromise;
}
const getRundownPromise = (async () => {
// Check if the rundown already exists:
// let rundown: VRundown | undefined = _.find(await this._vizMSE.getRundowns(), (rundown) => {
// return (
// rundown.show === this._showID &&
// rundown.profile === this._profile &&
// rundown.playlist === this._playlistID
// )
// })
this.emit('debug', `Creating new rundown ${[this._profile, this._playlistID]}`);
const rundown = await this._vizMSE.createRundown(this._profile, this._playlistID);
this._rundown = rundown;
if (!this._rundown)
throw new Error(`_getRundown: this._rundown is not set!`);
return this._rundown;
})();
this._getRundownPromise = getRundownPromise;
try {
const rundown = await this._getRundownPromise;
this._rundown = rundown;
return rundown;
}
catch (e) {
this._getRundownPromise = undefined;
throw e;
}
}
else {
return this._rundown;
}
}
mseConnectionChanged(connected) {
if (connected !== this._mseConnected) {
if (connected) {
// not the first connection
if (this._mseConnected === false) {
this._updateAfterReconnect = true;
}
}
this._mseConnected = connected;
this.onConnectionChanged();
}
}
onConnectionChanged() {
this.emit('connectionChanged', this._mseConnected && this._msePingConnected);
}
clearAllWaitWithLayer(_portId) {
// 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)
}
/**
* Returns true if the wait was cleared from someone else
*/
async waitWithLayer(layerId, delay) {
return this._waitWithLayers.waitOnKey(layerId, delay);
}
getElementsToKeep() {
return this._expectedPlayoutItems
.filter((item) => !!item.baseline)
.map((playoutItem) => this.getPlayoutItemContent(playoutItem))
.filter(types_1.isVizMSEPlayoutItemContentExternalInstance);
}
}
exports.VizMSEManager = VizMSEManager;
//# sourceMappingURL=vizMSEManager.js.map