cymedia 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0a1
4
+
5
+ First alpha.
6
+
7
+ - Graph engine: decode once, a filter chain and an encoder per branch, source group and object
8
+ numbers carried to every output, a keyframe at each group start.
9
+ - Realtime drop policy at group granularity, with input and branch events; frames wait while a
10
+ branch's encoder first opens.
11
+ - C sink ABI, a queue sink for Python, and native sinks passed as capsules.
12
+ - ABR ladder profiles for NVENC and software, and a live delay bench.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Giovanni Marzot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE README.md CHANGELOG.md
2
+ recursive-include src/cymedia *.pyx *.c *.h
3
+ recursive-include tests *.py
4
+ exclude src/cymedia/_core.c
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: cymedia
3
+ Version: 0.1.0a1
4
+ Summary: Low-latency, group-aligned media processing graphs on FFmpeg's libraries
5
+ Author: Giovanni Marzot
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gmarzot/cymedia
8
+ Project-URL: Repository, https://github.com/gmarzot/cymedia
9
+ Project-URL: Issues, https://github.com/gmarzot/cymedia/issues
10
+ Project-URL: Changelog, https://github.com/gmarzot/cymedia/blob/main/CHANGELOG.md
11
+ Keywords: ffmpeg,video,transcoding,abr,nvenc,low-latency,moq
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: C
17
+ Classifier: Programming Language :: Cython
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Video :: Conversion
22
+ Requires-Python: >=3.12
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8; extra == "test"
27
+ Dynamic: license-file
28
+
29
+ # cymedia
30
+
31
+ Low-latency, group-aligned media processing graphs on FFmpeg's libraries, for Python.
32
+
33
+ A graph decodes a source once and feeds any number of branches. Each branch is a libavfilter
34
+ chain ending in an encoder, and its output goes to a sink. Source group numbers, object numbers
35
+ and timestamps pass through unchanged, and every branch starts a keyframe at each source group
36
+ start, so equally numbered groups line up across all outputs. The first graph is an ABR ladder.
37
+
38
+ **Status: alpha.** The NVIDIA path (NVDEC, `scale_cuda`, NVENC) and a software path (libopenh264,
39
+ else libx264) work, on Linux and macOS with Python 3.12 and 3.13. The API can change before 1.0.
40
+
41
+ ## Install
42
+
43
+ cymedia builds against the system's FFmpeg, 6.1 or later. It needs FFmpeg's development files,
44
+ `pkg-config` and a C compiler:
45
+
46
+ sudo apt-get install pkg-config libavcodec-dev libavfilter-dev libavutil-dev # Ubuntu 24.04
47
+ brew install ffmpeg pkg-config # macOS
48
+ pip install cymedia
49
+
50
+ Prebuilt wheels with an LGPL FFmpeg are planned.
51
+
52
+ ## Example
53
+
54
+ ```python
55
+ from cymedia import Rung, ladder
56
+
57
+ rungs = [Rung(1280, 720, 3_750_000), Rung(640, 360, 900_000)]
58
+ with ladder(rungs, profile="nvenc", fps=(60, 1)) as g:
59
+ g.push(frame, group, obj, pts_us, key) # one H.264 access unit
60
+ for out in g.read(timeout=0.1):
61
+ ... # out.branch, out.group, out.object, out.pts_us, out.key, out.group_start, out.data
62
+ ```
63
+
64
+ - **Input** is one H.264 access unit per `push()`: Annex B, or 4-byte length prefixes with the
65
+ avcC passed as `extradata=`. `key` marks a group's first frame. `push()` returns False when
66
+ the frame was dropped.
67
+ - **Output** is Annex B, one `Output` per encoded frame, carrying its source frame's group,
68
+ object and timestamp. `g.fileno()` is readable while outputs are queued, for an event loop:
69
+ `loop.add_reader(g.fileno(), lambda: handle(g.read(0)))`.
70
+ - **`flush(timeout)`** ends input and waits for every branch's last frame; `stats()` and
71
+ `delays(branch)` report counters and recent push-to-output delays.
72
+
73
+ ### Profiles
74
+
75
+ | Profile | Decode | Scale | Encode |
76
+ |---|---|---|---|
77
+ | `nvenc` | NVDEC | `scale_cuda` | `h264_nvenc`: p1, ultra low latency, CBR, no B-frames; filler NAL units stripped |
78
+ | `cpu` | software | `scale` | libopenh264, else libx264 (veryfast, zerolatency) |
79
+
80
+ Any other graph is a list of branches:
81
+
82
+ ```python
83
+ from cymedia import Branch, Graph
84
+
85
+ g = Graph([Branch("scale=640:360,format=yuv420p", "libx264", {"preset": "veryfast"})],
86
+ decoder="h264", fps=(30, 1))
87
+ ```
88
+
89
+ ## How it runs
90
+
91
+ - One decode thread decodes each source frame once, then runs every branch's filter chain.
92
+ - Each branch encodes on its own thread. Python's GIL is released throughout.
93
+ - In realtime mode (the default) a full queue drops instead of blocking. Input resumes at the next
94
+ group start; a branch that falls behind abandons the rest of that group, so a group is cut
95
+ short and never has a hole in it. While a branch's encoder first opens, frames wait rather
96
+ than drop. With `realtime=False`, queues block instead.
97
+ - Outputs go to a sink with a C ABI ([`cymedia_sink.h`](https://github.com/gmarzot/cymedia/blob/main/src/cymedia/cymedia_sink.h)). The
98
+ default sink queues them for `Graph.read()`; a native consumer passes its own sink as a
99
+ `cymedia.sink` capsule and keeps frames out of Python entirely.
100
+
101
+ ## Measured delay
102
+
103
+ An RTX 4070, a 2160p60 H.264 source to six NVENC rungs (1440p to 180p), from `push()` to the
104
+ encoded output, the 4K decode included (about 4.4 ms of it):
105
+
106
+ | Run | GPU power state | p50 | p99 |
107
+ |---|---|---|---|
108
+ | 60 fps | P2, then P3 | 8.5–9.3 ms | 14.6–15.3 ms |
109
+ | 60 fps, clocks held up | P2 | 7.4–8.0 ms | 15.5–17.1 ms |
110
+ | 30 fps | P5 | 14.7–16.1 ms | 29.7–30.7 ms |
111
+ | 30 fps, clocks held up | P2/P3 | 8.5–9.0 ms | 16.0–16.9 ms |
112
+
113
+ Under a light load a GeForce card drops to a low power state and the delay doubles; no frames are
114
+ held. [`bench/ladder_delay.py`](https://github.com/gmarzot/cymedia/blob/main/bench/ladder_delay.py) measures it on your hardware.
115
+
116
+ ## Used by
117
+
118
+ [aiomoqt](https://github.com/gmarzot/aiomoqt)'s `abr_ingest` sits in front of a MoQ relay and
119
+ publishes a broadcast plus a cymedia ladder under one catalog.
120
+
121
+ ## Develop
122
+
123
+ python -m venv .venv && .venv/bin/pip install "Cython>=3.0.10" "setuptools>=77" pytest
124
+ .venv/bin/pip install --no-build-isolation -e .
125
+ .venv/bin/python -m pytest -q
126
+
127
+ GPU tests skip unless CUDA, `h264_nvenc` and `scale_cuda` are available. The delay bench:
128
+
129
+ .venv/bin/python bench/ladder_delay.py clip.mkv --profile nvenc --seconds 90
130
+
131
+ Contributor rules are in [AGENTS.md](https://github.com/gmarzot/cymedia/blob/main/AGENTS.md).
132
+
133
+ ## License
134
+
135
+ MIT. cymedia links FFmpeg dynamically. A build for distribution must link an LGPL-only FFmpeg
136
+ (no `--enable-gpl`, no `--enable-nonfree`; `scale_cuda` built with `--enable-cuda-llvm`) and
137
+ publish that FFmpeg's source and configure line. Distribution packages often ship GPL builds,
138
+ which are fine for development but not for redistributed wheels. `cymedia.ffmpeg_info()` reports
139
+ the license of the FFmpeg in use.
@@ -0,0 +1,111 @@
1
+ # cymedia
2
+
3
+ Low-latency, group-aligned media processing graphs on FFmpeg's libraries, for Python.
4
+
5
+ A graph decodes a source once and feeds any number of branches. Each branch is a libavfilter
6
+ chain ending in an encoder, and its output goes to a sink. Source group numbers, object numbers
7
+ and timestamps pass through unchanged, and every branch starts a keyframe at each source group
8
+ start, so equally numbered groups line up across all outputs. The first graph is an ABR ladder.
9
+
10
+ **Status: alpha.** The NVIDIA path (NVDEC, `scale_cuda`, NVENC) and a software path (libopenh264,
11
+ else libx264) work, on Linux and macOS with Python 3.12 and 3.13. The API can change before 1.0.
12
+
13
+ ## Install
14
+
15
+ cymedia builds against the system's FFmpeg, 6.1 or later. It needs FFmpeg's development files,
16
+ `pkg-config` and a C compiler:
17
+
18
+ sudo apt-get install pkg-config libavcodec-dev libavfilter-dev libavutil-dev # Ubuntu 24.04
19
+ brew install ffmpeg pkg-config # macOS
20
+ pip install cymedia
21
+
22
+ Prebuilt wheels with an LGPL FFmpeg are planned.
23
+
24
+ ## Example
25
+
26
+ ```python
27
+ from cymedia import Rung, ladder
28
+
29
+ rungs = [Rung(1280, 720, 3_750_000), Rung(640, 360, 900_000)]
30
+ with ladder(rungs, profile="nvenc", fps=(60, 1)) as g:
31
+ g.push(frame, group, obj, pts_us, key) # one H.264 access unit
32
+ for out in g.read(timeout=0.1):
33
+ ... # out.branch, out.group, out.object, out.pts_us, out.key, out.group_start, out.data
34
+ ```
35
+
36
+ - **Input** is one H.264 access unit per `push()`: Annex B, or 4-byte length prefixes with the
37
+ avcC passed as `extradata=`. `key` marks a group's first frame. `push()` returns False when
38
+ the frame was dropped.
39
+ - **Output** is Annex B, one `Output` per encoded frame, carrying its source frame's group,
40
+ object and timestamp. `g.fileno()` is readable while outputs are queued, for an event loop:
41
+ `loop.add_reader(g.fileno(), lambda: handle(g.read(0)))`.
42
+ - **`flush(timeout)`** ends input and waits for every branch's last frame; `stats()` and
43
+ `delays(branch)` report counters and recent push-to-output delays.
44
+
45
+ ### Profiles
46
+
47
+ | Profile | Decode | Scale | Encode |
48
+ |---|---|---|---|
49
+ | `nvenc` | NVDEC | `scale_cuda` | `h264_nvenc`: p1, ultra low latency, CBR, no B-frames; filler NAL units stripped |
50
+ | `cpu` | software | `scale` | libopenh264, else libx264 (veryfast, zerolatency) |
51
+
52
+ Any other graph is a list of branches:
53
+
54
+ ```python
55
+ from cymedia import Branch, Graph
56
+
57
+ g = Graph([Branch("scale=640:360,format=yuv420p", "libx264", {"preset": "veryfast"})],
58
+ decoder="h264", fps=(30, 1))
59
+ ```
60
+
61
+ ## How it runs
62
+
63
+ - One decode thread decodes each source frame once, then runs every branch's filter chain.
64
+ - Each branch encodes on its own thread. Python's GIL is released throughout.
65
+ - In realtime mode (the default) a full queue drops instead of blocking. Input resumes at the next
66
+ group start; a branch that falls behind abandons the rest of that group, so a group is cut
67
+ short and never has a hole in it. While a branch's encoder first opens, frames wait rather
68
+ than drop. With `realtime=False`, queues block instead.
69
+ - Outputs go to a sink with a C ABI ([`cymedia_sink.h`](https://github.com/gmarzot/cymedia/blob/main/src/cymedia/cymedia_sink.h)). The
70
+ default sink queues them for `Graph.read()`; a native consumer passes its own sink as a
71
+ `cymedia.sink` capsule and keeps frames out of Python entirely.
72
+
73
+ ## Measured delay
74
+
75
+ An RTX 4070, a 2160p60 H.264 source to six NVENC rungs (1440p to 180p), from `push()` to the
76
+ encoded output, the 4K decode included (about 4.4 ms of it):
77
+
78
+ | Run | GPU power state | p50 | p99 |
79
+ |---|---|---|---|
80
+ | 60 fps | P2, then P3 | 8.5–9.3 ms | 14.6–15.3 ms |
81
+ | 60 fps, clocks held up | P2 | 7.4–8.0 ms | 15.5–17.1 ms |
82
+ | 30 fps | P5 | 14.7–16.1 ms | 29.7–30.7 ms |
83
+ | 30 fps, clocks held up | P2/P3 | 8.5–9.0 ms | 16.0–16.9 ms |
84
+
85
+ Under a light load a GeForce card drops to a low power state and the delay doubles; no frames are
86
+ held. [`bench/ladder_delay.py`](https://github.com/gmarzot/cymedia/blob/main/bench/ladder_delay.py) measures it on your hardware.
87
+
88
+ ## Used by
89
+
90
+ [aiomoqt](https://github.com/gmarzot/aiomoqt)'s `abr_ingest` sits in front of a MoQ relay and
91
+ publishes a broadcast plus a cymedia ladder under one catalog.
92
+
93
+ ## Develop
94
+
95
+ python -m venv .venv && .venv/bin/pip install "Cython>=3.0.10" "setuptools>=77" pytest
96
+ .venv/bin/pip install --no-build-isolation -e .
97
+ .venv/bin/python -m pytest -q
98
+
99
+ GPU tests skip unless CUDA, `h264_nvenc` and `scale_cuda` are available. The delay bench:
100
+
101
+ .venv/bin/python bench/ladder_delay.py clip.mkv --profile nvenc --seconds 90
102
+
103
+ Contributor rules are in [AGENTS.md](https://github.com/gmarzot/cymedia/blob/main/AGENTS.md).
104
+
105
+ ## License
106
+
107
+ MIT. cymedia links FFmpeg dynamically. A build for distribution must link an LGPL-only FFmpeg
108
+ (no `--enable-gpl`, no `--enable-nonfree`; `scale_cuda` built with `--enable-cuda-llvm`) and
109
+ publish that FFmpeg's source and configure line. Distribution packages often ship GPL builds,
110
+ which are fine for development but not for redistributed wheels. `cymedia.ffmpeg_info()` reports
111
+ the license of the FFmpeg in use.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "Cython>=3.0.10"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cymedia"
7
+ version = "0.1.0a1"
8
+ description = "Low-latency, group-aligned media processing graphs on FFmpeg's libraries"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Giovanni Marzot" }]
14
+ keywords = ["ffmpeg", "video", "transcoding", "abr", "nvenc", "low-latency", "moq"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: MacOS",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: C",
21
+ "Programming Language :: Cython",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Multimedia :: Video :: Conversion",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/gmarzot/cymedia"
30
+ Repository = "https://github.com/gmarzot/cymedia"
31
+ Issues = "https://github.com/gmarzot/cymedia/issues"
32
+ Changelog = "https://github.com/gmarzot/cymedia/blob/main/CHANGELOG.md"
33
+
34
+ [project.optional-dependencies]
35
+ test = ["pytest>=8"]
36
+
37
+ [tool.setuptools]
38
+ package-dir = { "" = "src" }
39
+ packages = ["cymedia"]
40
+
41
+ [tool.setuptools.package-data]
42
+ cymedia = ["*.h"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
49
+
50
+ [tool.ruff.lint]
51
+ select = ["E", "F", "W"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ import subprocess
2
+
3
+ from Cython.Build import cythonize
4
+ from setuptools import Extension, setup
5
+
6
+ LIBS = ["libavcodec", "libavfilter", "libavutil"]
7
+
8
+
9
+ def pkg_config(flag):
10
+ return subprocess.check_output(["pkg-config", flag, *LIBS], text=True).split()
11
+
12
+
13
+ ext = Extension(
14
+ "cymedia._core",
15
+ sources=["src/cymedia/_core.pyx", "src/cymedia/engine.c", "src/cymedia/qsink.c"],
16
+ include_dirs=["src/cymedia"],
17
+ extra_compile_args=pkg_config("--cflags") + ["-std=c11", "-O2", "-Wall"],
18
+ extra_link_args=pkg_config("--libs") + ["-lpthread"],
19
+ )
20
+
21
+ setup(ext_modules=cythonize([ext], compiler_directives={"language_level": 3}))
@@ -0,0 +1,8 @@
1
+ """Low-latency, group-aligned media processing graphs on FFmpeg's libraries."""
2
+ from cymedia._core import (Graph, ffmpeg_info, have_decoder, have_encoder, have_filter,
3
+ hw_available, set_ffmpeg_log_level)
4
+ from cymedia._types import Branch, Output
5
+ from cymedia.ladder import PROFILES, Rung, ladder
6
+
7
+ __all__ = ["Branch", "Graph", "Output", "PROFILES", "Rung", "ffmpeg_info", "have_decoder",
8
+ "have_encoder", "have_filter", "hw_available", "ladder", "set_ffmpeg_log_level"]