-
Notifications
You must be signed in to change notification settings - Fork 0
RENDITIONS
Generated from
docs/RENDITIONS.md. Edit that file and re-runnode scripts/publish-wiki.mjs --push. An edit made here is a fork of the documentation that nothing reconciles, and the next run of this script will overwrite it without asking.
A rendition is a named video output profile that several destinations can share. It is how polyemesis serves platforms that will not accept your source video without giving up the thing that makes it fast.
- The problem
- A rendition re-encodes video only
- Passthrough is a rendition
- A rendition only runs when something needs it
- Presets are starting points
- Hardware encoders
You ingest 4K60. YouTube will take it. Twitch, Kick and X cap well below 4K, so they reject it — or accept it and quietly transcode it into something worse.
Without renditions the only way out is to drop your whole ingest to the lowest common denominator: YouTube gets 1080p because Kick cannot do 4K. Running one polyemesis destination per resolution does not help either, because each destination copies video — none of them can change it.
Destinations select a rendition rather than owning one, so three platforms that all want 1080p60 cost one encode, not three.
relay ─────────────────────────► dest:youtube -c:v copy + audio graph A
│ (rendition = passthrough, zero cost)
└──► rendition "1080p60 6M" ONE encode
(video encoded, ALL audio tracks copied through untouched)
└──► rendition's own relay hub
├─► dest:twitch -c:v copy + audio graph B
├─► dest:kick -c:v copy + audio graph C
└─► dest:x -c:v copy + audio graph D
This is the load-bearing rule, and it is worth stating plainly: a rendition
encodes video, and passes every audio track through with -c:a copy. It
never mixes, never downmixes, never re-encodes audio. There is no audio setting
on a rendition and there never will be one.
That is what keeps the differentiator intact on top of shared video. The
destinations downstream of a rendition still receive the full multitrack stream,
still compile their own -filter_complex from their own routing profile, and
still do -c:v copy — exactly as a passthrough destination does. Audio is
encoded once, at the destination, and never twice.
Consequently, changing which rendition a destination is on does not change its audio, and changing its audio routing does not restart the rendition or disturb the other destinations sharing it.
Passthrough is the zero-cost default: no process, no encode. The destination subscribes straight to the ingest relay and copies the source video, which is precisely what every destination has always done.
Every destination stays on passthrough with no action from you. This feature is strictly additive: if you never create a rendition, nothing about your install changes.
The encode starts when the first enabled destination selects the rendition, and stops when the last one releases it. A rendition that nothing enabled points at has no process and burns no CPU — creating a tier you are not using yet is free, and stopping the last destination on a tier stops its encode too.
| Action | What restarts |
|---|---|
| Editing a rendition | That encode, and exactly the destinations reading it |
| Renaming it, or editing its note | Nothing |
| Deleting it | Its destinations fall back to passthrough and keep running |
Deleting a rendition does not delete its destinations — but it does mean they are suddenly being handed your source video, so the delete tells you how many destinations that just happened to. Check the source still fits what each of those platforms accepts.
Each destination's card on the dashboard shows the rendition it is on directly above the audio tracks it receives, so "what video and what audio does this platform get" is one glance, not two.
The rendition editor offers these as editable starting points:
| Preset | Size | Rate | Video bitrate | Encoder |
|---|---|---|---|---|
| Source passthrough | source | source | — (no encode) | — |
| 1080p60 | 1920×1080 | 60 fps | 6000 kbps |
libx264, veryfast, 2 s GOP |
| 1080p30 | 1920×1080 | 30 fps | 4500 kbps |
libx264, veryfast, 2 s GOP |
| 720p60 | 1280×720 | 60 fps | 4500 kbps |
libx264, veryfast, 2 s GOP |
| 720p30 | 1280×720 | 30 fps | 3000 kbps |
libx264, veryfast, 2 s GOP |
Verify current limits with the platform.
These are not authoritative ceilings and are not presented as any platform's policy. Published limits change without notice, and they differ by partner, affiliate and beta status for two different accounts on the same platform. Being confidently wrong about one of these numbers breaks a live stream, so where we were unsure we picked the lower value: an under-spec stream is watchable, an over-spec one is rejected at the ingest.
Check your own account's current limits, then edit the rendition. Every field is yours to change.
Keyframe interval is set in seconds, not frames, so it stays correct when you change the frame rate. Two seconds suits every live platform we know of.
When the rendition's shape does not match the source's, something has to give. A 9:16 rendition of a 16:9 ingest is one more entry in the ladder, encoded once and shared, rather than a parallel pipeline.
| Mode | What happens |
|---|---|
| Stretch to fit | Scales to the target size and lets the picture distort. Anamorphic, almost never what anyone wants — but it is what renditions did before the other modes existed, so it stays the default |
| Crop to fill | Centre-crops to the target shape, then scales. Subjects keep their on-screen size; the edges of the frame are gone |
| Letterbox | Scales the whole frame to fit and fills the rest with a flat colour. Nothing is lost, but a 16:9 source on a 9:16 canvas is mostly bars |
| Blurred fill | Fills the remainder with a blurred, cropped-to-fill copy of the frame itself |
Blurred fill is the convention vertical feeds have settled on, and it is the difference between a repurposed landscape stream looking deliberate and looking lazy. The blur is computed on a 1/8-scale proxy, because a gaussian wide enough to read as "background" costs more per frame at 1080p than the H.264 encode it feeds — and the upscale back to full size does most of the blurring for free.
Aspect handling needs both a width and a height. With one axis free the scale already preserves the aspect ratio, so there is no shape to convert to and the control is disabled rather than saved as something quietly inert.
For SDI bridges, capture cards and legacy broadcast kit. Progressive sources — which is almost everyone — should leave this off, because deinterlacing a progressive frame softens it for no gain.
| Mode | What happens |
|---|---|
| Off | Default |
| Only interlaced frames | Touches only frames the source flagged as interlaced, so progressive frames pass through untouched. The right choice for anything mixed — a camera that switches modes, a playout chain splicing SD and HD |
| Every frame | Unconditional. For sources that are interlaced but do not say so — plenty of capture kit flags everything progressive regardless of what it was fed, and on those "only interlaced" is a no-op that looks like a broken setting |
It uses bwdif rather than yadif — the same idea done better, for a few
percent more CPU — in send_frame mode, which emits one progressive frame per
input frame rather than one per field. send_field would double the frame
rate, which silently doubles the bitrate the platform receives and breaks the
keyframe arithmetic computed from the source rate.
Deinterlacing runs first, before any scaling. That ordering is load-bearing rather than tidy: scaling interlaced content blends the two fields together, and once that has happened the combing is baked into the pixels and no later filter can remove it.
A rendition can burn a still image into the picture — a logo, a sponsor card, a channel mark.
It lives here, on the rendition, rather than on a destination, and that is not
an arrangement of the settings page. A destination copies video with
-c:v copy; there is no mechanism by which a copied bitstream acquires a logo.
Burning one in is a re-encode, so it belongs where re-encoding is already the
contract.
The practical consequence is worth being clear about:
- Adding a watermark to a rendition costs no new process. A few percent CPU on an encode that was already running. This case is cheap.
-
Giving two platforms different branding costs a second full encode. They
are different pictures, so they are different encodes — roughly 1.5–3 cores on
x264
veryfast, or near-zero CPU on NVENC at the cost of one of its 3–8 concurrent sessions. - A clean feed is a destination on no rendition, or on one with no watermark. Nothing to switch off.
| Setting | Means |
|---|---|
| Width | the image's width as a percentage of the frame's width |
| Margin X / Y | the gap from the anchored edges, as percentages of the frame |
| Position | one of nine anchors — corners, edge centres, or the middle |
| Opacity | 1–100%; the image's own transparency is respected either way |
Percentages rather than pixels, because the same watermark has to be correct on a 1920×1080 tier and a 1080×1920 one. A logo placed 40 px from the right edge of a landscape frame is in a sensible place; the same 40 px on a vertical frame is not the same place at all, and a size in pixels that reads well on one lands comically large or invisible on the other.
Margins are ignored on a centred axis — a centred logo is centred.
Put the file in <data-directory>/overlays/ and give the rendition the relative
path, overlays/logo.png. PNG with transparency is the usual choice.
The path is confined to the data directory, the same way a slate image and a
file:// pull source are. An absolute path or a .. is refused rather than
resolved: the field is operator input that becomes an FFmpeg argument, and
anything else would be a file-read primitive for whoever reaches the API.
A watermarked rendition needs an explicit width and height. The image is sized as a percentage of the output, so the output has to have a size. A rendition with one axis free is refused when you save it rather than starting and quietly having no logo.
Editing a watermark restarts the encode, and therefore every destination riding that rendition. Nudging a logo by 2% while live drops them for a second or two. Replacing the image file has the same effect, and deliberately so — the alternative is an encoder that keeps compositing the picture you just replaced.
A rendition can also burn in a line of text. The settings mirror the watermark's reasoning — percentages, not pixels:
| Setting | Means |
|---|---|
| Content | the line to draw |
| Font | Inter Regular or Inter Bold, shipped embedded — or your own, dropped in <data-directory>/fonts/
|
| Position | the same nine anchors a watermark uses |
| Size | as a percentage of frame height |
| Colour | the text colour |
| Margin X / Y | as percentages of the frame |
| Box | an optional background box, with its own colour and opacity |
Fonts are embedded rather than assumed because FFmpeg's drawtext takes a font
path, not bytes, and a container image routinely has neither fontconfig nor a
single font file on it. Asking the operator to supply one would make the feature
work on a developer's laptop and fail in Docker.
drawtext is optional in FFmpeg, not guaranteed. A build without libfreetype
has no way to render text at all — the FFmpeg in Homebrew is sometimes one of
them. polyemesis probes for the filter and, when it is missing, runs the
rendition without the text rather than refusing to start. Dropping the text
keeps the picture up, which is the right way round: nobody watching would prefer
a black screen with correct typography.
Editing text restarts the encode, exactly as editing a watermark does.
No clock, no viewer counts, no animation, no browser sources. Those are designed and costed in the roadmap; they are not built.
EXPERIMENTAL — the flags polyemesis hands an NVENC, QSV, VA-API or AMF encoder have not been confirmed on real hardware. The probe described below is genuine evidence about whether an encoder opens on your machine; it says nothing about whether the rate control, preset and profile flags then behave as documented, and for those four families they were read out of FFmpeg's option tables rather than measured against silicon. No NVENC, QSV or VA-API encode has been observed. Every encoder stays selectable and there is no flag to turn this off; see ENCODING.md § Per-encoder flags.
VideoToolbox is not in that set.
TestEveryConfiguredEncoderOpensWithItsOwnFlagsruns a real encode per registered encoder with that encoder's own flags, andh264_videotoolbox/hevc_videotoolboxpass on macOS.
At startup polyemesis encodes one frame with each of the six encoders it
probes — the five H.264 hardware encoders and libx264 — and keeps the exit
status:
ffmpeg -f lavfi -i testsrc2=size=320x240:rate=1 -frames:v 1 -c:v h264_nvenc -f null -That is the only test that means anything. ffmpeg -encoders lists what the
build was compiled with, not what the machine can do: a stock Ubuntu FFmpeg
lists h264_nvenc, h264_qsv, h264_vaapi and h264_amf on a box with no GPU
in it at all.
The editor offers only what encoded a frame here, and shows everything else
greyed out with FFmpeg's own reason — Cannot load libcuda.so.1, No VA display found for device /dev/dri/renderD128 — so you find out at the dropdown rather
than after you have gone live. A rendition saved on an encoder that later stops
working is refused at start with the same message, instead of crash-looping.
The six HEVC encoders are not probed. Each takes its sibling's verdict:
hevc_nvenc opens the same device through the same driver as h264_nvenc, so
if one cannot load libcuda neither can the other. The editor says when a verdict
was inferred rather than measured. The inference is good enough to stop offering
a choice and deliberately not good enough to refuse a start, so a rendition
already saved on an HEVC encoder is never killed on a guess.
The scan is bounded, runs its probes concurrently, and cannot fail the launch: if it cannot run at all, every encoder stays on offer and the product falls back to software. Measured cost on the development machine: 218 ms added to startup.
Renditions → re-detect hardware re-runs the whole thing without a restart,
which is what you want after installing a driver or passing a GPU into a
container.
| Family | Encoders | Notes |
|---|---|---|
| Software |
libx264, libx265
|
Always available, and the only two whose behaviour is identical on every machine. |
| NVIDIA |
h264_nvenc, hevc_nvenc
|
Presets are p1–p7; p4 is the honest middle. The only family that must be told whether it is doing CBR or capped VBR — -rc cbr otherwise pins it to constant bitrate and a ceiling does nothing. |
| Intel Quick Sync |
h264_qsv, hevc_qsv
|
Needs a working VA-API/QSV runtime, not just the CPU. |
| Apple |
h264_videotoolbox, hevc_videotoolbox
|
No preset knob; -realtime is the lever. |
| VA-API (Linux) |
h264_vaapi, hevc_vaapi
|
Needs a render node, /dev/dri/renderD128 by default, and a format=nv12,hwupload filter tail — it encodes from GPU surfaces and cannot open without both. This is the AMD path on Linux. |
| AMD |
h264_amf, hevc_amf
|
Windows. Ubuntu's packaged FFmpeg contains no *_amf encoder at all — on Linux, use VA-API for AMD. |
A working hardware encoder is the default for a new rendition, because a
machine with a usable GPU that quietly software-encodes cannot serve the feature
the GPU was bought for. libx264 is the default everywhere else, and is always
selectable: at a given bitrate it still beats every fixed-function encoder on
quality, so choosing it over hardware is a legitimate trade of headroom for
picture.
See HARDWARE.md for per-vendor container images
(Dockerfile.cuda, Dockerfile.vaapi), the GPU passthrough flags, and what each
driver error message actually means.
libx264 at 4K60 is not a workload a normal streaming box handles. Even at
veryfast it needs a very high core count to hold realtime, and this machine is
already running your ingest, your recorder, the preview and one FFmpeg per
destination. If it cannot keep up, the encode falls behind realtime and every
destination on that rendition suffers.
If you are ingesting 4K60 and need it re-encoded, use a hardware encoder. If you have no hardware encoder, rendition down from 4K rather than at 4K — that is what renditions are for.
Note also that most RTMP ingests accept H.264 only. The HEVC encoders are listed because they are real and occasionally useful (SRT, a file destination, an ingest you control), not because a live platform is likely to take one.
- ARCHITECTURE.md — hub topology, ref counting and reconcile order
- HARDWARE.md — GPU passthrough and driver errors
- PLATFORMS.md — what each platform will accept
- AUDIO-ROUTING.md — the audio a rendition never touches
Getting it running
- Quickstart: from nothing to a live restream
- Install polyemesis — an SRT server on your own box
- OBS SRT setup: multitrack audio to one ingest
- TLS certificates for a self-hosted SRT server
The routing
- Audio routing: a different mix per destination
- Renditions: one shared video encode
- Encoding: what is copied and what is encoded
- Hardware encoding: NVENC, QSV, VA-API, AMF
Operating it
- Configuration: config.yaml and the web UI
- Streaming platforms: what can be automated
- Broadcasting from a file, on a schedule
- What a settings change restarts, and what it does not
- Upgrading polyemesis and its database
- Troubleshooting: SRT, RTMP and audio problems
Automating it
- Monitoring: Prometheus metrics and alerts
- Lifecycle webhooks: one signed POST per event
- MQTT telemetry and Home Assistant
- HTTP API reference — polyemesis /api/v1
Understanding it