streamcraft 0.1.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 gst-tools contributors
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,358 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamcraft
3
+ Version: 0.1.0
4
+ Summary: Fluent Python wrappers for GStreamer — build pipelines, control cameras, and manage WebRTC sessions without the boilerplate.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/ranaweerasupun/streamcraft
7
+ Project-URL: Repository, https://github.com/ranaweerasupun/streamcraft
8
+ Project-URL: Issues, https://github.com/ranaweerasupun/streamcraft/issues
9
+ Keywords: gstreamer,webrtc,video,audio,streaming,pipeline,v4l2,ptz,camera,raspberry-pi
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Video
19
+ Classifier: Topic :: Multimedia :: Sound/Audio
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: aiohttp
26
+ Requires-Dist: aiohttp>=3.9; extra == "aiohttp"
27
+ Dynamic: license-file
28
+
29
+ # streamcraft
30
+
31
+ Fluent Python wrappers for GStreamer — build pipelines, control cameras, and manage WebRTC sessions without the boilerplate.
32
+
33
+ [![Tests](https://github.com/ranaweerasupun/streamcraft/actions/workflows/test.yml/badge.svg)](https://github.com/ranaweerasupun/streamcraft/actions/workflows/test.yml)
34
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
36
+ [![Platform: Linux](https://img.shields.io/badge/platform-Linux-lightgrey.svg)](https://www.kernel.org/)
37
+
38
+ ---
39
+
40
+ ## The problem
41
+
42
+ Writing a GStreamer pipeline in Python looks like this:
43
+
44
+ ```python
45
+ # 45 lines just to describe: camera → decode → encode → RTP
46
+ pipeline = Gst.Pipeline.new("video")
47
+
48
+ v4l2src = Gst.ElementFactory.make("v4l2src", "v4l2src")
49
+ v4l2src.set_property("device", "/dev/video0")
50
+ v4l2src.set_property("do-timestamp", True)
51
+ mjpeg_caps = Gst.Caps.from_string("image/jpeg,width=1280,height=720,framerate=30/1")
52
+
53
+ queue = Gst.ElementFactory.make("queue", "queue_v1")
54
+ queue.set_property("max-size-buffers", 3)
55
+ queue.set_property("max-size-time", 0)
56
+ queue.set_property("max-size-bytes", 0)
57
+ queue.set_property("leaky", 2)
58
+
59
+ jpegdec = Gst.ElementFactory.make("jpegdec", "jpegdec")
60
+ videoconv = Gst.ElementFactory.make("videoconvert", "videoconvert")
61
+
62
+ x264enc = Gst.ElementFactory.make("x264enc", "x264enc")
63
+ x264enc.set_property("tune", "zerolatency")
64
+ x264enc.set_property("speed-preset", "ultrafast")
65
+ x264enc.set_property("bitrate", 2000)
66
+ x264enc.set_property("key-int-max", 30)
67
+
68
+ # ... 20 more lines of pipeline.add() and link() calls ...
69
+ ```
70
+
71
+ With streamcraft the same pipeline is:
72
+
73
+ ```python
74
+ from streamcraft import PipelineBuilder
75
+
76
+ pipeline, elems = (
77
+ PipelineBuilder(name="video-pipeline")
78
+ .element("v4l2src", name="src", device="/dev/video0", do_timestamp=True)
79
+ .caps("image/jpeg,width=1280,height=720,framerate=30/1")
80
+ .element("queue", name="buffer", max_size_buffers=3, leaky=2)
81
+ .element("jpegdec")
82
+ .element("videoconvert")
83
+ .element("x264enc", name="encoder", tune="zerolatency",
84
+ speed_preset="ultrafast", bitrate=2000)
85
+ .element("h264parse", config_interval=1)
86
+ .caps("video/x-h264,stream-format=byte-stream,alignment=au")
87
+ .element("rtph264pay", name="pay", pt=96, config_interval=1, mtu=1200)
88
+ .build()
89
+ )
90
+
91
+ # pipeline is a real Gst.Pipeline — do anything you want with it
92
+ encoder = elems["encoder"]
93
+ encoder.set_property("bitrate", 4000)
94
+ pipeline.set_state(Gst.State.PLAYING)
95
+ ```
96
+
97
+ The topology is the code. Each line is one element, in the order data flows through it.
98
+
99
+ ---
100
+
101
+ ## What's in the library
102
+
103
+ ### `PipelineBuilder`
104
+
105
+ A fluent builder for linear GStreamer pipelines. It handles the repetitive `ElementFactory.make()` → `set_property()` → `link()` / `link_filtered()` loop for you, with clear error messages when something goes wrong.
106
+
107
+ The builder only handles linear topologies by design. For elements with dynamic pads (`decodebin`, `webrtcbin`) or branching topologies (`tee`), `build()` returns a real `Gst.Pipeline` that you extend with the standard GStreamer API — the two approaches compose naturally and neither gets in the way of the other.
108
+
109
+ One detail that saves a constant small friction: GStreamer property names use hyphens (`speed-preset`, `key-int-max`, `max-size-buffers`) but Python keyword arguments must use underscores. The builder converts underscores to hyphens automatically, so you write Python-idiomatic code and the right thing happens.
110
+
111
+ ```python
112
+ from streamcraft import PipelineBuilder
113
+
114
+ pipeline, elems = (
115
+ PipelineBuilder()
116
+ .element("audiotestsrc", num_buffers=100)
117
+ .element("audioconvert")
118
+ .caps("audio/x-raw,channels=1,rate=48000")
119
+ .element("opusenc", name="enc", bitrate=128000, complexity=5)
120
+ .element("fakesink", sync=False)
121
+ .build()
122
+ )
123
+
124
+ enc = elems["enc"]
125
+ print(enc.get_property("bitrate")) # 128000
126
+ ```
127
+
128
+ ### `require_elements`
129
+
130
+ Checks that all the GStreamer plugins your pipeline needs are installed before you try to build anything. If something is missing, you get a clear error message with the exact `apt install` command to fix it — not a cryptic `None` return buried ten lines into your startup code.
131
+
132
+ ```python
133
+ from streamcraft import require_elements
134
+
135
+ require_elements(
136
+ "webrtcbin", "v4l2src", "jpegdec", "videoconvert",
137
+ "x264enc", "h264parse", "rtph264pay",
138
+ "alsasrc", "opusenc", "rtpopuspay",
139
+ )
140
+ # Raises EnvironmentError with an apt install hint if anything is missing.
141
+ # Returns None silently if everything is present.
142
+ ```
143
+
144
+ ### `check_v4l2_device` and `list_v4l2_devices`
145
+
146
+ Verifies that a V4L2 camera device exists, is readable, and actually opens — catching "device busy" and permission errors before you commit to building a pipeline for a real connection.
147
+
148
+ ```python
149
+ from streamcraft import check_v4l2_device, list_v4l2_devices
150
+
151
+ print(list_v4l2_devices()) # ['/dev/video0', '/dev/video2']
152
+
153
+ ok, msg = check_v4l2_device("/dev/video0")
154
+ if not ok:
155
+ raise SystemExit(f"Camera not available: {msg}")
156
+ # e.g. "Device '/dev/video0' exists but is not readable.
157
+ # Try: sudo usermod -aG video $USER (then re-login)"
158
+ ```
159
+
160
+ ### `V4L2PTZCamera`
161
+
162
+ Controls pan, tilt, and zoom on any V4L2 camera that exposes those controls — Obsbot, Logitech PTZ Pro, and similar. It uses `v4l2-ctl` to auto-detect the camera's valid ranges at startup, so you never have to hardcode min/max values per camera model.
163
+
164
+ If no PTZ camera is connected, all operations are silent no-ops that return `False`. This means your application starts and streams video correctly even when the PTZ camera isn't plugged in.
165
+
166
+ ```python
167
+ from streamcraft import V4L2PTZCamera
168
+
169
+ cam = V4L2PTZCamera("/dev/video0")
170
+
171
+ if cam.available:
172
+ cam.set_pan(90_000) # pan right (units are arc-seconds × 100)
173
+ cam.set_tilt(-50_000) # tilt down
174
+ cam.set_zoom(200) # zoom in (range is camera-specific)
175
+ cam.reset() # back to center, minimum zoom
176
+
177
+ print(cam.status.to_dict())
178
+ # {'available': True, 'pan': 0, 'tilt': 0, 'zoom': 100,
179
+ # 'ranges': {'pan': {'min': -522000, 'max': 522000}, ...}}
180
+ ```
181
+
182
+ ### `WebRTCSession`
183
+
184
+ Manages the entire WebRTC signaling lifecycle for a GStreamer pipeline: SDP offer/answer exchange, ICE candidate buffering (including the race condition where candidates arrive before the remote description is set), GLib→asyncio thread bridging, and pipeline state transitions.
185
+
186
+ Decoupled from any web framework — `send_json` is any async callable that accepts a dict, so it works with aiohttp, FastAPI, Starlette, or raw `websockets` with no modification.
187
+
188
+ ```python
189
+ from streamcraft import WebRTCSession
190
+ from aiohttp import web
191
+ import json
192
+
193
+ async def handle_ws(request):
194
+ ws = web.WebSocketResponse()
195
+ await ws.prepare(request)
196
+
197
+ pipeline = build_my_pipeline() # your PipelineBuilder call
198
+ session = WebRTCSession(pipeline)
199
+ session.connect_ice_sender(ws.send_json) # forwards local ICE candidates
200
+
201
+ try:
202
+ async for msg in ws:
203
+ if msg.type == web.WSMsgType.TEXT:
204
+ await session.handle_message(json.loads(msg.data), ws.send_json)
205
+ finally:
206
+ session.stop() # releases camera, mic, and all GStreamer resources
207
+
208
+ return ws
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Installation
214
+
215
+ streamcraft depends on GStreamer's Python bindings, which come from the system package manager and are not available on PyPI. Install the system packages first, then install streamcraft into a virtual environment.
216
+
217
+ **Step 1 — system dependencies (Debian / Ubuntu / Raspberry Pi OS):**
218
+
219
+ ```bash
220
+ sudo apt update && sudo apt install -y \
221
+ python3-gi \
222
+ gir1.2-gstreamer-1.0 \
223
+ gir1.2-gst-plugins-base-1.0 \
224
+ gir1.2-gst-plugins-bad-1.0 \
225
+ gstreamer1.0-plugins-base \
226
+ gstreamer1.0-plugins-good \
227
+ gstreamer1.0-plugins-bad \
228
+ gstreamer1.0-plugins-ugly \
229
+ gstreamer1.0-libav \
230
+ gstreamer1.0-tools \
231
+ gstreamer1.0-alsa \
232
+ v4l-utils
233
+ ```
234
+
235
+ **Step 2 — create a virtual environment with access to the system packages:**
236
+
237
+ The `--system-site-packages` flag is important — it gives the virtual environment visibility into the GStreamer bindings that `apt` installed in Step 1. Without it, `import gi` would fail inside the venv even though GStreamer is installed on the system.
238
+
239
+ ```bash
240
+ python -m venv --system-site-packages ~/streamcraft_env
241
+ source ~/streamcraft_env/bin/activate
242
+ ```
243
+
244
+ **Step 3 — install streamcraft:**
245
+
246
+ ```bash
247
+ # From GitHub:
248
+ pip install git+https://github.com/ranaweerasupun/streamcraft.git
249
+
250
+ # With aiohttp for the streaming server example:
251
+ pip install "git+https://github.com/ranaweerasupun/streamcraft.git#egg=streamcraft[aiohttp]"
252
+
253
+ # Or clone and install locally:
254
+ git clone https://github.com/ranaweerasupun/streamcraft.git
255
+ cd streamcraft
256
+ pip install -e ".[aiohttp]"
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Example project — bidirectional streaming server
262
+
263
+ The `examples/` directory contains a complete bidirectional video and audio streaming server. Run it on a Raspberry Pi 5; connect from any browser on a device in the same [Tailscale](https://tailscale.com) network.
264
+
265
+ What it does: streams live H.264 video and Opus audio from the Pi's camera to the browser over WebRTC, receives the browser's camera and microphone back on the Pi, and exposes the camera's pan/tilt/zoom controls through sliders in the browser UI. No robot-specific code, no serial ports, no joysticks — just streaming, which is useful to almost anyone.
266
+
267
+ What streamcraft replaces in the example: the GStreamer pipeline that would have been ~130 lines of `ElementFactory.make()` and `link()` boilerplate is 12 readable lines. The WebRTC signaling handler that would have been ~140 lines of SDP parsing, ICE buffering, and GLib→asyncio threading code is one `WebRTCSession` call. The PTZ camera class that would have been ~190 lines is one `V4L2PTZCamera` line.
268
+
269
+ **Running the example:**
270
+
271
+ ```bash
272
+ git clone https://github.com/ranaweerasupun/streamcraft.git
273
+ cd streamcraft
274
+
275
+ # Set up the environment
276
+ python -m venv --system-site-packages ~/streamcraft_env
277
+ source ~/streamcraft_env/bin/activate
278
+ pip install -e ".[aiohttp]"
279
+
280
+ # Generate a TLS certificate
281
+ # Option A — Tailscale HTTPS (requires a paid Tailscale plan):
282
+ sudo tailscale cert <your-device-fqdn>
283
+
284
+ # Option B — self-signed certificate (free, browser will show a warning once):
285
+ sudo mkdir -p /var/lib/tailscale/certs
286
+ sudo openssl req -x509 -newkey rsa:4096 -days 365 -nodes \
287
+ -keyout /var/lib/tailscale/certs/<your-fqdn>.key \
288
+ -out /var/lib/tailscale/certs/<your-fqdn>.crt \
289
+ -subj "/CN=<your-fqdn>"
290
+
291
+ # Grant your user read access to the certificate files
292
+ sudo setfacl -m u:$USER:x /var/lib/tailscale
293
+ sudo setfacl -m u:$USER:rx /var/lib/tailscale/certs
294
+ sudo setfacl -m u:$USER:r /var/lib/tailscale/certs/<your-fqdn>.crt
295
+ sudo setfacl -m u:$USER:r /var/lib/tailscale/certs/<your-fqdn>.key
296
+
297
+ # Edit the four config lines at the top of the server file
298
+ nano examples/streaming_server.py
299
+
300
+ # Run from the project root
301
+ python examples/streaming_server.py
302
+ ```
303
+
304
+ Then open `https://<your-device-fqdn>:8443` in a browser on another device in your Tailscale network.
305
+
306
+ ---
307
+
308
+ ## Design philosophy
309
+
310
+ **Sit on top, not underneath.** streamcraft reduces boilerplate without hiding GStreamer concepts. You still work with real `Gst.Pipeline`, `Gst.Element`, and `Gst.Pad` objects. When you need to do something the builder doesn't support, you use the standard GStreamer API directly on the returned objects — the library never gets in the way.
311
+
312
+ **Linear pipelines only (in the builder).** The builder handles the common case: a straight chain from source to sink. This deliberate constraint keeps the builder simple and predictable. Anything more complex uses the builder for the linear parts and the raw GStreamer API for the rest.
313
+
314
+ **Fail fast with actionable messages.** Missing plugin? You get the exact `apt install` command. Bad property name? You get the GStreamer hyphenated name next to your typo, plus the `gst-inspect-1.0` command to look it up. Failed link? You get the element names and a hint about which pad is incompatible. The error messages are part of the library's contract.
315
+
316
+ **No web framework lock-in.** `WebRTCSession` takes a callable, not a framework object. Any async function that accepts a dict and sends it as JSON is compatible — aiohttp, FastAPI, Starlette, raw `websockets`, or anything else.
317
+
318
+ ---
319
+
320
+ ## Running the tests
321
+
322
+ ```bash
323
+ # Software-only tests — no hardware required, runs in CI
324
+ pytest
325
+
326
+ # Including hardware tests — requires a camera on /dev/video0
327
+ pytest -m hardware
328
+ ```
329
+
330
+ The test suite has 93 tests. Hardware-dependent tests are marked `@pytest.mark.hardware` and skipped by default. The software-only tests use GStreamer's built-in test elements (`videotestsrc`, `audiotestsrc`, `fakesink`) and complete in under one second.
331
+
332
+ ---
333
+
334
+ ## Project structure
335
+
336
+ ```
337
+ streamcraft/
338
+ ├── streamcraft/ ← the installable Python package
339
+ │ ├── __init__.py ← public API
340
+ │ ├── pipeline.py ← PipelineBuilder
341
+ │ ├── devices.py ← require_elements, V4L2PTZCamera, check_v4l2_device
342
+ │ └── webrtc.py ← WebRTCSession
343
+ ├── examples/
344
+ │ ├── streaming_server.py ← bidirectional streaming server (Raspberry Pi)
345
+ │ └── interface.html ← browser UI served by the example server
346
+ ├── tests/
347
+ │ ├── test_pipeline.py ← 37 tests for PipelineBuilder
348
+ │ ├── test_devices.py ← 37 tests for devices module
349
+ │ └── test_webrtc.py ← 19 tests for WebRTCSession
350
+ ├── pyproject.toml
351
+ └── README.md
352
+ ```
353
+
354
+ ---
355
+
356
+ ## License
357
+
358
+ MIT — see [LICENSE](LICENSE) for details.