# Experiment: Offline rendering with CasparCG

**URL:** <https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525>\
**Category:** General\
**Tags:** graphics, html, server\
**Created:** [March 26, 2026, 11:22am UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525 "2026-03-26T11:22:44Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![olekristensen](https://casparcgforum.org/user_avatar/casparcgforum.org/olekristensen/32/2356_2.png) [@olekristensen](https://casparcgforum.org/u/olekristensen)\
**Post date:** [March 26, 2026, 11:22am UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/1 "2026-03-26T11:22:44Z")

</div>

Hey everyone,

I’ve been experimenting with using CasparCG as an offline renderer, compositing video and HTML graphics to file without the real-time constraint.

I built an **offline consumer** that skips the sync clock so the pipeline runs as fast as the encoder allows, and an **Ultralight HTML producer** (WebKit-based, but less features than CEF) that renders exactly one frame per channel tick, synchronously. No wall-clock dependency. One tick, one frame.

Caveat: **I only tested this in Docker on a Mac with software OpenGL, no GPU at all.** Numbers should be much better on Linux with a real GPU.

Results at 720p25 on software GL:

| Configuration | Speed | Frames | Deterministic |
| --- | --- | --- | --- |
| Video only (offline) | 2.3x | 501 | +1 |
| Video + Ultralight offline | 1.5x | 500 | exact |
| Video + CEF offline | 1.5x | 500 | no\* |
| FFmpeg consumer (any) | 0.9x | 501 | +1 |

\* CEF overlay frames are duplicated/skipped since its compositor is wall-clock driven.

Changes to existing code are small: 44 lines across 8 files. Ultralight is opt-in via cmake. CEF is untouched except for one JS injection per tick.

This is an experiment, not production-ready. I’d love feedback:

- Useful for your workflow?
- Templates that might break under Ultralight vs CEF?
- Anyone able to test on real hardware?

Repo _(beware lfs+submodules)_: [github.com/den-frie-vilje/casparCG](https://github.com/den-frie-vilje/casparCG)

PR: [den-frie-vilje/server#1](https://github.com/den-frie-vilje/server/pull/1)  
ultralight: ultralig.ht

Cheers,  
Ole Kristensen  
Den Frie Vilje

---

<div class="post-metadata">

**Author:** ![Julusian](https://casparcgforum.org/user_avatar/casparcgforum.org/julusian/32/12_2.png) [@Julusian](https://casparcgforum.org/u/Julusian)\
**Post date:** [March 26, 2026, 1:30pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/2 "2026-03-26T13:30:34Z")

</div>

I will happily accept a PR for the offline consumer part upstream, some users have been asking for this for years, so a piece of the puzzle that starts in that direction would be good.  
ultralight I am less sure on though, as the paid licensing of it makes it challenging to use, and any builds using it wont be GPL compliant (but that doesnt stop users/organisations building it for their own use).

On the offline consumer though, it looks like 90% of the code is the same as the current ffmpeg producer, the changes I can see from a diff are:

- Removing a bunch of comments/TODO notes
- Removing the realtime parameter/property (hardcoding to false)
- Adding a new queue\_depth property
- Some minor logging/naming changes
- The main part; changing how send() and has\_synchronization\_clock() work

So it looks to me that this could be achieved by possibly replacing the realtime boolean with an enum (file/stream/offline?), and then possibly either handling determining that inside the existing factory methods, or some new factory methods.

That will greatly reduce the maintenance compared to having a separate file with 90% duplication, and will ensure that any fixes made for one apply to the other too.

---

<div class="post-metadata">

**Author:** ![olekristensen](https://casparcgforum.org/user_avatar/casparcgforum.org/olekristensen/32/2356_2.png) [@olekristensen](https://casparcgforum.org/u/olekristensen)\
**Post date:** [March 26, 2026, 1:52pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/3 "2026-03-26T13:52:18Z")

</div>

Definitely, you’re right, there’s a lot of duplication. And as such the offline consumer isn’t that big of a modification. It’s really just because my first intention was to reduce the blast radius in the existing code while testing and comparing.

But I could definitely try to merge them with an enum like you suggest, and add that to a PR.

I’m aware that Ultralight might be a difficult dependency, but as for now it seems like the only html-renderer that will work in lock-step. That’s why I hid it behind a feature flag that’s off by default. I just really enjoyed having a path where the html render is deterministic. As such I didn’t intend it to be invasive. Unfortunately I don’t think there are many other alternatives out there, as browser engines tend to be beasts, and not even Ultralight is feature-complete compared to webkit.

I’ll do a pr against the main repo, and then we can work from there!

Thanks for the feedback.

/ o

---

<div class="post-metadata">

**Author:** ![noumaan](https://casparcgforum.org/letter_avatar_proxy/v4/letter/n/dfb087/32.png) [@noumaan](https://casparcgforum.org/u/noumaan)\
**Post date:** [March 26, 2026, 6:45pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/4 "2026-03-26T18:45:15Z")

</div>

Performance-wise, is **Ultralight** considered superior for the HTML producer because it bypasses the CPU-to-GPU IPC and memory-copying stages inherent in CEF?

---

<div class="post-metadata">

**Author:** ![olekristensen](https://casparcgforum.org/user_avatar/casparcgforum.org/olekristensen/32/2356_2.png) [@olekristensen](https://casparcgforum.org/u/olekristensen)\
**Post date:** [March 26, 2026, 9:31pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/5 "2026-03-26T21:31:56Z")

</div>

Hi noumaan

that’s not the issue, no.

The issue is that I haven’t found a way to make CEFs free-running thread sync up with the frame consumer. This leads to sync drift and for offline rendering it means that CEF will bot animate smoothly, as there’s no synchronous calls to upside and render the framebuffer of the html view.

But then ultralight has other issues.

---

<div class="post-metadata">

**Author:** ![gam\_ol](https://casparcgforum.org/user_avatar/casparcgforum.org/gam_ol/32/2364_2.png) [@gam\_ol](https://casparcgforum.org/u/gam_ol)\
**Post date:** [March 27, 2026, 1:51pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/6 "2026-03-27T13:51:35Z")

</div>

Hello Ole Kristensen,

This is a very interesting and useful solution!

I’m moving in a similar direction, but from a slightly different angle.  
CasparCG Logic Without CasparCG  
For the past six months, I’ve been creating templates for CasparCG. Ultimately, I realized that for simpler workflows, where a human acts as an intermediary for data entry, a full-fledged server might be overkill.

As a result, I developed Lottie Local Lower Thirds. It uses Playwright to render Lottie animations directly to ProRes 4444 with an alpha channel. No CasparCG installation or server setup is required. It’s designed as a portable tool for editors. The goal is to create a new template simply by replacing the JSON file.

But we’re still a long way off, and this is just an experiment and an attempt to simplify some parts of our work.

It would be interesting to try out your work.

---

<div class="post-metadata">

**Author:** ![didikunz](https://casparcgforum.org/user_avatar/casparcgforum.org/didikunz/32/7_2.png) [@didikunz](https://casparcgforum.org/u/didikunz)\
**Post date:** [March 27, 2026, 1:59pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/7 "2026-03-27T13:59:25Z")

</div>

I just want to point out, that the [OGraf](https://tech.ebu.ch/news/2025/04/ograf-the-ebu's-open-spec-for-cross-platform-graphics-integration) initiative of the EBU contains a specification for “non-realtime” rendering of templates, that supports this. Maybe interesting to look at that, before a Caspar specific workflow is implemented.

---

<div class="post-metadata">

**Author:** ![olekristensen](https://casparcgforum.org/user_avatar/casparcgforum.org/olekristensen/32/2356_2.png) [@olekristensen](https://casparcgforum.org/u/olekristensen)\
**Post date:** [March 27, 2026, 2:53pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/8 "2026-03-27T14:53:19Z")

</div>

Hi there

Thanks for all the feedback. I’ve also had a look of Ograf, and it’s a super clever way of making web components announce their parameters. I haven’t read the full spec and just experimented with frame synced renders for html, but yes, it would make sense to go straight to ograf, if there’s interest. Currently I’m just doing this for fun, but if someone wants to sponsor a real contribution, I’d be happy to go that direction. Also - I would dare say - if html rendering is becoming a broadcast standard, maybe it’s time to get involved with webkit or chromium to solve deterministic framestepping there once and for all.

---

<div class="post-metadata">

**Author:** ![niklas](https://casparcgforum.org/letter_avatar_proxy/v4/letter/n/34f0e0/32.png) [@niklas](https://casparcgforum.org/u/niklas)\
**Post date:** [April 3, 2026, 9:41pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/9 "2026-04-03T21:41:17Z")

</div>

I’ve also been looking into deterministic rendering lately and have some ideas.

1. To make “offline rendering” deterministic, which I think it should be for it to be worthwhile, I think we need to have a global config option that says whether the server is running in real-time (what we have now) or not. If not, the mixer should always wait until all producers have produced their next frame, the concept of “late frames” should not be on the table in that mode.
2. Regarding deterministic html rendering with CEF, I’ve been doing some experimentation based on this [Rendering in Headless Chrome - Google Docs](https://docs.google.com/document/d/1PppegrpXhOzKKAuNlP6XOEnviXFGUiX2hop00Cxcv4o/edit?tab=t.0#bookmark=id.dukbomwxpb3j) and this [Chrome DevTools Protocol - Emulation domain](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setVirtualTimePolicy) , and the feeling so far is positive.

---

<div class="post-metadata">

**Author:** ![niklas](https://casparcgforum.org/letter_avatar_proxy/v4/letter/n/34f0e0/32.png) [@niklas](https://casparcgforum.org/u/niklas)\
**Post date:** [October 6, 2026, 1:03pm UTC](https://casparcgforum.org/t/experiment-offline-rendering-with-casparcg/7525/10 "2026-10-06T13:03:38Z")

</div>

Like I said before, I’ve also been working on deterministic rendering. I’ve now have gotten to the point that there is a branch, fairly well tested, where this works. There is quite a lot of stuff going on, and I would really have liked for this to be “slimmer”. But, one thing leads to another…

Anyway, there is a PR against the nxtedition fork of the casparcg server that has:

1. Different pacing strategies for output channels
  1. The regular, clock-based one
  2. a new one, based on frame availability from the active producers, used for deterministic rendering

2. Support for explicitly ticking the HTML producer. It has some caveats, media apis for instance doesn’t respect this, making the use of video and audio elements not possible at this time.
3. New AMCP commands for scheduling, at frame precision, what commands to execute at which time. Since “offline rendering” is not dependent on the wall clock, there is no way to accurately provide commands to a rendering “in-flight”, they all have to be provided upfront.
4. Support for shutting down the server on stdin EOF, enabling use cases where the commands are piped in to the server at startup, the rendering runs, and once everything is completed, the server exits. Enabling truly on-demand rendering

The PR can be found here: [Deterministic rendering by niklaspandersson · Pull Request #99 · nxtedition/casparcg · GitHub](https://github.com/nxtedition/casparcg/pull/99) . I intend to bring this to upstream at some point, but I feel that some design decisions and maybe the scope need to be discussed first. At least deterministic rendering through casparcg will start to get evaluated for real soon
