Before anyone reads the long replay
I have been playing with the idea of creating a guide that includes all the amazing work from Reto Inderbitzin, found here, and adding how to properly and fully setup the server and client app of building a CasparCG client. It would be lengthy and require a good knowledge of JavaScript and it’s frameworks but I would argue the rest of this post does as well. Please leave a heart or comment if this is someone you would want!!
@itod Here is an example using Caspar Connection and Express running on a NodeJS Server.
The graphic templates I build all follow this pattern.
- Load Template
- Send data to Template (Since this is via AMCP, as long as no one is sniffing the local machine, sending password should be safe)
- Template parses data and authenticates itself with the data over HTTPS (HTTPS not covered in this reply) .
- Template then can safely communicate with the server since we have verified with a password that the template is legit.
Dependencies:
Optional:
The easiest file, The Environment file
# Server Config
PORT=8080
ENV=DEV
# Database Config
DBURL=mongodb://localhost/YOUR-MONGODB_URL
# Caspar Config
CASPAR_HOST=127.0.0.1
CASPAR_PORT=5250
CASPAR_DEBUG=true
CASPAR_AUTO_CONNECT=false
CASPAR_AUTO_RECONNECT=true
SERVER_URL=http://localhost:8080/api
CASPAR_USERNAME=casparcg
CASPAR_PASSWORD=PASSWORD
The Sevrer app.js
file:
// Server imports
const app = require('express')(),
session = require('express-session'),
// Optional imports. Only needed if you are usng websockets w/ Socket.io
http = require('http').Server(app),
io = require('socket.io')(http);
// App imports
const bodyParser = require('body-parser'),
mongoose = require('mongoose'),
MongoStore = require('connect-mongo')(session),
passport = require('passport');
const {CasparCG} = require('casparcg-connection');
// Express body parsing
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Setup static file server
app.use('/api', require('express').static('public'));
// Load an .env file from the root directory if there is one
require('dotenv').config();
// Mongoose setup for MongoDB. This allows us to store data in a database.
// @param {string} process.env.DBURL - The location of the Database
// @param {object} - Modifies the way Mongoose interacts with MongoDB.
mongoose.connect(process.env.DBURL, {useNewUrlParser: true, useFindAndModify: false, useCreateIndex: true})
.then(() => {
if(process.env.ENV === 'PROD') mongoose.set('debug', false);
console.log(`Mongoose successfully connected to ${process.env.DBURL}`);
})
.catch(err => {
console.error(`Mongoose failed to connect to ${process.env.DBURL}`, err);
});
// Session Setup
app.use(session({
secret: 'YOUR SECRET',
resave: false,
saveUninitialized: false,
store: new MongoStore({
mongooseConnection: mongoose.connection
}),
cookie: {
httpOnly: false,
maxAge: 1000 * 60 * 60 * 8
}
}));
// Setup Passport to handle user authentication
app.use(passport.initialize());
// Setup Passport Sessions
app.use(passport.session());
// Initialize a new Caspar Connection instance.
const casparConnection = new CasparCG({
port: process.env.CASPAR_PORT,
host: process.env.CASPAR_HOST,
debug: process.env.CASPAR_DEBUG === 'true' ? true : false,
autoConnect: process.env.CASPAR_AUTO_CONNECT === 'true' ? true : false,
autoReconnect: process.env.CASPAR_AUTO_RECONNECT === 'true' ? true : false,
autoReconnectAttempts: 1
});
// VERY IMPORTANT. This next line allows you to get the same CasparConnection instance in other route files
app.set('casparConnection', casparConnection);
// If you are using socket.io, add this line
app.set('io', io);
// Routes Setup
app.use('/api', require('./routes'));
// Listen to a local port.
// If you are not using socket.io, replace http.listen with app.listen
http.listen(process.env.PORT, () => {
console.log(`ILEC Media Client running on port: ${process.env.PORT}`);
});
routes/index.js File (Handles our HTTP requests):
const router = require('express').Router();
router.post('/caspar/info', (req, res, next) => {
// If you need to send a new command to the Caspar Server,
// get the Caspar Connection instance stored earlier.
const casparConnection = req.app.get('casparConnection');
// From here you could save the info coming from the template to a DB
// or send it to the clients if you are using socket.io
// Example of saving the log and telling the use with socket.io
// Get the Socket.io instance stored earlier
const io = req.app.get('io');
myDB.create(newRecord, (error, result) => {
// Goes to the next route and handles the error there.
if(error) return next(error);
if(result) {
// In a perfect world, you would have stored the requester's
// socket ID in a cookie and sent that to the server.
// Then you could send a message to everyone but the sender but for now,
// we will send the message to everyone.
io.emit('onAirGraphic', {channel: req.body.channel, layer: req.body.layer, info: req.boy.info});
// Complete the previous request
return req.json(true);
}
});
});
Finally in your template.
// Global Varaibles
let serverUrl = 'http://localhost:8080',
// Whether or not we have authenticated ourselves
authenticated = false,
// Socket to connect the user to the Socket.IO connection
socket = null;
// Checks the result of an HTTP Request - SKIP THIS FOR CORE FUNCTIONALITY
// @param {object} res - The HTTP Result
// @returns {Promise Result} The result of the request
const checkRequestResult = (res, resolve, reject) => {
const contentType = res.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
if(res.status < 200 || res.status >= 300)
return res.json().then(result => reject({message: 'Error with json request', res: result}));
res.json().then(result => resolve(result));
} else {
if(res.status < 200 || res.status >= 300) return reject({message: 'Error with request', res});
return resolve(res);
}
}
// Handy fetch request. Only excepts Json
// @param {string} url - The URL to send the request to
// @param {string} method [n] - The method to send the request as
// @param {object} body - The object that needs the be stringifed and sent to the server
// @returns {Promise} An Promise resolving to an HTTP response from the server.
const Fetch = (url, method, body) => {
return new Promise((resolve, reject) => {
if(ENV === 'DEV') {
console.log('FETCH:', body);
return resolve();
}
if(!url) return reject();
const headers = {method: method.toUpperCase() || "GET", headers: {}};
if(method !== 'GET' && body) headers.headers["Content-Type"] = "application/json";
// This next line is really important and needs to be in every fetch request if you
// are using Passport for authentication on the server.
headers.credentials = 'same-origin';
if(body) { headers.body = JSON.stringify(body) }
return fetch(serverUrl + url, headers)
.then(res => checkRequestResult(res, resolve, reject))
.catch(error => checkRequestResult(error, resolve, reject));
});
};
// Authenticates the template with the Server connected to Caspar - SKIP THIS FOR CORE FUNCTIONALITY
// @param {string} username - Username provided by the Server to login
// @param {string} password - Password provided by the Server to login
// @param {string} [n] SERVER_URL - The Server Url that needs to be updated.
// @returns {Promise} An Promise resolving to an HTTP response from the server.
const authenticate = (username, password, SERVER_URL) => {
return new Promise((resolve, reject) => {
if(SERVER_URL) serverUrl = SERVER_URL;
if(ENV === 'DEV') return resolve();
fetch(serverUrl + '/auth/login', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({username, password})
})
.then(res => res.json())
.then(result => {
authenticated = true;
return resolve();
})
.catch(error => reject({messgage: error.message}));
});
}
// If you are using Socket.io - SKIP THIS FOR CORE FUNCTIONALITY
// Handles all requests being sent to the server via Socket.IO
// @param {object} body - The data to be passed to the web server
// @returns - New pormise that resolves once the message is sent
const socketMessage = (body) => {
return new Promise((resolve, reject) => {
socket.emit('info', {
name: templateInfo.name,
channel: templateInfo.playoutInfo.channel,
layer: templateInfo.playoutInfo.layer,
flashLayer: templateInfo.playoutInfo.flashLayer,
...body
});
return resolve();
});
}
// Then we make this handle logger function - SKIP THIS FOR CORE FUNCTIONALITY
// Logs to the templates logger
// @param {string} level - The severity of the message or error
// @param {string} status [n=null] Status to update the template to
// @param {message} error - Message message to pass to the logger
// @returns {Promise} An Promise resolving to an HTTP response from the server.
const logInfo = ({level, status, duration, message}) => {
if(authenticated) {
if(socket) {
return SocketMessage({
level, status, duration, message, error: null
});
} else {
return Fetch('/caspar/template/info', 'POST', {
level, status, duration, message, error: null
});
}
} else {
console.log({level, status, message});
}
};
// You can now call logInfo and if you are authenticated with the server
// the message will be sent through HTTP or websockets.
Last but not least, for using websockets you need to include a link to the socket script like so
<script rel="application/javascript" src="http://localhost:8082/socket.io/socket.io.js" />
What we accomplished:
- Setup a server to handle a single request
- Setup a basic socket-io communications
- Templates can communicate over HTTP or Websockets to a web server
What still needs to happen:
- Passport needs to be setup for authentication
- Mongoose needs to be setup to interact with MongoDB to save data and login users
- Caspar Connection error and log functions need to be setup
- HTTPS Setup
- Serve the HTML files from Express’ static file server
Please let me know if you have any questions!! Thank you everyone.