v-connection
Version:
Sofie TV Automation Vizrt Media Sequencer Engine connection library
285 lines • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMSE = exports.MSERep = exports.CREATOR_NAME = void 0;
const peptalk_1 = require("./peptalk");
const events_1 = require("events");
const xml_1 = require("./xml");
const rundown_1 = require("./rundown");
const uuid = require("uuid");
const util_1 = require("./util");
exports.CREATOR_NAME = 'Sofie';
class MSERep extends events_1.EventEmitter {
constructor(hostname, restPort, wsPort, resthost) {
super();
this.connection = undefined;
this.reconnectTimeout = undefined;
this.lastConnectionAttempt = undefined;
this.timeoutMS = 3000;
this.hostname = hostname;
this.restPort = typeof restPort === 'number' && restPort > 0 ? restPort : 8580;
this.wsPort = typeof wsPort === 'number' && wsPort > 0 ? wsPort : 8595;
this.resthost = resthost; // For ngrok testing only
this.pep = this.initPep();
}
initPep() {
if (this.pep) {
this.pep.removeAllListeners();
}
const pep = (0, peptalk_1.startPepTalk)(this.hostname, this.wsPort);
pep.on('close', () => this.onPepClose());
this.lastConnectionAttempt = Date.now();
this.connection = pep.connect().catch((e) => e);
return pep;
}
onPepClose() {
if (!this.reconnectTimeout) {
this.connection = undefined;
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = undefined;
this.pep = this.initPep();
}, Math.max(2000 - (Date.now() - (this.lastConnectionAttempt ?? 0)), 0));
}
}
async checkConnection() {
if (this.connection) {
await this.connection;
}
else {
throw new Error('Attempt to connect to PepTalk server failed.');
}
}
getPep() {
return this.pep;
}
// private readonly sofieShowRE = /<entry name="sofie_show">\/storage\/shows\/\{([^\}]*)\}<\/entry>/
async getRundowns() {
await this.checkConnection();
const playlistList = await this.pep.getJS('/storage/playlists', 3);
const atomEntry = playlistList.js;
// Horrible hack ... playlists not following atom pub model
if (atomEntry.entry) {
atomEntry.entry.entry = atomEntry.entry.playlist;
delete atomEntry.entry.playlist;
}
const flatList = await (0, xml_1.flattenEntry)(playlistList.js);
return Object.keys(flatList)
.filter((k) => k !== 'name' && typeof flatList[k] !== 'string' && flatList[k].sofie_show)
.map((k) => new rundown_1.Rundown(this, flatList[k].profile, k, flatList[k].description));
}
async getRundown(playlistID) {
const playlist = await this.getPlaylist(playlistID);
return new rundown_1.Rundown(this, playlist.profile, playlistID, playlist.description);
}
async getEngines() {
await this.checkConnection();
const handlers = await this.pep.getJS('/scheduler');
const handlersBody = handlers.js;
// Sometimes the main node is is called 'scheduler', sometimes 'entry'
// It doesn't seem to depend on specific version, so let's just support both
const vizEntries = (handlersBody.entry || handlersBody.scheduler).handler.filter((x) => x.$.type === 'viz');
const viz = await Promise.all(vizEntries.map(async (x) => (0, xml_1.flattenEntry)(x)));
return viz;
}
async listProfiles() {
await this.checkConnection();
const profileList = await this.pep.getJS('/config/profiles', 1);
const flatList = await (0, xml_1.flattenEntry)(profileList.js);
return Object.keys(flatList).filter((x) => x !== 'name');
}
async getProfile(profileName) {
await this.checkConnection();
const profile = await this.pep.getJS(`/config/profiles/${profileName}`);
const flatProfile = await (0, xml_1.flattenEntry)(profile.js);
return flatProfile;
}
async listShows() {
await this.checkConnection();
const showList = await this.pep.getJS('/storage/shows', 1);
const flatList = await (0, xml_1.flattenEntry)(showList.js);
return Object.keys(flatList).filter((x) => x !== 'name');
}
async listShowsFromDirectory() {
await this.checkConnection();
const showList = await this.pep.getJS('/directory/shows');
const flatMap = await (0, xml_1.toFlatMap)(showList.js);
this.extractShowIdsFromPaths(flatMap);
return flatMap;
}
extractShowIdsFromPaths(flatMap) {
for (const [key, value] of flatMap) {
const showId = value.match(/{(.+)}/);
if (!showId) {
// probably some faulty ref
flatMap.delete(key);
}
else {
flatMap.set(key, showId[1]);
}
}
}
async getShow(showId) {
await this.checkConnection();
const show = await this.pep.getJS(`/storage/shows/${(0, util_1.wrapInBracesIfNeeded)(showId)}`);
const flatShow = await (0, xml_1.flattenEntry)(show.js);
return flatShow;
}
async listPlaylists() {
await this.checkConnection();
const playlistList = await this.pep.getJS('/storage/playlists', 1);
const atomEntry = playlistList.js;
// Horrible hack ... playlists not following atom pub model
if (atomEntry.entry) {
atomEntry.entry.entry = atomEntry.entry.playlist;
delete atomEntry.entry.playlist;
}
const flatList = await (0, xml_1.flattenEntry)(playlistList.js);
return Object.keys(flatList).filter((x) => x !== 'name');
}
async getPlaylist(playlistName) {
await this.checkConnection();
const playlist = await this.pep.getJS(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(playlistName)}`);
let flatPlaylist = await (0, xml_1.flattenEntry)(playlist.js);
if (Object.keys(flatPlaylist).length === 1) {
flatPlaylist = flatPlaylist[Object.keys(flatPlaylist)[0]];
}
return flatPlaylist;
}
// Rundown basics task
async createRundown(profileName, playlistID, description) {
await this.assertProfileExists(profileName);
description = description ? description : `Sofie Rundown ${new Date().toISOString()}`;
playlistID = playlistID ? playlistID.toUpperCase() : uuid.v4().toUpperCase();
if (!(await this.doesPlaylistExist(playlistID, profileName))) {
await this.createNewPlaylist(playlistID, description, profileName);
await this.createPlaylistDirectoryReferenceIfMissing(playlistID);
}
return new rundown_1.Rundown(this, profileName, playlistID, description);
}
async assertProfileExists(profileName) {
try {
await this.pep.get(`/config/profiles/${profileName}`, 1);
}
catch (err) {
throw new Error(`The profile with name '${profileName}' for a new rundown does not exist. Error is: ${(0, peptalk_1.getPepErrorMessage)(err)}.`);
}
}
async doesPlaylistExist(playlistID, profileName) {
const playlist = await this.getPlaylist(playlistID.toUpperCase()).catch(() => undefined);
if (!playlist) {
return false;
}
if (playlist.profile && !playlist.profile.endsWith(`/${profileName}`)) {
throw new Error(`Referenced playlist exists but references profile '${playlist.profile}' rather than the given '${profileName}'.`);
}
return true;
}
async createNewPlaylist(playlistID, description, profileName) {
const modifiedDate = this.getCurrentTimeFormatted();
await this.pep.insert(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(playlistID)}`, `<playlist description="${description}" modified="${modifiedDate}" profile="/config/profiles/${profileName}" name="${(0, util_1.wrapInBracesIfNeeded)(playlistID)}">
<elements/>
<entry name="environment">
<entry name="alternative_concept"/>
</entry>
<entry name="cursors">
<entry name="globals">
<entry name="last_taken"/>
<entry name="last_read"/>
</entry>
</entry>
<entry backing="transient" name="active_profile"/>
<entry name="meta"/>
<entry name="settings"/>
<entry name="ncs_cursor"/>
</playlist>`, peptalk_1.LocationType.Last);
}
async createPlaylistDirectoryReferenceIfMissing(playlistID) {
if (await this.doesPlaylistDirectoryReferenceExists(playlistID)) {
return;
}
await this.insertDirectoryPlaylistReference(playlistID);
}
async doesPlaylistDirectoryReferenceExists(playlistID) {
await this.checkConnection();
const pepResponseJS = await this.pep.getJS('/directory/playlists/');
const directoryPlaylistRefs = await (0, xml_1.flattenEntry)(pepResponseJS.js);
return Object.keys(directoryPlaylistRefs)
.filter((key) => key.startsWith('ref'))
.map((key) => directoryPlaylistRefs[key].value)
.some((refValue) => !!refValue && refValue.includes((0, util_1.wrapInBracesIfNeeded)(playlistID)));
}
async insertDirectoryPlaylistReference(playlistID) {
await this.pep.insert(`/directory/playlists/`, `<ref author="${exports.CREATOR_NAME}" description="${playlistID}">/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(playlistID)}</ref>`, peptalk_1.LocationType.Last);
}
getCurrentTimeFormatted() {
const date = new Date();
return `${date.getUTCDate().toString().padStart(2, '0')}.${(date.getUTCMonth() + 1)
.toString()
.padStart(2, '0')}.${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
.getMinutes()
.toString()
.padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
}
// Rundown basics task
async deleteRundown(rundown) {
const playlist = await this.getPlaylist(rundown.playlist);
// console.dir(playlist, { depth: 10 })
if (playlist.active_profile.value) {
throw new Error(`Cannot delete an active profile.`);
}
const delres = await this.pep.delete(`/storage/playlists/${(0, util_1.wrapInBracesIfNeeded)(rundown.playlist)}`);
return delres.status === 'ok';
}
// Advanced feature
async createProfile(_profileName, _profileDetailsTbc) {
return Promise.reject(new Error('Not implemented. Creating profiles is a future feature.'));
}
// Advanced feature
async deleteProfile(_profileName) {
return Promise.reject(new Error('Not implemented. Deleting profiles ia a future feature.'));
}
async ping() {
try {
const res = await this.pep.ping();
return { path: 'ping', status: 200, response: res.body };
}
catch (err) {
err.path = 'ping';
err.status = 418;
err.response = (0, peptalk_1.getPepErrorMessage)(err);
throw err;
}
}
async close() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
if (this.connection) {
await this.pep.close();
return true;
}
return false;
}
timeout(t) {
if (typeof t !== 'number')
return this.timeoutMS;
return this.pep.setTimeout(t);
}
}
exports.MSERep = MSERep;
/**
* Factory to create an [[MSE]] instance to manage commumication between a Node
* application and a Viz Media Sequencer Engine.
* @param hostname Hostname or IP address for the instance of the MSE to control.
* @param restPort Optional port number for HTTP traffic, is different from the
* default of 8580.
* @param wsPort Optional port number for PepTalk traffic over websockets, if
* different from the default of 8695.
* @param resthost Optional different host name for rest connection - for testing
* purposes only.
* @return New MSE that will start to initialize a connection based on the parameters.
*/
function createMSE(hostname, restPort, wsPort, resthost) {
return new MSERep(hostname, restPort, wsPort, resthost);
}
exports.createMSE = createMSE;
//# sourceMappingURL=mse.js.map