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

352 lines
13 KiB
JavaScript

// -------------------------------------------------------
// Handle Express server routes for the CasparCG commands.
// -------------------------------------------------------
var express = require("express");
const router = express.Router();
const path = require('path');
const logger = require('../utils/logger');
logger.debug('Caspar-route loading...');
const spx = require('../utils/spx_server_functions.js');
const ip = require('ip')
const ipad = ip.address(); // my ip address
const port = config.general.port || 5656;
const PlayoutCCG = require('../utils/playout_casparCG.js');
// ROUTES CCG -----------------------------------------------------------------------------------
router.get('/', function (req, res) {
res.send('Nothing here. Go away!');
});
router.get('/system', function (req, res) {
res.send('Nothing here. Get lost.');
});
router.get('/requestInfo', async (req, res) => {
var SERVER = req.query.server; // ?server=OVERLAY
var COMMAND = req.query.command; // &command=INFO
// console.log('requestInfo from ' + SERVER + ', cmd: ' + COMMAND);
const CCGHost = global.CCGSockets[PlayoutCCG.getSockIndex(SERVER)].spxhost;
const CCGPort = global.CCGSockets[PlayoutCCG.getSockIndex(SERVER)].spxport;
const net = require('net')
CasparRequest = new net.Socket();
CasparRequest.connect(CCGPort, CCGHost, function () {
CasparRequest.write(COMMAND + '\r\n');
CasparRequest.on('data', function (data) {
res.set('Content-Type', 'text/plain'); // format
res.status(200).send(data);
CasparRequest.destroy();
});
CasparRequest.on('error', function (error) {
return res.status(500).send(error);
CasparRequest.destroy();
});
});
});
router.get('/system/:data', (req, res) => {
// same principle as with /control/:data
// do OpSys level stuff, open folders etc..
//
// NOTE: This endpoint is in the WRONG PLACE
// it should be moved to a more generic place
//
// console.log('System utilities / ' + req.params.data);
data = JSON.parse(req.params.data);
let directoryPath = "";
logger.verbose('System utilities / ' + JSON.stringify(data));
res.sendStatus(200);
switch (data.command) {
case 'DATAFOLDER':
directoryPath = path.normalize(config.general.dataroot);
require('child_process').exec('start "" ' + directoryPath);
break;
case 'TEMPLATEFOLDER':
// directoryPath = path.normalize(config.general.templatefolder);
directoryPath = path.join(spx.getStartUpFolder(), 'ASSETS', 'templates');
require('child_process').exec('start "" ' + directoryPath);
break;
case 'CHECKCONNECTIONS':
// console.log('CCG: Checking server connections...');
spx.checkServerConnections();
break;
case 'RESTARTSERVER':
//
logger.error('RESTART SERVER REQUEST RECEIVED. Will try to kill and restart using process manager.');
process.exit(2);
break;
}
});
router.get('/control/:data', (req, res) => {
// Principle:
// We get data object which has
// - data.command (ADD | STOP | UPDATE)
// - data.fields [{ id: 'f0', value: 'Eka' }, { id: 'f1', value: 'Toka' }];
// - data.element headline1
// - data.profile News
//
// Then we read profiles-file and search for the required element
// such as 'headline1' and get needed data from it:
// - templatefile
// - server
// - channel
// - layer
data = JSON.parse(req.params.data);
logger.info('CCG/Control/ ' + JSON.stringify(data));
res.sendStatus(200);
if (!spx.CCGServersConfigured){ return } // exit early, no need to do any CasparCG work
var DataStr = "";
const directoryPath = path.normalize(config.general.dataroot);
let profilefile = path.join(directoryPath, 'config', 'profiles.json');
const profileDataAsJSON = spx.GetJsonData(profilefile);
var arr = profileDataAsJSON.profiles;
let GFX_Teml = "";
let GFX_Serv = "";
let GFX_Chan = "";
let GFX_Laye = "";
for (var i = 0; i < arr.length; i++) {
var obj = arr[i];
if (obj.name == data.profile) {
GFX_Teml = eval("obj.templates." + data.element + ".templatefile");
GFX_Serv = eval("obj.templates." + data.element + ".server");
GFX_Chan = eval("obj.templates." + data.element + ".channel");
GFX_Laye = eval("obj.templates." + data.element + ".layer");
}
}
logger.debug('CCG/Control - Profile ' + data.profile + "', Template: '" + GFX_Teml, "', CasparCG: " + GFX_Serv + ", " + GFX_Chan + ", " + GFX_Laye);
if (data.command == "ADD" || data.command == "UPDATE") {
let TEMPLATEDATA = "";
if (data.fields) {
data.fields.forEach(item => {
logger.verbose('Processing field for CCG: ' + item.id + ' with value: ' + item.value);
TEMPLATEDATA += spx.CGComponentFactory(item.id, item.value)
});
}
DataSta = "<templateData>";
DataEnd = "</templateData>";
DataStr = DataSta + TEMPLATEDATA + DataEnd;
}
try {
switch (data.command) {
case 'ADD':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' ADD 1 "' + GFX_Teml + '" 1 "' + DataStr + '"\r\n');
break;
case 'UPDATE':
console.log('TODO: This is probably not used to send update to CasparCG... See playout_casparCG.js instead! ** FIXME:**');
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' UPDATE 1 "' + DataStr + '"\r\n');
break;
case 'NEXT':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' NEXT 0\r\n');
break;
case 'STOP':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' STOP 1\r\n');
break;
default:
logger.warn('CCG/Control - Unknown command: ' + data.command);
}
} catch (error) {
logger.error('ERROR in /control/:data, unable to send CCG command. Server up? ' + error)
}
}); // control ended
router.get('/controljson/:data', (req, res) => {
data = JSON.parse(req.params.data);
logger.info('CCG/controljson ' + JSON.stringify(data));
res.sendStatus(200);
if (!spx.CCGServersConfigured){ return } // exit early, no need to do any CasparCG work
const directoryPath = path.normalize(config.general.dataroot);
let profilefile = path.join(directoryPath, 'config', 'profiles.json');
const profileDataAsJSON = spx.GetJsonData(profilefile);
var arr = profileDataAsJSON.profiles;
let GFX_Teml = "";
let GFX_Serv = "";
let GFX_Chan = "";
let GFX_Laye = "";
let GFX_JSON = ""
let stng = ""
for (var i = 0; i < arr.length; i++) {
var obj = arr[i];
if (obj.name == data.profile) {
GFX_Teml = eval("obj.templates." + data.element + ".templatefile");
GFX_Serv = eval("obj.templates." + data.element + ".server");
GFX_Chan = eval("obj.templates." + data.element + ".channel");
GFX_Laye = eval("obj.templates." + data.element + ".layer");
GFX_JSON = JSON.stringify(data.jsonData);
}
}
logger.verbose("CCG/ControlJSON Profile: '" + data.profile + "', Template: '" + GFX_Teml, "', CasparCG: " + GFX_Serv + ", " + GFX_Chan + ", " + GFX_Laye)
logger.debug('json' + JSON.stringify(GFX_JSON));
stng = GFX_JSON;
stng = stng.split('§backslash§').join('&bsol;'); // replace §backslash§ with html corresponding entity
stng = stng.split('"').join('\\"'); // replace " with \"
try {
switch (data.command) {
case 'ADD':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' ADD 1 "' + GFX_Teml + '" 1 "' + stng + '"\r\n');
break;
case 'UPDATE':
global.CCGSockets[spx.getSockIndex(spx.getChannel(data))].write('CG ' + GFX_Chan + '-' + GFX_Laye + ' UPDATE 1 "' + stng + '"\r\n');
break;
default:
logger.warn('CCG/ControlJSON - Unknown command: ' + data.command);
}
} catch (error) {
logger.error('ERROR in /controljson/:data: ' + error)
}
});
router.post('/disable', async (req, res) => {
// Added in 1.1.0. Set "disabled" state of the given CasparCG server
// This only affects playback commands. Initialization works normally.
logger.verbose('CasparCG server ' + req.body.server + ' disabled to ' + req.body.disabled);
try {
var ConfigFile = global.configfileref;
var ConfigData = await spx.GetJsonData(ConfigFile);
if (ConfigData.casparcg.servers) {
ConfigData.casparcg.servers.forEach((serverItem,index) => {
if (serverItem.name == req.body.server) {
serverItem.disabled = req.body.disabled
}
});
}
global.config = ConfigData; // update mem version also
await spx.writeFile(ConfigFile,ConfigData);
let response = ['Config changed']
res.status(200).send(response); // ok 200 AJAX RESPONSE
} catch (error) {
console.error('ERROR', error);
let errmsg = 'Server error in CCG disable [' + error + ']';
logger.error(errmsg);
res.status(500).send(errmsg) // error 500 AJAX RESPONSE
};
});
// Create all required Socket Connections for CasparCG servers specified in config.json
const net = require('net')
let ServerDataForLogger = [];
if (config.casparcg) {
config.casparcg.servers.forEach((element,index) => {
const CurName = element.name;
const CurHost = element.host;
const CurPort = element.port;
ServerDataForLogger.push({ name: CurName, host: CurHost, port: CurPort });
// next two lines creates a dynamic variable for this loop iteration
// Changed in 1.3.0, from:
// var CurCCG = CurName + "= undefined";
// eval(CurCCG);
// ..to:
var CurCCG = new net.Socket();
// end of change
global.CCGSockets.push(CurCCG); // --> PUSH Socket object to a global array for later use
CurCCG.spxname = CurName; // save each entry a name for later searching!
CurCCG.spxhost = CurHost; // save each entry a host for later searching! (v.1.0.14)
CurCCG.spxport = CurPort; // save each entry a port for later searching! (v.1.0.14)
CurCCG.connect(CurPort, CurHost, function () {
ServerDataForLogger.push({ name: CurName, host: CurHost, port: CurPort });
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#00CC00' };
io.emit('SPXMessage2Client', data);
data = { spxcmd: 'updateStatusText', status: 'Communication established with ' + CurName + '.' };
io.emit('SPXMessage2Client', data);
logger.verbose('SPX connected to CasparCG as \'' + CurName + '\' at ' + CurHost + ":" + CurPort + '.');
});
CurCCG.on('data', function (data) {
logger.verbose('SPX received data from CasparCG ' + CurName + ': ' + data);
// we must parse the data so we can evaluate it...
let CCG_RETURN_TEXT = String(data).replace('\r','').replace('\n',''); // convert return object to string, strip \r\n
let CCG_RETURN_CODE = CCG_RETURN_TEXT.substring(0, 2); // first two chars
switch (CCG_RETURN_CODE) {
case "20":
logger.verbose('Comms good with ' + CurName + ": " + CCG_RETURN_TEXT);
break;
case "40":
logger.error(CurName + ' CasparCG response: ' + CCG_RETURN_TEXT );
logger.debug('Verify CasparCG\'s (' + CurName + ') access to templates on SPX server at ' + spx.getTemplateSourcePath());
data = { spxcmd: 'updateStatusText', status: 'Error in comms with ' + CurName + '.' };
io.emit('SPXMessage2Client', data);
break;
case "50":
logger.error('Failed ' + CurName + ": " + CCG_RETURN_TEXT);
data = { spxcmd: 'updateStatusText', status: CurName + ' failed.' };
io.emit('SPXMessage2Client', data);
break;
default:
logger.error('Unknown status value ' + CurName + ' - ' + CCG_RETURN_CODE + ' - ' + CCG_RETURN_TEXT);
break;
}
// SocketIO call to client
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#00CC00' };
io.emit('SPXMessage2Client', data);
if (data.toString().endsWith('exit')) {
CCGclient.destroy();
}
});
CurCCG.on('close', function () {
// SocketIO call to client
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#CC0000' };
io.emit('SPXMessage2Client', data);
data = { spxcmd: 'updateStatusText', status: 'Connection to ' + CurName + ' was closed.' };
io.emit('SPXMessage2Client', data);
logger.verbose('SPX connection to CasparCG "' + CurName + '" closed (' + CurHost + ':' + CurPort + ').');
});
CurCCG.on('error', function (err) {
// console.log('Still 4?', CurCCG.connecting)
data = { spxcmd: 'updateServerIndicator', indicator: 'indicator' + index, color: '#CC0000' };
io.emit('SPXMessage2Client', data);
data = { spxcmd: 'updateStatusText', status: 'Communication error with ' + CurName + '.' };
io.emit('SPXMessage2Client', data);
logger.warn('Unable to connect CasparCG "' + CurName + '" (' + CurCCG.spxhost + ':' + CurCCG.spxport + '). Is it running?');
// console.log('Sockets: ', global.CCGSockets);
});
// console.log('Still 3?', CurCCG.connecting)
});
}; // end if
logger.debug('ServerDataForLogger during init: ' + JSON.stringify(ServerDataForLogger));
module.exports = router;