Need help moving from Flash to HTML templates

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.

5 Likes