Need help moving from Flash to HTML templates

html
template

#1

I started playing a bit with HTML templates, trying to migrate an existing template from Flash to HTML.
The performance of the Flash producer was becoming unbearable so I tried some alternatives. Design wise it was very straightforward but stumbled upon some obstacles.

The template I used to test is very simple and it uses three main elements:
template

  • A background video loop, with IN and OUT animations at the beginning and at the end, respectively. The loop can change colors so in flash three different image sequences are saved and played at request.
  • Text (clipped inside the video) with auto shrink.
  • A shared masked video in a lower Caspar layer (that is not actually in the template). The playing clip is determined by the template and the mask layer uses a FILL command to clip the background to the top of the template, which is variable. Some templates are double in height, others are half in width. When transitioning from one template to another the background just keeps playing and the mask transforms to the new size.

The first two items work very, very well. Performance is excellent and it behaves as expected.

For the third item, in flash I would use Caspar’s ServerConnection library to ask the server what is playing and play the background movie and mask along the template.

Is there a similar library I could use in the template itself? I’ve seen Superfly’s library but I’m not sure how and if it works in a template.
How is the correct path to follow here?


#2

Looking at this section:

Some templates are double in height, others are half in width. When transitioning from one template to another the background just keeps playing and the mask transforms to the new size.

Do you have a video example of this? If all your trying to do is mask the graphic with a solid rectangle, than a SVG clipPath will work the best. I don’t think the server needs to know what graphic is playing to make the adjustment because the template can manage itself. Let me know if that wouldn’t work.

Also are you referencing this Superfly library (caspar-connection) or a different one?

I have never used Caspar’s ServerConnection library but if you are looking for real time communication over HTTP or websockets server let me know and I can post some starting points.

Also, I would also recommend changing the background videos to SVGs for even better performance and the ability to change the colors to anything you want in real time!


#3

This is an example:

Those are two separate templates (plus the tag above the first one).
When the second is added, the first just fade out and won’t remove the background playing in the server. The second one then fills the mask to its new position.

The templates are pre-made (a client bought a premade Ae template pack and handed it to me to make it work in Caspar) so I can’t really change the way that it is made. I never make such static templates. Most of the time vectors and filters work just fine.


#4

Just do the lower layer as video in the HTML template, then it is all self-contained. You have all the masking options of Flash available in HTML templates (and more!).


#5

I think that is a workaround, I have that as a plan B.

In the client, those templates work independently. there are two files the client handle as separate templates. The reason for not including the background in each template is to have a seamless transition between the two (the video don’t show that very well because of performance constraints in the PC I’m working).

So there’s no easy way of talking to the server from the HTML producer?


#6

Any special reason for using two templates? I usually do everything for a show as one template. That makes stuff like you are doing, where you need seamless transitions etc. a lot easier. It would look a lot cleaner than the fades you seem to be doing in the Youtube video.

Currently there is no way of talking to CCG from HTML templates except going through websockets, XHR, fetch or other ways to something that can talk to CCG. I’ve spent a lot of time trying to get it working, it’s the holy grail for Caspar.

If you are having performance issues, having everything in one template might also make things a lot more performant.


#7

I see. So it would be possible to write a WebSocket to AMCP server-client that receives messages and redirect them the other way?

I found that to be the easiest and lightest way to develop them. The client shows each one as a different option and each one has a set of parameters. That visually simplifies for the operators the actual differences:


Having two templates that load side by side saves me a lot of programming.
So I would need to pack everything in a single template and load the assets dynamically.


#8

Yeah websockets to telnet, or simply a http server that sends commands over telnet to CCG when different paths are hit (or read the command from a query parameter). You should be able to find something on Github that’s already been done to work with CCG.


#9

Option 3 would be to take the background video in question and move it to the lowest layer on a third template. It may not be best practice but then have the client send two commands when changing. One to the new graphic and one to the bottom video layer to update.
I would recommend using Fetch over websockets if you have a time constraint. Fetch doesn’t require any additional libraries since it is a Browser API.


#10

I would recommend using Fetch over websockets if you have a time constraint. Fetch doesn’t require any additional libraries since it is a Browser API.

Is there any example available how to use Fetch with custom CCG client?


#11

@itod give me an hour and I will post one here. You are using a custom web client using Caspar Connection?


#12

@ouellettec I’m using both custom C# client for production stuff, and testing the advantages of using web client, and this would be the tipping point to cross to JavaScript client. Looking forward to the example, thanks.


#13

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.


#14

@ouellettec what an amazing reply!

Off to process all this information.


#15

how are you doing this ? did you embedded video file to html template ?

Hi, is there any software or tool to covert video file to svg ?


#16

@noumaan is there any software or tool to covert video file to svg?

Not that I am aware of. Here are my recommendations for HTML Video in Caspar (I hope you wanted them :sweat_smile:).

For any video that is not a graphical element. Example, footage of a concert. Use WebM .webm and play it out of the HTML <video> element. You can use JS to dynamically load the video based on the data sent in the update function. Adobe media encoder, any number of online converters, and a JS library called Handbrake (Lets you upload and convert videos on a web server!) can be used to convert any video type to WebM.

For graphics like OP’s where the background is a bunch of graphical layers stacked on top of each other, you can create an SVG (Scalable Vector Graphic). Learning SVG’s takes some time but, the advantage is a full tool set of clipping, masking, and drawing tools.

I would start here on MDN’s SVG Getting Started Page.

And I took notes so here are the summaries.

Please let me know if you have any other question :relaxed:


#17

ahh right !

well when i was switching from flash to html so i tried webM video but quality was compressed and i think vp8 is designed for web that’s why it’s quality is not like lossless then i used casparcg video layer for background and html layer for text.

recently i was testing bodymovin and it was great. bodymovin is a after effects extension and it converts aftereffects timeline to svg. but sometimes i feel animation is laggy i think it is because of everything is rendering in realtime and also we are waiting for updated CEF version with opengl support in casparcg for more smooth performance.


#18

You must have been using crappy settings. VP8 & VP9 supports lossless compression, alpha and can be seeked frame accurately with the correct settings.


#19

Like @hreinnbeck said, try WebM again. Maybe type exporting with a higher bit rate? I use it for 1080P video and it looks fine. The only issue I have found is when duplicating the video frame to a <canvas> element, the colors are different. I am guessing this is because they are operating in different color spaces? I need to look into it more.

The benefit to moving the video out of Caspar’s video player is you can do stuff like this (breaking up the video) with HTML Canvas.

Here is the reference I used when setting it all up.

I also asked this question a while ago and was wondering if anyone had any updates on injecting live video into an HTML template?


#20

The video is likely YUV but canvas is RGB. You could try making the VP8/9 with sRGB colospace (though IIRC that won’t allow alpha), or you could see if you can make the colour adjustments in JS.

Quick search of “YUV canvas” https://github.com/brion/yuv-canvas