├── .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 | [](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 |
--------------------------------------------------------------------------------