This release is a pre-release and may not be stable for production use.
cymedia
Low-latency, group-aligned media processing graphs on FFmpeg's libraries, for Python.
A graph decodes a source once and feeds any number of branches. Each branch is a libavfilter chain ending in an encoder, and its output goes to a sink. Source group numbers, object numbers and timestamps pass through unchanged, and every branch starts a keyframe at each source group start, so equally numbered groups line up across all outputs. The first graph is an ABR ladder.
Status: alpha. H.264 and AV1 ladders on the NVIDIA path (NVDEC, scale_cuda, NVENC) and in
software, and H.264 ladders on Apple's VideoToolbox, on Linux and macOS with Python 3.12 and 3.13. The API can change before 1.0.
Install
cymedia builds against the system's FFmpeg, 6.1 or later. It needs FFmpeg's development files,
pkg-config and a C compiler:
sudo apt-get install pkg-config libavcodec-dev libavfilter-dev libavutil-dev # Ubuntu 24.04
brew install ffmpeg pkg-config # macOS
pip install cymedia
Prebuilt wheels with an LGPL FFmpeg are planned.
Packaged FFmpeg builds differ in what they include; Homebrew's has no libaom, for example.
scripts/build-ffmpeg.sh
builds one with what the profiles need, in a directory of its own:
scripts/build-ffmpeg.sh
source ~/.local/cymedia-ffmpeg/env.sh
pip install --no-cache-dir --no-binary cymedia cymedia
That is an LGPL FFmpeg with libaom, plus NVENC and scale_cuda on Linux or VideoToolbox on macOS.
It has no software H.264 encoder: --gpl adds libx264 and makes the build GPL.
Example
from cymedia import Rung, ladder
rungs = [Rung(1280, 720, 3_750_000), Rung(640, 360, 900_000)]
with ladder(rungs, profile="nvenc", codec="h264", fps=(60, 1)) as g:
g.push(frame, group, obj, pts_us, key) # one H.264 access unit
for out in g.read(timeout=0.1):
... # out.branch, out.group, out.object, out.pts_us, out.key, out.group_start, out.data
- Input is one H.264 access unit per
push(): Annex B, or 4-byte length prefixes with the avcC passed asextradata=.keymarks a group's first frame.push()returns False when the frame was dropped. - Output is one
Outputper encoded frame, carrying its source frame's group, object and timestamp: Annex B for H.264, a temporal unit of OBUs for AV1.g.fileno()is readable while outputs are queued, for an event loop:loop.add_reader(g.fileno(), lambda: handle(g.read(0))). flush(timeout)ends input and waits for every branch's last frame;stats()anddelays(branch)report counters and recent push-to-output delays.
Profiles
| Profile | Decode | Scale | codec="h264" |
codec="av1" |
|---|---|---|---|---|
nvenc |
NVDEC | scale_cuda |
h264_nvenc |
av1_nvenc |
videotoolbox |
VideoToolbox | scale_vt |
h264_videotoolbox |
none |
cpu |
software | scale |
libopenh264, else libx264 (veryfast, zerolatency) | libaom (realtime, cpu-used 8) |
The NVENC encoders run p1, ultra low latency, CBR with a one-frame buffer and no B-frames; H.264 filler NAL units and AV1 padding OBUs are stripped. Every profile forces a keyframe at each group start. VideoToolbox runs in its low-latency mode with no B-frames. SVT-AV1 is not used: version 1.7 ignores forced keyframes.
Any other graph is a list of branches:
from cymedia import Branch, Graph
g = Graph([Branch("scale=640:360,format=yuv420p", "libx264", {"preset": "veryfast"})],
decoder="h264", fps=(30, 1))
How it runs
- One decode thread decodes each source frame once, then runs every branch's filter chain.
- Each branch encodes on its own thread. Python's GIL is released throughout.
- In realtime mode (the default) a full queue drops instead of blocking. Input resumes at the next
group start; a branch that falls behind abandons the rest of that group, so a group is cut
short and never has a hole in it. While a branch's encoder first opens, frames wait rather
than drop. With
realtime=False, queues block instead. - Outputs go to a sink with a C ABI (
cymedia_sink.h). The default sink queues them forGraph.read(); a native consumer passes its own sink as acymedia.sinkcapsule and keeps frames out of Python entirely.
Measured delay
An RTX 4070, a 2160p60 H.264 source to six NVENC rungs (1440p to 180p), from push() to the
encoded output, the 4K decode included (about 4.4 ms of it):
| Run | GPU power state | p50 | p99 |
|---|---|---|---|
| 60 fps | P2, then P3 | 8.5–9.3 ms | 14.6–15.3 ms |
| 60 fps, clocks held up | P2 | 7.4–8.0 ms | 15.5–17.1 ms |
| 30 fps | P5 | 14.7–16.1 ms | 29.7–30.7 ms |
| 30 fps, clocks held up | P2/P3 | 8.5–9.0 ms | 16.0–16.9 ms |
Under a light load a GeForce card drops to a low power state and the delay doubles; no frames are held.
An Apple M4, a 1080p60 source to four VideoToolbox rungs (720p to 180p): p50 12 ms on the first rung and 75–80 ms on the others, in realtime with no drops. It cannot keep up with a 2160p60 source to six rungs.
bench/ladder_delay.py measures it on your hardware.
Used by
aiomoqt's abr_ingest sits in front of a MoQ relay and
publishes a broadcast plus a cymedia ladder under one catalog.
Develop
python -m venv .venv && .venv/bin/pip install "Cython>=3.0.10" "setuptools>=77" pytest
.venv/bin/pip install --no-build-isolation -e .
.venv/bin/python -m pytest -q
GPU tests skip unless CUDA, h264_nvenc and scale_cuda are available. The delay bench:
.venv/bin/python bench/ladder_delay.py clip.mkv --profile nvenc --seconds 90
Contributor rules are in AGENTS.md.
License
MIT. This software uses code of FFmpeg licensed under the
LGPLv2.1 and its source can be
downloaded here. cymedia ships no FFmpeg: it links the
system's, and cymedia.ffmpeg_info() reports that build's license. Redistributing FFmpeg with
cymedia is subject to FFmpeg's terms.
Metadata
Release files for cymedia 0.1.0a3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cymedia-0.1.0a3.tar.gz | 30.5 kB | With provenance | Details for cymedia-0.1.0a3.tar.gz |
Release files / cymedia-0.1.0a3.tar.gz
| Download URL | cymedia-0.1.0a3.tar.gz |
|---|---|
| Size | 30.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a126f786077f885681146d46c91c57679723dd5a76f5b5292e76beb459c6b0d6
|
|
BLAKE2b-256 checksum How to use checksums |
dc7a37f7eb66b3a8874c36adf510f1b4d1fec3f830493af3b03d6814d8277fd4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.8
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for cymedia 0.1.0a3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
1 file in this release has been uploaded with provenance metadata.
Attestation for cymedia-0.1.0a3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.