Need help moving from Flash to HTML templates

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.

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.

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?

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

@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.

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

@ouellettec what an amazing reply!

Off to process all this information.

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 ?

@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:

1 Like

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.

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

1 Like

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?

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

1 Like

I bet you are onto something with the sRGB output. I use SVG clipPath and masks for all the alpha stuff so no big deal there!


Scratch that, @hreinnbeck found the answer.

maybe. it was hard to find webm converter on that time and the one i got was with predefined presets

now i’m going to test it with handbrake thank you for correction.

thanks, now i’m definitely going to use HTML with WebM.

for quick answer we can use streaming and can we play directshow in html video tag ?

I found Microsoft’s docs on Direct Show here, it looks like it is a C++ API. Definitely a bit out of my league. Do you have any additional remcondation? And or a way to do this without C++ knowledge?
Also, I had an idea to create a local video stream with VLC then point the HTML video to it. Maybe that would work?

Use NDI’s Virtual Input or BMD WDM capture and then:


You can do VLC to NDI to Webcam to HTML. A long and sweet chain!
Works :ok_hand:t2: (VLC is a bit buggy when playing a list of videos, live streams work OK).

A couple of FYIs that might shorten those chains a little -

You can tap live video streams using Caspar’s native FFMPEG. I can’t remember the exact syntax but is more or less vanilla FFMPEG RTMP connection string. Local file playout is what CasparCG does. It’s just a matter of thinking about building your layers in CasparCG rundowns rather than HTML.

Depending on the version of CEF in your CasparCG build you can load and play a local file in HTML5. Here’s an example on JSFiddle A playlist could be run in JS within the template to avoid the VLC playlist bugs mentioned. Easy management too, just load a jsonp playlist.

Taking it further - WebRTC. It’s been a while since I was down that rabbit hole - again depending on the CEF & therefore Chromium version in your CasparCG build. You can receive a video stream from any WebRTC source. I’m pretty sure a video element can be the source for an outgoing WebRTC media stream - combined with the above local file HTML player it gets neato, no?

BTW - the above is all that’s really needed to build one’s own Skype TX equivalent. But with the benefit of the contributor not needing Skype on their end. Just a decent web browser - great for call-ins etc.

2 Likes

Most of the videos play well in templates (I used that here), I think that is not an issue.
Playing videos or live inputs in another Caspar layer won’t let you change the video parameters in sync with HTML templates (you can send the commands to both but dealing with that in the client side (both in the official as well as custom ones) is inefficient.
I suggested VLC because it gives you more versatile live input options (even TV tuners) but you can show any WDM input that way. NDI Virtual Input is very convenient for that matter.

That’s very cool!

1 Like

This is a very good example of that (it starts after 22s):

This is very difficult (read impossible) to do without templates that handle the input internally.

Here I've put together a simple example

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>LiveVideo</title>
<script type="text/javascript" src="gsap.min.js"></script>
<style>
html,
body
{
    background-color: transparent;
    overflow: hidden;
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

*,
*:before,
*:after
{
    box-sizing: inherit;
}
#container
{
    position: absolute;
    background: #333;
    border: .2vw #aaa outset;
	bottom: 20vh;
	right: 10vh;
	width: 20vw;
	height: 5.2vh;
    border-radius: 2vh;
    overflow: hidden;
    opacity: 0;
    box-shadow: black 0 1vh 2vh;
}



#textTag
{
    z-index: 1000;
    background: #aaa;
    color: #333;
    border-radius: 1vh;
    position: absolute;
    top: .5vw;
    left: .5vw;
    margin: 0;
    padding: .5vh 1vh;
    line-height: 1em;
    font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
    font-weight: bolder;
    font-size: 1vw;
}

#liveVideo
{
    z-index: 0;
	width: 100%;
	height: 100%;
    background: transparent;
    opacity: 0;
}
</style>
</head>
<body>
<div id="container">
    <p id="textTag"></p>
    <div>
        <video id="liveVideo" autoplay="true">
        </video>
    </div>
</div>
<script>
var video = document.querySelector("#liveVideo");
var text = document.querySelector("#textTag");


if (navigator.mediaDevices.getUserMedia) {       
	navigator.mediaDevices.getUserMedia({video: true})
  .then(function(stream) {
    video.srcObject = stream;
  })
  .catch(function(err0r) {
    console.log("Something went wrong!");
  });
}

function play()
{
    gsap.from("#container",{duration:1,y:"+=500",ease: "power3.out",delay:1});
    gsap.to("#container",{duration:.5,opacity:1,ease: "power3.out",delay:1});
    gsap.to("#container",{duration:1,css:{height:"20vh"},ease: "power3.inOut",delay:2.5});
    gsap.to("#liveVideo",{duration:1,opacity:1,ease: "power3.out",delay:3});
}

function update(data)
{
    jsdata = JSON.parse(data);
    if(jsdata.hasOwnProperty("f0")) text.textContent = jsdata.f0;
}
</script>
</body>
</html>

NDI