// ------------------------------------------------------- // 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 = ""; DataEnd = ""; 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('\'); // 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;