Files
WebCG/utils/playout_casparCG.js
2026-08-12 14:21:28 +09:00

266 lines
10 KiB
JavaScript

// ================== functions alphabetical order ==================================================
const logger = require('./logger.js');
const fs = require('fs');
const path = require('path')
const moment = require('moment');
module.exports = {
isDisabled: function (serverName) {
let outcome = false;
config.casparcg.servers.forEach((item,index) => {
// console.log('Iterating server ' + item.name + ', disabled = ' + item.disabled);
if (item.name == serverName && item.disabled == true) {
outcome = true
}
});
return outcome;
},
clearChannelsFromGCServer: function (serverName='') {
//TODO: add 2nd parameter an Array of layer numbers...
let ClearAMCPCommand = 'CLEAR 1\r\n CLEAR 2\r\n CLEAR 3\r\n CLEAR 4\r\n CLEAR 5\r\n CLEAR 6\r\n CLEAR 7\r\n CLEAR 8\r\n';
if (serverName) {
// clear the given server
global.CCGSockets[this.getSockIndex(serverName)].write(ClearAMCPCommand);
} else {
// clear all configured servers
if ( config.casparcg && config.casparcg.servers ) {
config.casparcg.servers.forEach((item,index) => {
global.CCGSockets[this.getSockIndex(item.name)].write(ClearAMCPCommand);
});
}
}
logger.verbose('Clearing CasparCG [server: ' + serverName + ']');
},
CGComponentFactory: function (fieldID, value) {
// Generate template data entry XML for CasparCG.
// decodeURIComponent and UnSwapCharacters() added here to workaround issue #5.
// require ..... fieldID, such as 'f0' and value such as 'Tuomo'
// returns ..... component element as xml string, example:
/*
<componentData id=\"f0\">
<data id=\"text\" value=\"Donald Trump\"/>
</componentData>
*/
logger.debug('CGComponentFactory (for XML data only) - fieldID: ' + fieldID + ', value: ' + value);
let decodedValue = decodeURIComponent(value) || ""; // changed again. Was += " " and then "null" <:-/
return `<componentData id=\\"${fieldID}\\"><data id=\\"text\\" value=\\"${decodedValue}\\"/></componentData>`;
},
playoutController: function (data){
// We get data object which has
// - data.command (ADD | STOP | UPDATE)
// let GFX_Teml = data.relpathCCG; // before 1.0.7
// let GFX_Teml = 'http://' + ip.address() + ':' + config.general.port + '/templates/' + data.relpathCCG + '.html'; // changed to http in 1.0.7
// let GFX_Teml = 'http://localhost:' + config.general.port + '/templates/' + data.relpathCCG + '.html'; // accidentally left to "localhost" in 1.0.8. Oops <:-O
// 1.0.12 - Check if we do have CCG servers configured
let CCGSERVER = global.CCGSockets[this.getSockIndex(data.playserver)];
if (!CCGSERVER || typeof CCGSERVER === 'undefined') {
logger.verbose('No CasparCG servers configured (or server [' + data.playserver + '] not found) in SPX-GC. Skipping CCG command ' + data.command);
return
}
if (!data.playserver || data.playserver=='' || typeof data.playserver === 'undefined') {
logger.verbose('No CasparCG server configured in the template. Skipping CCG command ' + data.command);
return
}
if (this.isDisabled(data.playserver)===true) {
logger.verbose('Server ' + data.playserver + ' temporarily disabled, canceling playout commands.' );
return;
}
let GFX_Teml = getCCGTemplateFilepath(data.relpathCCG); // v.1.0.9 = Supports both FILE or HTTP template paths, see config>general.casparcg-template-folder
let GFX_Serv = data.playserver;
let GFX_Chan = data.playchannel;
let GFX_Laye = data.playlayer;
let DataType = data.dataformat || 'json'; // added in 1.2.0
let InvFunct = data.invoke;
data.command = data.command.toUpperCase();
// Notify user
if ( !global.CCGSockets[this.getSockIndex(data.playserver)] ) {
logger.warn('Server [' + GFX_Serv + '] not found in your config! Make sure configuration and project settings match.' )
}
logger.verbose('CasparCG playoutController - command ' + data.command + ', Template: ' + GFX_Teml, ', CasparCG: ' + GFX_Serv + ', ' + GFX_Chan + ', ' + GFX_Laye);
logger.debug('CasparCG playoutController ' + JSON.stringify(data,null,4));
logger.debug('Generating DATA for CasparCG [command ' + data.command + '], source: ' + JSON.stringify(data.fields,null,4));
let TEMPLATEDATA = "";
var DataStr = "";
if (data.command == "ADD" || data.command == "UPDATE") {
if (data.fields) {
data.fields.forEach((item,index) => {
logger.debug(' DATA --> ' + item.field + ' : ' + item.value);
// Fixes "undefined" issue in AMCP playout
let value = ""; // default value if null or undefined
if (item.value!=null && typeof item.value !== 'undefined') {
value = item.value.toString();
}
if (DataType == 'xml'){
// generate data in XML format
TEMPLATEDATA += this.CGComponentFactory(item.field, value);
} else {
logger.debug('Generating JSON data for CasparCG, field: ' + item.field + ', value: ' + value);
TEMPLATEDATA += '\\"' + item.field + '\\":\\"' + value + '\\",';
}
});
}
if (DataType == 'xml'){
// finalize XML format
DataStr = "<templateData>" + TEMPLATEDATA + "</templateData>";
} else {
// finalize JSON formatby removing trailing comma
if (TEMPLATEDATA.slice(-1)==','){
TEMPLATEDATA = TEMPLATEDATA.slice(0, -1);
}
DataStr = "{" + TEMPLATEDATA + "}";
}
}
logger.debug('[' + DataType + '] ' + DataStr);
try {
switch (data.command) {
case 'ADD':
global.CCGSockets[this.getSockIndex(data.playserver)].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' ADD 1 "' + GFX_Teml + '" 1 "' + DataStr + '"\r\n');
break;
case 'UPDATE':
global.CCGSockets[this.getSockIndex(data.playserver)].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' UPDATE 1 "' + DataStr + '"\r\n');
break;
case 'STOP':
global.CCGSockets[this.getSockIndex(data.playserver)].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' STOP 1\r\n');
break;
case 'NEXT':
global.CCGSockets[this.getSockIndex(data.playserver)].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' NEXT 0\r\n');
break;
case 'INVOKE':
InvFunct = InvFunct.replace(/"/g, '\\"'); // replace " with \" globally. Added in 1.1.0.
global.CCGSockets[this.getSockIndex(data.playserver)].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' INVOKE 1 \"' + InvFunct + '\"\r\n');
break;
default:
logger.warn('CCG/Control (util) - Unknown command: ' + data.command);
}
} catch (error) {
logger.error('ERROR in playoutController: ' + error)
}
}, // end add,
VideoPlayerController: function (data) {
// We get data object which has
// - data.command (ADD | STOP | UPDATE)
// handle video commands
console.log('Handling VIDEO PLAYOUT', data);
let GFX_Serv = data.playserver;
let GFX_Chan = data.playchannel;
let GFX_Laye = data.playlayer;
let GFX_File = data.relpath;
let GFX_CCGf = data.relpathCCG;
let GFX_OPTS = data.playoptions;
logger.info("VideoPlayerController / CasparCG: " + GFX_Serv + ", " + GFX_Chan + ", " + GFX_Laye + ", Videofile " + GFX_CCGf);
logger.debug('CasparCG VideoPlayerController ' + JSON.stringify(data,null,4));
try {
switch (data.command) {
case 'play':
logger.verbose('Playing VIDEO / ' + GFX_CCGf);
global.CCGSockets[this.getSockIndex(GFX_Serv)].write('PLAY ' + GFX_Chan + '-' + GFX_Laye + ' ' + GFX_CCGf + ' ' + GFX_OPTS + ' \r\n');
break;
case 'stop':
logger.verbose('Stopping VIDEO / ' + GFX_CCGf);
global.CCGSockets[this.getSockIndex(GFX_Serv)].write('STOP ' + GFX_Chan + '-' + GFX_Laye + '\r\n');
break;
case 'fadeout':
logger.verbose('Fadeout VIDEO / ' + GFX_CCGf);
// PLAY 1-10 CLEAR MIX 50
global.CCGSockets[this.getSockIndex(GFX_Serv)].write('PLAY ' + GFX_Chan + '-' + GFX_Laye + ' EMPTY MIX 13\r\n');
break;
default:
logger.warn('CCG/controlvideo - Unknown command: ' + data.command);
}
// res.sendStatus(200);
} catch (error) {
logger.error('ERROR in VideoPlayerController: ' + error)
// res.sendStatus(500);
}
}, // end video player
getSockIndex: function (SERVERNAME) {
// Get an index of a CasparCG socket connection reference, not the object directly.
// require .... SERVERNAME (example "TG")
// returns .... CCG Server object INDEX (such as 0)
logger.debug('getSockIndex / Searching for Socket reference for connection "' + SERVERNAME + '"...');
let serverIndex = "";
CCGSockets.forEach(function (item, index) {
if (global.CCGSockets[index].spxname.toLowerCase() == SERVERNAME.toLowerCase()) {
serverIndex = index;
logger.debug('getSockIndex found [' + global.CCGSockets[index].spxname + '] so index is [' + serverIndex + '].');
}
else {
logger.debug('getSockIndex skipping ' + global.CCGSockets[index].spxname) + '...';
}
});
return serverIndex;
}
} // end of exports PlayoutCCG.<functionName>
function getCCGTemplateFilepath(fileRef) {
// Added in 1.0.9.
// Return either the "simple filepath" or "full URL" for the CasparCG command.
const spx = require('./spx_server_functions.js');
let TemplateSource = spx.getTemplateSourcePath()
let TemplatePathForCasparCGServer
if ( TemplateSource.substring(0, 4)=='http' )
// HTTP
TemplatePathForCasparCGServer = TemplateSource + ':' + config.general.port + '/templates/' + fileRef + '.html'; //
else {
// FILE
TemplatePathForCasparCGServer = fileRef; // as-is, a filepath in CasparCG server's own template-path directory
}
return TemplatePathForCasparCGServer
}