Lottie crawl is stuttering

I built up a ticker-crawl in AfterEffects (1080p50), exported everything by the bodymovin plugin and integrated it to an html-template.

When playing my template on the decklink-output the crawl is continually stuttering (or is it a flicker?)
When playing my template in the chrome browser without casparcg everything works fine.

I’m using casparcg server version 2.2.0 stable
The configuration of my caspar-channel is 1080i50

In the attachement there is an example which plays the lottie-player after DOMLoad with the default text.
https://drive.google.com/file/d/1-1TvKtBOmGHEidwpmMKkQkgtg61Yr7O0/view?usp=sharing

Any suggestions what could be wrong?

Or is there another rocket proof technic to get smooth ticker-crawls in html-templates?

sometimes I have the same issue, it seems like that CasparCG’s html performance need to improve, especially when you have gradient transparent html template.

Yes it seems to me that html producer lacks performance a bit.
Sometimes it takes very long that html templates are loading.

I had best experience to build templates with greensock library (in flash and html). But pure scripting isn’t a well tool for designers to build up animations. It really lacks a nice editor similar to after effects. And lottie isn’t really ready for complex dynamic graphics now. It still needs heavy javascript tweaking to build up shapes whos length depend on dynamic text. Or I struggle with dynamic ticker-graphics or there are problems with textareas to keep individual kerning of letters and so on. It would be a great effort if lottie accepted all expression objects from after effects. But it doesn’t.

Flash is dead. I sometimes think with its editor and integrated action script possibillities it wasn’t really that bad at all.

Try using the 2.3.0-rc and add the following to the config

<html>
    <enable-gpu>true</enable-gpu>
</html>

This may not work well without an nvidia gpu, which is known limitation and why it is not enabled by default

I did enable-gpu in 2.2.0 and in 2.3.0-rc.

In 2.2.0 there is no difference, it works but it’s not scrolling smooth.

In 2.3.0-rc the bodymovin player doesn’t show up. It throws the event DOMLoaded for my bodymovin-animation but it’s not showing up the animation in the casparcg output.

If I debug with the remote debugging in chrome the animation shows in the browser.

strange.
Is there a change in the html producer of version 2.3.0-rc?

In server 2.2.0 I’ve tested with an output 720p5000. In progressive mode the ticker runs smooth. But I need 1080i5000 output.

If it runs smooth in progressive mode you need to change the speed of the crawl to make it look good in interlace mode.

Or there is a performance issue, I think 2.2 had started doing everything internally as progressive so even if your channel is 50i the internal processing is 50p. 1080p50 is more than 2x the size pr. frame than 720p50 and is rendered twice per frame for 50i.

But also, like didi says above, the golden rule of tickers and other items that have continuous motion is to have a fixed pixels pr. frame move.

If watching the computers parameters cpu, ram, gpu then there is much headroom. I don’t think it’s a performance issue. But I get a clean crawl also when playing 1080i5000 in a virtual channel 2 and routing this one to channel 1 (interlaced, too) with the decklink output. I don’t really understand why.

And it only goes the right way if I use subframes enabled in the lottie player.

That is weird. You say, that a virtual interlaced channel routed to a Decklink interlaced channel makes the problem go away? How does it look? Like a doubled text? Showing everything twice? Then it would be a field sequence error That would then result in an issue on GitHub.

Yes, interlaced channel to interlaced channel did it for my lottie-crawl. There is no doubled frame then. The letters are clean. Before there was a constant flickering in the text and a bit of stuttering when scrolling from right to left. It must have been something with the field sequence. But playing interlaced video seems to be good no matter which channel I choose.

As I wrote on top of this topic you could try with this example:

https://drive.google.com/file/d/1-1TvKtBOmGHEidwpmMKkQkgtg61Yr7O0/view?usp=sharing

It works with no parameters it just plays as you call it.

If whitout the route trick it is a kind of interlace error, that can not be solved by changeing the speed of the crawl, it is probanly a bug in the way the HTML consumer handles interlace video.

I am in vaccation and have no access to anything except my iPhone, so I can not see the error anyway. :weary:

This sounds like it is caused by Inconsistent channel tick rate for interlaced decklink · Issue #1165 · CasparCG/server · GitHub which should be better in 2.3.0-rc

I tried it with server 2.3.0-rc on windows 10. Now it works pretty well. thanks for your advice!

@knoepsche Have you managed to get your crawl working with dynamic text?
If so would you be able to explain how you have done this - in After Effects or in js?

I imagine you would have to calculate the length of the text and move it across, then when it has fully moved across reset the position and start again. This would be easy to do in js but curious if you have done this in AE. Thanks.

If the design allows it I would try to combine Lottie (for complex logo and shape animations) and DOM / SVG for the expanding texts / backgrounds within the HTML template.

Best of both worlds.

Ticker.zip.ft (583.3 KB)

There is no upload for zip-files. I tried it by using another file-suffix (ft). You have to rename and decompress it and then there should be a folder structure with my solution.

I don’t use it in production but think it should work. If you deliver your template-folder also on a local webserver (e.g. on port 8005) you could try http://localhost:8005/template/Ticker/Ticker.html?debug=true
to open the html file in a chrome browser.

It works with PLAY, STOP and UPDATE commands on a caspar server.

If you have another really smooth html-solution for casparcg-tickers I would be interested in them, too.

Thanks for sharing your solution. I’ve had a good play with different solutions to achieve a smooth ticker that can handle text updates gracefully.

GSAP seemed promising to move the text smoothly, and I had a working version of a looping ticker that was buttery smooth, but had issues when it came to updating the text and keeping the position of the text correct. This is because GSAP uses durations for its movements. You can calculate the duration by doing ‘length of text / speed = duration’ and modify the GSAP timeline with the new time and x position data. You could probably get around this but it all became more effort than it was worth.

GSAP uses requestAnimationFrame at core so though I might as well use this too and use a constant speed (speed being how many pixels to move the text by per frame).


I’ve basically gone down the route of ‘see how long the text is, move it to the left at a constant speed until it is past the end point, then move it back’. using javascript to move it. You get the current position and move the text by getting/setting the element transform matrix(...).

When this plays in Chrome it doesn’t look very smooth but looks perfect when playing in CasparCG (tested in the latest version d3e3efef596c03dd834007de5b100b3e1d6e08ae)

In my setup I have the lottie config + webCG in one file and the ticker text movement in another file, which is why I use a simple but effective way of call functions like this:

if (window.playStarted) window.playStarted();

I’ve included both lottie config + webCG and ticker movement code below, you will most likely be interested in the “Ticker text bit”.
The AE animation has a single text line named `.f0’.

///////////////////////////////////////////////////////////////////////////////
// Lottie Player bit //////////////////////////////////////////////////////////

var animData = window.animationData; // Path to the animation json
var animContainer = document.getElementById('lottie-container');
var anim = lottie.loadAnimation({
  container: animContainer,
  renderer: 'svg',
  loop: false,
  autoplay: false,
  animationData: animData,
});

// on Update / Data
webcg.on('data', function (data) {
  if (window.updateStarted) window.updateStarted();

  // Update text here...

  if (window.updateComplete) window.updateComplete();
});

// on Play
webcg.on('play', function () {
  if (window.playStarted) window.playStarted();
  anim.removeEventListener('complete');

  // Play animation intro
  anim.playSegments([0, 50], true);

  anim.addEventListener('complete', function _playComplete(e) {
    anim.removeEventListener(e.type, _playComplete);
    if (window.playComplete) window.playComplete();
  });
});

// on Stop
webcg.on('stop', function () {
  if (window.stopStarted) window.stopStarted();
  anim.removeEventListener('complete');

  // Play animation outro
  anim.playSegments([51, 100], true);

  anim.addEventListener('complete', function _stopComplete(e) {
    anim.removeEventListener(e.type, _stopComplete);
    if (window.stopComplete) window.stopComplete();
  });
});

///////////////////////////////////////////////////////////////////////////////
// Ticker text bit ////////////////////////////////////////////////////////////

var tickerInitialized = false;
var tickerRunning = false;

var tickerSpeed = 8; // pixels per frame
var tickerPadding = 100;

var animContainerWidth = 0;
var tickerTextEle;
var tickerTextWidth = 0;
var tickerContainerWidth = 0;

var tickerTextTransformString;
var tickerTextTransformMatrixArray;
var tickerTextTransformMatrixObj = {};

var tickerXStartPosition;
var tickerXFinishPosition;

var animationFrame;

window.updateComplete = function () {
  initTicker();
};

window.playStarted = function () {
  if (!tickerRunning) {
    initTicker();

    // If the ticker is not running, move the text to the start position.
    tickerTextTransformMatrixObj.tx = tickerXStartPosition;
    updateTransform(tickerTextEle, tickerTextTransformMatrixObj);

    // start the movement
    animationFrame = requestAnimationFrame(moveTicker);
    tickerRunning = true;
  }
};

window.stopComplete = function () {
  cancelAnimationFrame(animationFrame);
  tickerRunning = false;
};

function initTicker() {
  if (!tickerInitialized) {
    animContainerWidth = animContainer.getBoundingClientRect().width;
    tickerTextEle = document.getElementsByClassName('f0')[0];
    tickerTextTransformString = tickerTextEle.getAttribute('transform'); // The 'transform' value needs to be only 'matrix(a,b,c,d,tx,ty)'. Fingers crossed BodyMovin does not change this.
    tickerTextTransformMatrixArray = JSON.parse(
      tickerTextTransformString.replace(/^\w+\(/, '[').replace(/\)$/, ']') // This will only work if the the 'transform' value is only 'matrix()'
    );
    tickerTextTransformMatrixObj.a = tickerTextTransformMatrixArray[0];
    tickerTextTransformMatrixObj.b = tickerTextTransformMatrixArray[1];
    tickerTextTransformMatrixObj.c = tickerTextTransformMatrixArray[2];
    tickerTextTransformMatrixObj.d = tickerTextTransformMatrixArray[3];
    tickerTextTransformMatrixObj.tx = tickerTextTransformMatrixArray[4];
    tickerTextTransformMatrixObj.ty = tickerTextTransformMatrixArray[5];

    tickerXInitPosition = tickerTextTransformMatrixObj.tx;
    tickerXStartPosition = animContainerWidth;

    tickerInitialized = true;
  }

  // We need to wait for the text to be updated before we can get the text width
  // The timeout duration needs to be atleast 1 frame
  setTimeout(() => {
    tickerTextWidth = tickerTextEle.getBoundingClientRect().width;

    tickerXFinishPosition =
      tickerXInitPosition - (tickerTextWidth + tickerPadding);
  }, 60);
}

function moveTicker() {
  tickerTextTransformMatrixObj.tx =
    tickerTextTransformMatrixObj.tx - tickerSpeed;

  // Move the ticker back to the start position if it has moved past the finish position
  if (tickerTextTransformMatrixObj.tx < tickerXFinishPosition)
    tickerTextTransformMatrixObj.tx = tickerXStartPosition;

  updateTransform(tickerTextEle, tickerTextTransformMatrixObj);

  // loop the movement
  animationFrame = requestAnimationFrame(moveTicker);
}

function updateTransform(ele, m) {
  var matrixString = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d +',' + m.tx + ',' + m.ty + ')';
  ele.setAttribute('transform', matrixString);
}

I like your idea of using an array of text, you could incorporate that into the above by joining them into a single string:

var textString = textFields.join('   ')

Sorry for the lengthy post, hope this is of some use!

3 Likes

Hi there,
Have you all managed to get properly smooth tickers with HTML templates at 1080i50? We’re still experiencing a lot of stutter and jumps with the 2.3.2 release.

The template is a bit heavy, with Webm videos on the background, (but even without the videos, the behaviour is pretty similar).

Thanks!