Short recap, because this is the second half of a story. In the previous article I described how our on-air graphics at VSporte left vMix. The overlay became a web page fed into the mixer as a single input, which took the graphics out of vMix's memory and made them testable. Then Rive moved the layout work off developers and onto designers. That last step had a consequence nobody planned: a graphic was now a file with a state machine, driven by plain data. It did not need a browser to render. And it had never needed a GPU, we had just always rendered it through tools that came with one attached.
Everything below is VSporte engineering too: real feeds, measured numbers, and a container that does what I say it does.
Can the rendering leave the building?
Once the graphic is a Rive file plus a JSON state, the renderer can be anything that runs the Rive state machine and rasterizes vectors.
Rive is something you normally render on the web, in a browser or an app, and that is where we ran it. I wanted to know whether the same file could be rendered headless, on a server with no GPU in it at all, and what a live broadcast would cost if it could. Which is the obvious question restated: why is there still a GPU workstation in a rented room attached to every parallel broadcast?
The path was not a straight line. First browser screenshots driven by a headless browser, then a WebAssembly renderer on CPU, then a native build, each cheaper and more stable than the last. Before committing to the native build I benchmarked it to establish that thirty frames a second at 1080p is reachable on eight cores, and catalogued what the native path gives up, fonts and automatic layout. That is the sort of thing that decides an engine choice more often than raw throughput does.
What I ended up with is a single container. Input is an SRT stream, output is an SRT stream with the graphics burned in. Inside it are four threads. StateManager holds a websocket subscription to the graphics state, with an HTTPS polling fallback for when the socket will not establish, and the state itself is plain JSON naming the scene file, the state machine, and the view model as key value pairs, and downloads the Rive file and its assets. SrtReceiver decodes incoming video into raw frames. RiveRenderer runs the state machine and rasterizes into an RGBA buffer through Skia on CPU. Compositor blends the overlay over the frame with premultiplied alpha, encodes with libx264, and pushes the result back out over SRT. The whole thing is a Docker image whose most important environment variable is the URL it reads its graphics state from.
Skia is the part doing the real work in that third thread and it deserves the credit by name. It is a mature 2D rasterizer, the same engine that draws Chrome's canvas and Android's UI, and vector artwork with a bounded number of shapes is what it is built for. Rive supplies the scene and the state machine, Skia turns it into pixels. Worth saying why Skia and not Rive's own renderer, which is where Rive itself has moved: that renderer is designed around the GPU, and the Skia backend was the mature path to a raster surface on CPU. That pairing is the reason 1080p at thirty frames a second sits comfortably on eight cores. Worth knowing before you copy the choice: Rive has retired that backend on iOS, Android and WASM. It is still there on Linux, which is where this container lives, but it is a dependency with a direction of travel, and that belongs on the risk list rather than in a footnote.
It works on CPU because of what broadcast graphics actually are: vector shapes, text and simple transitions at 1080p. The GPU was never required by the pixels. It was required by the tools that used to draw them.
One thing about the topology is worth stating explicitly, because it is the first question anyone from a broadcaster asks and the cloud pitch never answers it. In the topology I built, the clean feed does not go through the container. It runs past it, untouched, straight to the mixer, and what the container receives and burns into is the dirty path. Two consequences fall out of that. The clean feed still exists, which matters to every international partner, regional version and ad insertion downstream, and none of them were ever going to accept a workflow that had only one copy of the program feed with graphics welded into it. And the failure story is smaller than it looks from outside: the container sits inline in one path, the one carrying graphics. If it dies, the mixer still has clean video arriving, and you cut to it. That is a few seconds of a plainer looking broadcast, not a dead one.
The latency budget, which is where the real work was
Anything live has a budget, and the useful question is not "is it low latency" but "where do the milliseconds sit". Ours:
SRT input latency. The buffer that lets SRT recover lost packets, a couple hundred milliseconds in practice. The single largest line item, and a deliberate purchase. Push it down and you trade robustness for delay, and on a real network you feel that trade immediately.
Getting state to the renderer. State arrives over a websocket, pushed the moment it changes, so this line of the budget is close to free. There is an HTTPS polling fallback for when the socket will not establish, and which one is the default was not an aesthetic choice. The target was under a second from the data changing to the frame that shows it, and a poll interval spends a good part of that budget before the renderer has heard anything at all. Keeping both paths in the same binary is worth it anyway: a fallback you can measure against the default is how you find out what the transport is actually costing you.
Encoder. libx264 tuned for zerolatency with a fast preset. Zerolatency matters more than the preset, because it disables the frame reordering and lookahead that otherwise silently add delay.
Queues between the threads. This one bit us. If the compositor falls behind, frames accumulate, and an accumulating queue is latency you cannot see in any config file. We settled on dropping frames deliberately when the queue grows, and logging the drop. A visible dropped frame is a much better failure than a stream drifting steadily further behind live.
A subtler one on the same path: the overlay looked stuttery with nothing overloaded, because frames were arriving unevenly rather than late. Instrument decode, render and composite separately from day one. Aggregate CPU load tells you nothing about this class of problem.
What it costs, and how to check the number
One container per broadcast, on a node with no GPU. The bench machine is eight dedicated vCPU and sixteen gigabytes of RAM, which is what 1080p at thirty frames a second and six megabits needs with room to spare.
From there the calculation is one line: instance size, times the length of the event, times the hourly price. Two anchors so you can place your own. On the provider I ran it on, four hours land at about a dollar, disk and public address included. On AWS the equivalent box is a c7i.2xlarge, eight vCPU and sixteen gigabytes, and in a local zone it runs about forty six cents an hour on demand, so the same match is about two dollars. Pick your own region and the number moves, which is the point of having two anchors rather than one.
One line item to carry across when you do: traffic. That provider bundles traffic into the machine price, so a dollar really is the whole bill. Hyperscalers charge it separately, and six megabits for four hours is roughly eleven gigabytes out, which at usual egress rates adds about another dollar on top of the two. It does not change the argument. Leaving it out of your own spreadsheet will.
I am not naming the provider, because the interesting number is not mine, it is yours. Note that the two anchors differ by a factor of two and the argument survives it completely, because what this replaces is a GPU workstation, in a rented room, purchased again for every parallel show.
One cost the cloud pitch usually skips, and we paid it. With vMix in the studio the finished picture is right there on the local machine. Move the render out and you have to send it back, over SRT, and on our demo rig that return leg was a machine of its own. Anyone who tells you cloud graphics is purely a subtraction has not run it.
The property that matters is the shape of the curve, not the absolute number. A parallel broadcast is another container, not another machine and another room. Our demo rig ran two graphics packages over the same source feed into one receiving vMix, and going from two parallel outputs to ten is a config change.
Where this is a bad idea
I would not pitch this as universal, because it is not.
If your graphics are 3D, heavy particle work, or anything that genuinely needs shaders, the CPU rasterizer is the wrong tool and you should stop reading here. This works because broadcast graphics are mostly vectors, text and simple transitions at 1080p.
If you need frame accurate integration with studio hardware, SDI in and out, genlock, tally, then a cloud container is in the wrong place on the network no matter how cheap it is.
The economics also assume 1080p at thirty frames a second, and that is worth saying plainly to a European audience, because European broadcast lives at 1080i50 and 1080p50. Fifty frames is not thirty with a larger number attached to it. Push to 50p, to 4K, or to a high bitrate ladder, and the encode cost per show climbs fast enough that a cheap CPU node stops being cheap. At that point you are buying hardware again, just somewhere else. What it actually takes to hold 1080p50 on this architecture is its own article, and I would rather write that one properly than wave at it here.
And if you run two marquee matches a week, the old model is fine and the migration will not pay for itself. The economics turn over when you have many low value parallel events, which is exactly the segment where a GPU station per broadcast is unbearable.
The part worth taking away
The portable file made the renderer replaceable, and a replaceable renderer is what put the GPU and the rented room in question at all. None of the steps that got us there knew about the dollar. The dollar fell out at the end.
So the question I would ask of your own pipeline is not "can we do this in the cloud". It is the one we kept asking: which piece of infrastructure is in there because of the work, and which is in there because of the tool that used to do the work.
And where does the cloud come into it?
Late, and by accident. Nobody set out to move broadcast graphics into a data centre. Four bottlenecks got solved in order, and by the end of them a graphic was a file, a state machine and eight cores, which is already the description of a container. That is why the dollar sits at the bottom of this article instead of the top.
Two things here are articles rather than paragraphs, so I have left them out on purpose: what it takes to hold 1080p50 instead of 30, and what the graphics control platform looks like from the operator's side, which is the half of this story that is not about rendering at all. Tell me in the comments which one you would read first.
I am Andrei Novikov. I spent seven years running the engineering side of VSporte and I write about backend and realtime architecture. If you are at IBC and doing something adjacent, I am interested in comparing notes.


Top comments (0)