├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── index.js ├── lib ├── connection.js ├── data.js ├── mixer.js ├── playout.js ├── query.js ├── template.js └── xml2json.js ├── package.json ├── test.js └── test ├── .jshintrc └── connection.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "else", 4 | "for", 5 | "while", 6 | "do", 7 | "try", 8 | "catch" 9 | ], 10 | "requireSpaceAfterKeywords": [ 11 | "if", 12 | "else", 13 | "for", 14 | "while", 15 | "do", 16 | "switch", 17 | "return", 18 | "try", 19 | "catch" 20 | ], 21 | "requireSpacesInNamedFunctionExpression": { 22 | "beforeOpeningCurlyBrace": true 23 | }, 24 | "disallowSpacesInNamedFunctionExpression": { 25 | "beforeOpeningRoundBrace": true 26 | }, 27 | "requireSpacesInAnonymousFunctionExpression": { 28 | "beforeOpeningRoundBrace": true, 29 | "beforeOpeningCurlyBrace": true 30 | }, 31 | "requireSpacesInFunctionExpression": { 32 | "beforeOpeningCurlyBrace": true 33 | }, 34 | "disallowSpacesInFunctionDeclaration": { 35 | "beforeOpeningRoundBrace": true 36 | }, 37 | "disallowMultipleVarDecl": true, 38 | "requireBlocksOnNewline": true, 39 | "disallowEmptyBlocks": true, 40 | "disallowSpacesInsideObjectBrackets": true, 41 | "disallowSpacesInsideArrayBrackets": true, 42 | "disallowSpacesInsideParentheses": true, 43 | "disallowQuotedKeysInObjects": "allButReserved", 44 | "requireDotNotation": true, 45 | "disallowSpaceAfterObjectKeys": true, 46 | "requireCommaBeforeLineBreak": true, 47 | "requireOperatorBeforeLineBreak": [ 48 | "?", 49 | "+", 50 | "-", 51 | "/", 52 | "*", 53 | "=", 54 | "==", 55 | "===", 56 | "!=", 57 | "!==", 58 | ">", 59 | ">=", 60 | "<", 61 | "<=" 62 | ], 63 | "disallowSpaceBeforeBinaryOperators": [","], 64 | "requireSpaceBeforeBinaryOperators": [ 65 | "?", 66 | "+", 67 | "/", 68 | "*", 69 | "=", 70 | "==", 71 | "===", 72 | "!=", 73 | "!==", 74 | ">", 75 | ">=", 76 | "<", 77 | "<=" 78 | ], 79 | "disallowSpaceAfterPrefixUnaryOperators": ["!"], 80 | "requireSpaceAfterBinaryOperators": [ 81 | "?", 82 | "+", 83 | "/", 84 | "*", 85 | ":", 86 | "=", 87 | "==", 88 | "===", 89 | "!=", 90 | "!==", 91 | ">", 92 | ">=", 93 | "<", 94 | "<=" 95 | ], 96 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 97 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 98 | "requireSpaceBeforeBinaryOperators": [ 99 | "+", 100 | "-", 101 | "/", 102 | "*", 103 | "=", 104 | "==", 105 | "===", 106 | "!=", 107 | "!==" 108 | ], 109 | "requireSpaceAfterBinaryOperators": [ 110 | "+", 111 | "-", 112 | "/", 113 | "*", 114 | "=", 115 | "==", 116 | "===", 117 | "!=", 118 | "!==" 119 | ], 120 | "disallowKeywords": ["with"], 121 | "disallowMultipleLineStrings": true, 122 | "disallowMultipleLineBreaks": true, 123 | "validateLineBreaks": "LF", 124 | "validateQuoteMarks": "\"", 125 | "validateIndentation": "\t", 126 | "disallowTrailingWhitespace": true, 127 | "disallowKeywordsOnNewLine": ["else", "catch"], 128 | "requireLineFeedAtFileEnd": true, 129 | "safeContextKeyword": ["self","PushDevice"], 130 | "disallowYodaConditions": true 131 | } 132 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "globalstrict": true, 4 | "unused": false, 5 | "esnext": true 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.4" 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | First use node-caspar-cg. If you have problems, find a bug, or have a feature suggestion submit an issue. Give as many details as you can and post any relevent code. 4 | 5 | ## Making Changes 6 | 7 | 1. Fork the repo 8 | 2. Pick an issue to work on and make a topic branch 9 | 3. Test your changes 10 | 4. Submit a pull request 11 | 5. Jump to step 2 12 | 13 | ## Syntax 14 | 15 | * 1 tab indenting, no spaces 16 | * no trailing whitespace 17 | * blank lines should be blank 18 | * follow the conventions you see in existing files 19 | * declare variables in the proper scope 20 | * use descriptive variables and function names -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (grunt) { 4 | grunt.initConfig({ 5 | mochaTest: { 6 | options: { 7 | reporter: "spec" 8 | }, 9 | unit: { 10 | src: ["test/**/*.js"] 11 | } 12 | }, 13 | jshint: { 14 | options: { 15 | reporter: require("jshint-stylish") 16 | }, 17 | javascript: { 18 | src: [ 19 | "lib/**/*.js", 20 | "index.js", 21 | "tests/**/*.js" 22 | ], 23 | options: { 24 | jshintrc: ".jshintrc" 25 | } 26 | } 27 | }, 28 | jscs: { 29 | javascript: { 30 | src: [ 31 | "lib/**/*.js", 32 | "index.js", 33 | "tests/**/*.js" 34 | ], 35 | } 36 | } 37 | }); 38 | 39 | grunt.registerTask("default", ["test"]); 40 | grunt.registerTask("lint", ["jshint", "jscs"]); 41 | grunt.registerTask("test", ["lint", "mochaTest"]); 42 | 43 | require("load-grunt-tasks")(grunt); 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Kevin Smith 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CasparCG to Node interface 2 | 3 | [![Build Status](https://travis-ci.org/respectTheCode/node-caspar-cg.png)](https://travis-ci.org/respectTheCode/node-caspar-cg) 4 | 5 | This project is early in development and API may change. The query, playout, data and template commands are mostly finished and I will be adding more as I need them. If you need something that is missing add an issue. 6 | 7 | For now docs are in the source only. 8 | 9 | ## Install 10 | 11 | npm install caspar-cg 12 | 13 | ## Usage Example 14 | 15 | var CasparCG = require("caspar-cg"); 16 | 17 | ccg = new CasparCG("localhost", 5250); 18 | ccg.connect(function () { 19 | ccg.info(function (err, serverInfo) { 20 | console.log(serverInfo); 21 | }); 22 | 23 | ccg.play("1-1", "AMB"); 24 | 25 | setTimeout(function () { 26 | ccg.clear("1"); 27 | ccg.disconnect(); 28 | }, 10 * 1000); 29 | }); 30 | 31 | ccg.on("connected", function () { 32 | console.log("Connected"); 33 | }); 34 | 35 | ### Usage with Express and to render html5 templates 36 | This code example requires *express*. The templates are rendered by using *CALL* and stringifying your json payload. The template on your caspar server can cast that string back to json. 37 | 38 | ```javascript 39 | var express = require("express"); 40 | var app = express(); 41 | var router = express.Router(); 42 | var path = __dirname + '/app/'; // source code directory 43 | var bodyParser = require("body-parser"); 44 | 45 | var CasparCG = require("caspar-cg"); 46 | ccg = new CasparCG("your-casparcg-ip-address", 5250); 47 | 48 | router.post("/caspar/:template",function(req,res){ // 49 | ccg.connect(function () { 50 | var addCommand = 'CG 1-99 ADD 0 your-template-dir/'+req.params.template+' 1'; 51 | var updateCommand = 'CALL 1-99 UPDATE "'+JSON.stringify(req.body)+'"'; 52 | ccg.sendCommand(addCommand); 53 | ccg.sendCommand(updateCommand, function(){ 54 | console.log('Command was sent') 55 | }) 56 | }); 57 | 58 | res.send('Command was sent'); 59 | }); 60 | 61 | app.use(bodyParser.urlencoded({ extended: false })); 62 | app.use(bodyParser.json()); 63 | 64 | app.use(express.static('node_modules')) 65 | app.use(express.static('app')) 66 | 67 | app.use("/",router); 68 | 69 | app.use("*",function(req,res){ 70 | res.sendFile(path + "404.html"); 71 | }); 72 | 73 | app.listen(3000,function(){ 74 | console.log("Live at Port 3000"); 75 | }); 76 | ``` 77 | 78 | ## Changelog 79 | 80 | ### v0.1.0 81 | 82 | * Fix `disconnected` event on node v4+ 83 | * Requires node v4+ 84 | 85 | ### v0.0.9 86 | 87 | * Fix `info` parsing error when parsing `image-producer` data 88 | 89 | ### v0.0.8 90 | 91 | * Adds `resume` 92 | * Adds `swap` 93 | * Adds `print` 94 | * Adds `logLevel` 95 | 96 | ### v0.0.7 97 | 98 | * Adds `mixerFill` to move and resize layers 99 | 100 | ### v0.0.6 101 | 102 | `ccg.info()` now parses layers and returns a much more predictable result. 103 | 104 | * Layers is always an array 105 | * Numbers and Booleans are parsed (all values were strings before) 106 | * Parameters with `-` and `_` are replaced with cammel case 107 | * Inconsistent parameters are renamed 108 | 109 | ### v0.0.5-5 110 | 111 | Socket errors are now emitted as `connectionError` instead of `error`. 112 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var events = require("events"); 4 | var util = require("util"); 5 | var _ = require("underscore"); 6 | var count = 0; 7 | 8 | var ccg = module.exports = function (host, port) { 9 | events.EventEmitter.call(this); 10 | 11 | this.options = _.extend({}, this.options); 12 | 13 | if (typeof(host) == "string") { 14 | this.options.host = host; 15 | } else if (typeof(host) == "object") { 16 | _.extend(this.options, host); 17 | } 18 | 19 | if (port) { 20 | this.options.port = port; 21 | } 22 | 23 | this.index = count++; 24 | }; 25 | 26 | util.inherits(ccg, events.EventEmitter); 27 | 28 | ccg.prototype.options = { 29 | reconnect: true, 30 | host: "localhost", 31 | port: 5250, 32 | debug: false 33 | }; 34 | 35 | ccg.prototype.log = function () { 36 | if (!this.options.debug) return; 37 | 38 | var args = _.values(arguments); 39 | args.unshift("CCG" + this.index + ":"); 40 | 41 | console.log.apply(console, args); 42 | }; 43 | 44 | // connection management and command queing 45 | require("./lib/connection")(ccg); 46 | // query commands 47 | require("./lib/query")(ccg); 48 | // query commands 49 | require("./lib/playout")(ccg); 50 | // query data 51 | require("./lib/data")(ccg); 52 | // query templates 53 | require("./lib/template")(ccg); 54 | // mixer commands 55 | require("./lib/mixer")(ccg); 56 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var net = require("net"); 4 | 5 | module.exports = function (ccg) { 6 | ccg.prototype.connected = false; 7 | 8 | ccg.prototype.client = false; 9 | ccg.prototype.disconnecting = false; 10 | ccg.prototype.responseCode = false; 11 | ccg.prototype.responseMessage = ""; 12 | ccg.prototype.response = ""; 13 | ccg.prototype.readyForNextCommand = true; 14 | ccg.prototype.waitingForDoubleTerminator = false; 15 | 16 | // ccg.connect 17 | // ----------- 18 | // Connect to Caspar CG 19 | // 20 | // `ccg.connect(host, port[, cb]);` 21 | // 22 | // or use `ccg.connect();` after setting `ccg.options` 23 | ccg.prototype.connect = function (host, port, cb) { 24 | var self = this; 25 | self.commandQueue = []; 26 | 27 | if (typeof(host) != "string") { 28 | cb = port; 29 | port = host; 30 | host = false; 31 | } 32 | 33 | if (typeof(port) != "number") { 34 | cb = port; 35 | port = false; 36 | } 37 | 38 | if (typeof(cb) != "function") cb = false; 39 | 40 | if (host) self.options.host = host; 41 | if (port) self.options.port = port; 42 | 43 | if (self.options.reconnect && typeof(self.options.reconnectInterval) != "number") { 44 | self.options.reconnectInterval = 15; 45 | } 46 | 47 | self.disconnecting = false; 48 | 49 | var client = self.client = net.connect({port: self.options.port, host: self.options.host}); 50 | 51 | client.on("connect", function () { 52 | self.log("Connected to", self.options.host, self.options.port); 53 | self.connected = true; 54 | 55 | if (cb) { 56 | cb(); 57 | cb = false; 58 | } 59 | 60 | self.emit("connected"); 61 | }); 62 | 63 | client.on("error", function (err) { 64 | if (cb) { 65 | cb(err); 66 | } 67 | 68 | self.emit("connectionError", err); 69 | 70 | if (!self.disconnecting && self.options.reconnect) { 71 | setTimeout(function () { 72 | self.connect(); self.emit("reconnecting"); 73 | }, (self.options.reconnectInterval * 1000)); 74 | } 75 | }); 76 | 77 | client.on("reconnecting", function () { 78 | self.log("Reconnecting"); 79 | }); 80 | 81 | client.on("close", function () { 82 | self.log("Disconnected"); 83 | self.emit("disconnected"); 84 | self.connected = false; 85 | client = false; 86 | 87 | if (!self.disconnecting && self.options.reconnect) { 88 | setTimeout(function () { 89 | self.connect(); self.emit("reconnecting"); 90 | }, (self.options.reconnectInterval * 1000)); 91 | } 92 | }); 93 | 94 | client.on("data", function (chunk) { 95 | var commandQueue = self.commandQueue; 96 | var finishedCommand = function () { 97 | self.responseCode = false; 98 | self.responseMessage = ""; 99 | self.response = ""; 100 | 101 | if (commandQueue.length > 0) { 102 | // if there are queued commands send the next one 103 | self.log("Send:",commandQueue[0].cmd.substr(0, commandQueue[0].cmd.length - 2)); 104 | self.client.write(commandQueue[0].cmd); 105 | } else { 106 | // queue is empty we are ready for another command 107 | self.readyForNextCommand = true; 108 | } 109 | }; 110 | 111 | self.response += chunk.toString(); 112 | 113 | if (!self.responseCode) { 114 | self.responseCode = parseInt(self.response.substr(0, 3), 10); 115 | self.log("Response Code:", self.responseCode); 116 | } 117 | 118 | // if no \r\n then wait for more data 119 | if (self.response.substr(-2) != "\r\n") return; 120 | 121 | // this fixes the bugged DATA RETRIEVE response 122 | if (isNaN(self.responseCode)) { 123 | self.responseCode = 201; 124 | self.responseMessage = "Response Code is not a number."; 125 | } 126 | 127 | if (self.responseMessage === "") { 128 | self.responseMessage = self.response.substr(0, self.response.indexOf("\r\n")); 129 | self.response = self.response.substr(self.responseMessage.length + 2); 130 | } 131 | 132 | // 200 needs \r\n\r\n 133 | if (self.responseCode == 200 && self.response.substr(-4) != "\r\n\r\n") return; 134 | 135 | // strip trailing \r\n 136 | while (self.response.substr(-2) == "\r\n") { 137 | self.response = self.response.substr(0, self.response.length - 2); 138 | } 139 | 140 | // response is complete remove command from queue 141 | var item = commandQueue.shift(); 142 | 143 | if (!item) { 144 | self.log("ERROR: Not sure what this data is", self.response); 145 | return; 146 | } 147 | 148 | var cb = item.cb || false; 149 | 150 | // check for error 151 | if ( 152 | (self.responseCode >= 400) && 153 | (self.responseCode < 600) 154 | ) { 155 | // callback with error 156 | if (cb) { 157 | cb(new Error(self.responseMessage)); 158 | } 159 | 160 | finishedCommand(self); 161 | 162 | return; 163 | } 164 | 165 | if (cb) { 166 | if (self.response !== "") { 167 | cb(null, self.response); 168 | } else { 169 | cb(null); 170 | } 171 | } 172 | 173 | finishedCommand(self); 174 | }); 175 | }; 176 | 177 | // ccg.disconnect 178 | // -------------- 179 | // Empties command queue and closes connection to Caspar CG 180 | ccg.prototype.disconnect = function () { 181 | this.disconnecting = true; 182 | this.commandQueue = []; 183 | 184 | if (this.client && this.connected) { 185 | this.client.end(); 186 | } 187 | }; 188 | 189 | // ccg.sendCommand 190 | // --------------- 191 | // Sends raw command to CasparCG 192 | ccg.prototype.sendCommand = function (command, cb) { 193 | var self = this; 194 | var commandQueue = self.commandQueue; 195 | 196 | if (typeof(cb) != "function") cb = false; 197 | 198 | // check for connection first 199 | if (!self.connected) { 200 | if (cb) { 201 | cb(new Error("Not connected")); 202 | } 203 | 204 | return false; 205 | } 206 | 207 | if (command.substr(-2) == "\r\n") { 208 | if (cb) { 209 | cb(new Error("Invalid command")); 210 | } 211 | 212 | return false; 213 | } 214 | 215 | command += "\r\n"; 216 | 217 | commandQueue.push({cmd: command, cb: cb}); 218 | 219 | if (self.readyForNextCommand) { 220 | self.readyForNextCommand = false; 221 | self.log("Send:",commandQueue[0].cmd.substr(0, commandQueue[0].cmd.length - 2)); 222 | self.client.write(commandQueue[0].cmd); 223 | } 224 | }; 225 | }; 226 | -------------------------------------------------------------------------------- /lib/data.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (ccg) { 4 | 5 | // --- 6 | // ccg.xmlToDataObject 7 | // --- 8 | // create a Javascript object from CasparCG Template Data XML 9 | // 10 | // [CasparCG Template Data XML](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#Template_Data) 11 | ccg.prototype.xmlToDataObject = function (xml) { 12 | if (typeof(xml) !== "string") { 13 | return false; 14 | } 15 | 16 | if ( 17 | (xml.substr(0, "".length) != "") && 18 | (xml.substr(-"".length) != "") 19 | ) { 20 | return false; 21 | } 22 | 23 | var parts = xml.split("[\s]*"; 63 | out += ""; 64 | out += ""; 65 | } 66 | 67 | out += ""; 68 | 69 | out = out.replace(/"/g, "\\\""); 70 | 71 | return out; 72 | }; 73 | 74 | // --- 75 | // ccg.listData 76 | // --- 77 | // Returns a list of stored data names 78 | // 79 | // [DATA LIST](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#DATA_LIST) 80 | ccg.prototype.listData = function (cb) { 81 | var self = this; 82 | 83 | // callback is required 84 | if (typeof(cb) != "function") { 85 | self.log("Invalid arguments"); 86 | return; 87 | } 88 | 89 | self.sendCommand("DATA LIST", function (err, data) { 90 | if (err) { 91 | cb(err); 92 | return; 93 | } 94 | 95 | data = data.split("\r\n"); 96 | cb(null, data); 97 | }); 98 | }; 99 | 100 | // --- 101 | // ccg.storeData 102 | // --- 103 | // Store datatset 104 | // 105 | // [DATA STORE](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#DATA_STORE) 106 | ccg.prototype.storeData = function (name, data, cb) { 107 | var xml = this.datObJectToXml(data); 108 | 109 | this.sendCommand("DATA STORE " + name + " \"" + xml + "\"", cb); 110 | }; 111 | 112 | // --- 113 | // ccg.loadData 114 | // --- 115 | // Load datatset 116 | // 117 | // [DATA RETRIEVE](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#DATA_RETRIEVE) 118 | ccg.prototype.loadData = function (name, cb) { 119 | var self = this; 120 | 121 | // callback is required 122 | if (typeof(cb) != "function") { 123 | self.log("Invalid arguments"); 124 | return; 125 | } 126 | 127 | this.sendCommand("DATA RETRIEVE " + name, function (err, data) { 128 | if (err) { 129 | cb(err); 130 | 131 | return; 132 | } 133 | 134 | var obj = self.xmlToDataObject(data); 135 | 136 | cb(null, obj); 137 | }); 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /lib/mixer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (ccg) { 4 | 5 | ccg.prototype.mixerFill = function (channel, grid, cb) { 6 | var self = this; 7 | 8 | if (typeof(channel) == "number") { 9 | channel = channel.toString(); 10 | } 11 | 12 | if (typeof(channel) != "string" || !/[0-9]+-[0-9]+/.test(channel)) { 13 | self.log("Invalid channel"); 14 | return cb && cb(new Error("Invalid channel")); 15 | } 16 | 17 | var cmd = "MIXER " + channel + " FILL "; 18 | 19 | cmd += (grid.x || "0") + " "; 20 | cmd += (grid.y || "0") + " "; 21 | cmd += (grid.w || "0") + " "; 22 | cmd += (grid.h || "0"); 23 | 24 | self.sendCommand(cmd, cb); 25 | }; 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /lib/playout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (ccg) { 4 | var sendPlayoutCommand = function (self, cmd, channel, file, options, cb) { 5 | if (typeof(channel) == "number") { 6 | channel = channel.toString(); 7 | } 8 | 9 | if (typeof(channel) != "string" || !/[0-9]+-[0-9]+/.test(channel)) { 10 | self.log("Invalid channel"); 11 | return cb && cb(new Error("Invalid channel")); 12 | } 13 | 14 | cmd += " " + channel; 15 | 16 | if (typeof(file) != "string") { 17 | cb = options; 18 | options = file; 19 | file = false; 20 | } 21 | 22 | if (typeof(options) != "object") { 23 | cb = options; 24 | options = {}; 25 | } 26 | 27 | if (typeof(cb) != "function") cb = false; 28 | 29 | if (file) cmd += " \"" + file.replace(/\\/g, "\\\\") + "\""; 30 | 31 | if (options.loop) cmd += " LOOP"; 32 | 33 | // this should be validated 34 | if (options.transition) cmd += " " + options.transition; 35 | 36 | if (options.seek && parseInt(options.seek, 10) > 0) cmd += " SEEK " + options.seek; 37 | 38 | if (options.length && parseInt(options.seek, 10) > 0) cmd += " SEEK " + options.length; 39 | 40 | if (options.filter) cmd += " " + options.filter; 41 | 42 | if (options.auto) cmd += " AUTO"; 43 | 44 | self.sendCommand(cmd, cb); 45 | }; 46 | 47 | // --- 48 | // ccg.load 49 | // --- 50 | // Load media file 51 | // [LOAD](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#LOAD) 52 | ccg.prototype.load = function (channel, file, options, cb) { 53 | sendPlayoutCommand(this, "LOAD", channel, file, options, cb); 54 | }; 55 | 56 | // --- 57 | // ccg.loadBg 58 | // --- 59 | // Load media file into background 60 | // [LOADBG](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#LOADBG) 61 | ccg.prototype.loadBg = function (channel, file, options, cb) { 62 | sendPlayoutCommand(this, "LOADBG", channel, file, options, cb); 63 | }; 64 | 65 | // --- 66 | // ccg.play 67 | // --- 68 | // Play media file 69 | // [PLAY](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#PLAY) 70 | ccg.prototype.play = function (channel, file, options, cb) { 71 | sendPlayoutCommand(this, "PLAY", channel, file, options, cb); 72 | }; 73 | 74 | // --- 75 | // ccg.pause 76 | // --- 77 | // Pauses a media file on a channel or layer 78 | // 79 | // [PAUSE](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#PAUSE) 80 | ccg.prototype.pause = function (channel, cb) { 81 | this.sendCommand("PAUSE " + channel, cb); 82 | }; 83 | 84 | // --- 85 | // ccg.resume 86 | // --- 87 | // Resumes playback of a foreground clip previously paused with the PAUSE command. 88 | // 89 | // [PAUSE](http://casparcg.com/wiki/CasparCG_2.1_AMCP_Protocol#RESUME) 90 | ccg.prototype.resume = function (channel, cb) { 91 | this.sendCommand("RESUME " + channel, cb); 92 | }; 93 | 94 | // --- 95 | // ccg.stop 96 | // --- 97 | // Stops a media file on a channel or layer 98 | // 99 | // [STOP](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#STOP) 100 | ccg.prototype.stop = function (channel, cb) { 101 | this.sendCommand("STOP " + channel, cb); 102 | }; 103 | 104 | // --- 105 | // ccg.clear 106 | // --- 107 | // Clears a channel or layer 108 | // 109 | // [CLEAR](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#CLEAR) 110 | ccg.prototype.clear = function (channel, cb) { 111 | this.sendCommand("CLEAR " + channel, cb); 112 | }; 113 | 114 | // --- 115 | // ccg.updateMediaProperty 116 | // --- 117 | // Changes property of media playout 118 | // 119 | // [CALL](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#CALL) 120 | ccg.prototype.updateMediaProperty = function (channel, key, value, cb) { 121 | if (typeof(value) == "boolean") { 122 | value = (value) ? 1 : 0; 123 | } 124 | 125 | if (typeof(channel) == "number") { 126 | channel = channel.toString(); 127 | } 128 | 129 | if (typeof(channel) != "string" || !/[0-9]+-[0-9]+/.test(channel)) { 130 | this.log("Invalid channel"); 131 | return cb && cb(new Error("Invalid channel")); 132 | } 133 | 134 | var command = "CALL " + channel + " " + key + " " + value; 135 | this.sendCommand(command, cb); 136 | }; 137 | 138 | // --- 139 | // ccg.swap 140 | // --- 141 | // Swaps layers between channels (both foreground and background will be swapped) 142 | // 143 | // [CLEAR](http://casparcg.com/wiki/CasparCG_2.1_AMCP_Protocol#SWAP) 144 | ccg.prototype.swap = function (src, dest, cb) { 145 | this.sendCommand("SWAP " + src + " " + dest, cb); 146 | }; 147 | 148 | // --- 149 | // ccg.print 150 | // --- 151 | // Saves an RGBA PNG bitmap still image of the contents of the specified channel in the media folder 152 | // 153 | // [CLEAR](http://casparcg.com/wiki/CasparCG_2.1_AMCP_Protocol#PRINT) 154 | ccg.prototype.print = function (channel, cb) { 155 | this.sendCommand("PRINT " + channel, cb); 156 | }; 157 | 158 | // --- 159 | // ccg.print 160 | // --- 161 | // Changes the log level of the server (trace,debug,info,warning,error,fatal) 162 | // 163 | // [CLEAR](http://casparcg.com/wiki/CasparCG_2.1_AMCP_Protocol#LOG_LEVEL) 164 | ccg.prototype.logLevel = function (level, cb) { 165 | this.sendCommand("LOG LEVEL " + level, cb); 166 | }; 167 | 168 | }; 169 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var xml = require("./xml2json"); 4 | var _ = require("underscore"); 5 | 6 | module.exports = function (ccg) { 7 | // ccg.getMediaFiles 8 | // --- 9 | // Returns info about all available media files 10 | // 11 | // [CLS](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#CLS) 12 | ccg.prototype.getMediaFiles = function (cb) { 13 | var self = this; 14 | 15 | // callback is required 16 | if (typeof(cb) != "function") { 17 | self.log("Invalid arguments"); 18 | return; 19 | } 20 | 21 | self.sendCommand("CLS", function (err, data) { 22 | if (err) { 23 | cb(err); 24 | return; 25 | } 26 | 27 | var files = data.split("\r\n"); 28 | 29 | var out = []; 30 | 31 | for (var index in files) { 32 | var file = files[index]; 33 | 34 | // split file parts and strip quotes 35 | file = /"([^"]*)"[\s]+([^\s]*)[\s]+([^\s]*)[\s]+([^\s]*)/.exec(file); 36 | 37 | out.push({ 38 | file: file[1], 39 | type: file[2], 40 | length: file[3], 41 | date: new Date( 42 | parseInt(file[4].substr(0,4), 10), 43 | parseInt(file[4].substr(4, 2), 10) - 1, // month is 0 based in js 44 | parseInt(file[4].substr(6, 2), 10), 45 | parseInt(file[4].substr(8, 2), 10), 46 | parseInt(file[4].substr(10, 2), 10), 47 | parseInt(file[4].substr(12, 2), 10) 48 | ) 49 | }); 50 | } 51 | 52 | cb(null, out); 53 | }); 54 | }; 55 | 56 | // --- 57 | // ccg.getMediaFileInfo 58 | // --- 59 | // Returns info about all media files matching filename 60 | // 61 | // [CINF](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#CINF) 62 | ccg.prototype.getMediaFileInfo = function (filename, cb) { 63 | var self = this; 64 | 65 | if (typeof(filename) != "string") { 66 | self.log("Invalid arguments"); 67 | return; 68 | } 69 | 70 | // callback is required 71 | if (typeof(cb) != "function") { 72 | self.log("Invalid arguments"); 73 | return; 74 | } 75 | 76 | self.sendCommand("CINF \"" + filename + "\"", function (err, data) { 77 | if (err) { 78 | if (cb) cb(err); 79 | return; 80 | } 81 | 82 | var files = data.split("\r\n"); 83 | 84 | var out = []; 85 | 86 | for (var index in files) { 87 | var file = files[index]; 88 | 89 | // split file parts and strip quotes 90 | file = /"([^"]*)"[\s]+([^\s]*)[\s]+([^\s]*)[\s]+([^\s]*)/.exec(file); 91 | 92 | out.push({ 93 | file: file[1], 94 | type: file[2], 95 | length: file[3], 96 | date: new Date( 97 | parseInt(file[4].substr(0,4), 10), 98 | parseInt(file[4].substr(4, 2), 10) - 1, // month is 0 based in js 99 | parseInt(file[4].substr(6, 2), 10), 100 | parseInt(file[4].substr(8, 2), 10), 101 | parseInt(file[4].substr(10, 2), 10), 102 | parseInt(file[4].substr(12, 2), 10) 103 | ) 104 | }); 105 | } 106 | 107 | cb(null, out); 108 | }); 109 | }; 110 | 111 | // --- 112 | // ccg.getTemplates 113 | // --- 114 | // Returns info about all available templates 115 | // Returns info about all available templates 116 | // 117 | // [TLS](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#TLS) 118 | ccg.prototype.getTemplates = function (folder, cb) { 119 | var self = this; 120 | 121 | if (typeof(folder) != "string") { 122 | cb = folder; 123 | folder = false; 124 | } 125 | 126 | // callback is required 127 | if (typeof(cb) != "function") { 128 | self.log("Invalid arguments"); 129 | return; 130 | } 131 | 132 | var command = "TLS"; 133 | 134 | if (folder) command += " " + folder; 135 | 136 | self.sendCommand(command, function (err, data) { 137 | if (err) { 138 | cb(err); 139 | return; 140 | } 141 | 142 | var files = data.split("\r\n"); 143 | 144 | var out = []; 145 | 146 | for (var index in files) { 147 | var file = files[index]; 148 | 149 | // split file parts and strip quotes 150 | file = /"([^"]*)"\s([^\s]*)\s([^\s]*)/.exec(file); 151 | 152 | out.push({ 153 | file: file[1], 154 | length: file[2], 155 | date: new Date( 156 | parseInt(file[3].substr(0,4), 10), 157 | parseInt(file[3].substr(4, 2), 10) - 1, // month is 0 based in js 158 | parseInt(file[3].substr(6, 2), 10), 159 | parseInt(file[3].substr(8, 2), 10), 160 | parseInt(file[3].substr(10, 2), 10), 161 | parseInt(file[3].substr(12, 2), 10) 162 | ) 163 | }); 164 | } 165 | 166 | cb(null, out); 167 | }); 168 | }; 169 | 170 | // --- 171 | // ccg.getTemplateInfo 172 | // --- 173 | // Returns info about a template 174 | // 175 | // [INFO](http://casparcg.com/wiki/CasparCG_2.0_AMCP_Protocol#INFO) 176 | ccg.prototype.getTemplateInfo = function (file, cb) { 177 | var self = this; 178 | 179 | if (typeof(file) != "string") { 180 | self.log("Invalid arguments"); 181 | return cb && cb(new Error("Invalid arguments")); 182 | } 183 | 184 | // callback is required 185 | if (typeof(cb) != "function") { 186 | self.log("Invalid arguments"); 187 | return; 188 | } 189 | 190 | self.sendCommand("INFO TEMPLATE \"" + file.replace("\\", "\\\\") + "\"", function (err, data) { 191 | if (err) { 192 | cb(err); 193 | return; 194 | } 195 | 196 | if (!data) { 197 | cb("err" + file); 198 | return; 199 | } 200 | 201 | var parts = data.split("\n"); 202 | var part; 203 | data = {fields: []}; 204 | 205 | for (var i in parts) { 206 | part = /[\s]*/.exec(parts[i]); 207 | 208 | if (part) { 209 | data.fields.push(part[1]); 210 | } 211 | 212 | part = /[\s]* index + 1) { 23 | obj = object[path[index]]; 24 | 25 | // we always want the last object in an array 26 | if (_.isArray(obj)) obj = _.last(obj); 27 | 28 | makePath(obj, path, index + 1); 29 | } else { 30 | obj = object[path[index]]; 31 | 32 | if (!obj) { 33 | // object doesn't exist yet so make it 34 | object[path[index]] = {}; 35 | } else { 36 | if (!_.isArray(obj)) { 37 | // object isn't an array yet so make an array with the object as the first element 38 | object[path[index]] = [obj]; 39 | } 40 | 41 | // append the new object 42 | object[path[index]].push({}); 43 | } 44 | } 45 | } 46 | 47 | // helper function to set the value of a path 48 | function setValueForPath(object, path, value, index) { 49 | index = index || 0; 50 | 51 | if (path.length > index + 1) { 52 | var obj = object[path[index]]; 53 | 54 | // we always want the last object in an array 55 | if (_.isArray(obj)) obj = _.last(obj); 56 | 57 | setValueForPath(obj, path, value, index + 1); 58 | } else { 59 | // found the object so set its value 60 | object[path[index]] = value; 61 | } 62 | } 63 | 64 | parser.onerror = function (err) { 65 | // should pass this to the callback 66 | console.log("XML OnError:", err); 67 | }; 68 | 69 | parser.ontext = function (value) { 70 | setValueForPath(out, currentPath, value); 71 | }; 72 | 73 | parser.onopentag = function (node) { 74 | if (!out) { 75 | out = {}; 76 | } else { 77 | // add the current node to the path 78 | currentPath.push(node.name.toLowerCase()); 79 | 80 | // create the path 81 | makePath(out, currentPath); 82 | } 83 | }; 84 | 85 | parser.onclosetag = function () { 86 | // back out 87 | currentPath.pop(); 88 | }; 89 | 90 | parser.onend = function () { 91 | cb(null, out); 92 | }; 93 | 94 | try { 95 | if (!xml) { 96 | return cb(new Error("No data")); 97 | } 98 | 99 | parser.write(xml).close(); 100 | } catch (err) { 101 | cb(err); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caspar-cg", 3 | "description": "Caspar CG to Node interface", 4 | "version": "0.1.0", 5 | "author": "Kevin Smith (@respectTheCode)", 6 | "license": "MIT", 7 | "homepage": "https://github.com/respectTheCode/node-caspar-cg", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/respectTheCode/node-caspar-cg.git" 11 | }, 12 | "scripts": { 13 | "test": "grunt mochaTest" 14 | }, 15 | "dependencies": { 16 | "underscore": "~1.8.3", 17 | "sax": "~1.1.1" 18 | }, 19 | "devDependencies": { 20 | "grunt": "~0.4.5", 21 | "grunt-contrib-clean": "~0.4.0", 22 | "grunt-contrib-watch": "~0.3.1", 23 | "grunt-mocha-test": "~0.6.3", 24 | "grunt-contrib-jshint": "~0.7.1", 25 | "grunt-jscs": "2.8.0", 26 | "jshint-stylish": "~0.1.3", 27 | "load-grunt-tasks": "~0.2.0", 28 | "async": "~0.2.9", 29 | "chai": "~1.8.1" 30 | }, 31 | "engines" : { 32 | "node" : ">=4.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var CasparCG = require("./"); 2 | var async = require("async"); 3 | 4 | ccg = new CasparCG({ 5 | host: "localhost", 6 | port: 5250, 7 | debug: true 8 | }); 9 | 10 | ccg.connect(function () { 11 | ccg.play("1-1", "tests/test/AMB"); 12 | 13 | // ccg.getTemplates(function (err, templates) { 14 | // async.forEach(templates, function (template, cb) { 15 | // ccg.getTemplateInfo(template.file, function (err, data) { 16 | // console.log(template.file, data); 17 | // cb(err); 18 | // }); 19 | // }, function (err) { 20 | // console.log("done", err); 21 | // }); 22 | // }); 23 | 24 | // ccg.listData(function (err, data) { 25 | // console.log("list", data); 26 | // }); 27 | 28 | // ccg.storeData("SomeData", { 29 | // f0: "FirstName LastName", 30 | // f1: "Something about FirstName LastName", 31 | // f2: new Date().toString() 32 | // }, function (err) { 33 | // ccg.loadData("SomeData", function (err, data) { 34 | // console.log("data:", data); 35 | // }); 36 | 37 | // ccg.loadTemplate("1-2", "Gymnastics/LT-SINGLE NAME", "SomeData", function () { 38 | // ccg.playTemplate("1-2"); 39 | 40 | // setTimeout(function () { 41 | // ccg.updateTemplateData("1-2", {f0: "Someones Name", f1: "Some title"}); 42 | // }, 2 * 1000); 43 | 44 | // setTimeout(function () { 45 | // ccg.stopTemplate("1-2"); 46 | // }, 4 * 1000); 47 | // }); 48 | // }); 49 | 50 | // ccg.getMediaFiles(function (err, serverInfo) { 51 | // console.log("getMediaFiles", serverInfo); 52 | // }); 53 | 54 | // ccg.getMediaFileInfo("AMB 1080i60", function (err, serverInfo) { 55 | // console.log("getMediaFileInfo", serverInfo); 56 | // }); 57 | 58 | // ccg.getTemplates(function (err, serverInfo) { 59 | // console.log("info", serverInfo); 60 | // }); 61 | 62 | // ccg.info(function (err, serverInfo) { 63 | // console.log("info", serverInfo); 64 | // }); 65 | 66 | // ccg.info("1", function (err, serverInfo) { 67 | // console.log("info 1", serverInfo); 68 | // }); 69 | 70 | // ccg.info("1-1", function (err, serverInfo) { 71 | // console.log("info 1-1", serverInfo); 72 | // }); 73 | 74 | setTimeout(function () { 75 | ccg.clear("1", function () { 76 | ccg.disconnect(); 77 | }); 78 | }, 5 * 1000); 79 | }); 80 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": true, 3 | "unused": false, 4 | "node": true, 5 | "globals": { 6 | "describe": false, 7 | "beforeEach": false, 8 | "before": false, 9 | "inject": false, 10 | "after": false, 11 | "it": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var net = require("net"); 4 | var assert = require("chai").assert; 5 | 6 | var CasparCG = require("../"); 7 | 8 | var debug = true; 9 | var port = 8000; 10 | 11 | describe("connection", function () { 12 | var ccg1; 13 | var server1; 14 | var connection1; 15 | 16 | before(function (done) { 17 | server1 = new net.createServer(); 18 | server1.listen(port); 19 | 20 | server1.on("connection", function (socket) { 21 | connection1 = socket; 22 | done(); 23 | }); 24 | 25 | ccg1 = new CasparCG({ 26 | host: "localhost", 27 | port: port, 28 | debug: debug 29 | }); 30 | 31 | ccg1.connect(); 32 | port++; 33 | }); 34 | 35 | it("should connect and send 1 command", function (done) { 36 | connection1.on("data", function (data) { 37 | assert.equal(data.toString(), "PLAY 1-0 \"AMB\"\r\n", "data is a play command"); 38 | this.write("202 Play OK\r\n"); 39 | connection1.removeAllListeners("data"); 40 | }); 41 | 42 | ccg1.play("1-0", "AMB", done); 43 | }); 44 | 45 | it("should set error for callback when receiving an error status", function (done) { 46 | connection1.on("data", function (data) { 47 | this.write("501 PLAY FAILED\r\n"); 48 | connection1.removeAllListeners("data"); 49 | }); 50 | 51 | ccg1.play("1-0", "nonexistingfile", function(err) { 52 | assert(err); 53 | done(); 54 | }) 55 | }); 56 | 57 | describe("with 2 connections", function () { 58 | var ccg2; 59 | var server2; 60 | var connection2; 61 | var ccg3; 62 | var server3; 63 | var connection3; 64 | 65 | before(function (done) { 66 | server2 = new net.createServer(); 67 | server2.listen(port); 68 | 69 | server2.on("connection", function (socket) { 70 | connection2 = socket; 71 | 72 | server3 = new net.createServer(); 73 | server3.listen(port); 74 | 75 | server3.on("connection", function (socket) { 76 | connection3 = socket; 77 | done(); 78 | }); 79 | 80 | ccg3 = new CasparCG({ 81 | host: "localhost", 82 | port: port, 83 | debug: debug 84 | }); 85 | 86 | ccg3.connect(); 87 | port++; 88 | }); 89 | 90 | ccg2 = new CasparCG({ 91 | host: "localhost", 92 | port: port, 93 | debug: debug 94 | }); 95 | 96 | ccg2.connect(); 97 | port++; 98 | }); 99 | 100 | var cleanup = function () { 101 | connection1.removeAllListeners("data"); 102 | connection2.removeAllListeners("data"); 103 | connection3.removeAllListeners("data"); 104 | }; 105 | 106 | it("should only send to connection 1", function (done) { 107 | connection1.on("data", function (data) { 108 | assert.equal(data.toString(), "PLAY 1-1 \"AMB\"\r\n", "data is a play command"); 109 | this.write("202 Play OK\r\n"); 110 | cleanup(); 111 | }); 112 | connection2.on("data", function (data) { 113 | assert(false, "should not get data on 1"); 114 | cleanup(); 115 | }); 116 | connection3.on("data", function (data) { 117 | assert(false, "should not get data on 2"); 118 | cleanup(); 119 | }); 120 | 121 | ccg1.play("1-1", "AMB", done); 122 | }); 123 | 124 | it("should only send to connection 2", function (done) { 125 | connection2.on("data", function (data) { 126 | assert.equal(data.toString(), "PLAY 2-1 \"AMB\"\r\n", "data is a play command"); 127 | this.write("202 Play OK\r\n"); 128 | cleanup(); 129 | }); 130 | connection1.on("data", function (data) { 131 | assert(false, "should not get data on 0"); 132 | cleanup(); 133 | }); 134 | connection3.on("data", function (data) { 135 | assert(false, "should not get data on 2"); 136 | cleanup(); 137 | }); 138 | 139 | ccg2.play("2-1", "AMB", done); 140 | }); 141 | 142 | it("should only send to connection 3", function (done) { 143 | connection3.on("data", function (data) { 144 | assert.equal(data.toString(), "PLAY 3-1 \"AMB\"\r\n", "data is a play command"); 145 | this.write("202 Play OK\r\n"); 146 | cleanup(); 147 | }); 148 | connection1.on("data", function (data) { 149 | assert(false, "should not get data on 0"); 150 | cleanup(); 151 | }); 152 | connection2.on("data", function (data) { 153 | assert(false, "should not get data on 1"); 154 | cleanup(); 155 | }); 156 | 157 | ccg3.play("3-1", "AMB", done); 158 | }); 159 | }); 160 | }); 161 | --------------------------------------------------------------------------------