candyfloss 0.0.1__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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Robert Poekert
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.
22
+
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: candyfloss
3
+ Version: 0.0.1
4
+ Summary: An ergonomic interface over GStreamer
5
+ Author-email: Bob Poekert <bob@poekert.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://git.hella.cheap/bob/candyfloss
8
+ Classifier: Programming Language :: Python :: 3
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+
15
+ # Candyfloss
16
+
17
+ Candyfloss is an ergonomic interface to GStreamer. It allows users to build and run pipelines to decode and encode video files, extract video frames to use from python code, map python code over video frames, etc.
18
+
19
+ ## Installation
20
+
21
+ Candyfloss is installable by running setup.py in the normal way. It should also be available on PyPI soon.
22
+
23
+ Candyfloss requires that gstreamer is installed. Most desktop linux distros have it installed already. If you aren't on linux or don't have it installed check the GStreamer install docs [here](https://gstreamer.freedesktop.org/documentation/installing/index.html?gi-language=c). In addition to the installation methods mentioned there if you're on macos you can install it with homebrew by running `brew install gstreamer`.
24
+
25
+ ## Examples
26
+
27
+ ```python
28
+
29
+ # scale a video file to 300x300
30
+
31
+ from candyfloss import Pipeline
32
+ from candyfloss.utils import decode_file
33
+
34
+ with Pipeline() as p:
35
+
36
+ inp_file = p >> decode_file('input.mp4')
37
+ scaled_video = inp_file >> 'videoconvert' >> 'videoscale' >> ('video/x-raw', {'width':300,'height':300})
38
+
39
+ mux = p >> 'mp4mux'
40
+ scaled_video >> 'x264enc' >> mux
41
+ inp_file >> 'avenc_aac' >> mux
42
+ mux >> ['filesink', {'location':'output.mp4'}]
43
+
44
+ ```
45
+
46
+
47
+ ```python
48
+
49
+ # iterate over frames from a video file
50
+
51
+ from candyfloss import Pipeline
52
+
53
+ for frame in Pipeline(lambda p: p >> decode_file('input.webm')):
54
+ frame.save('frame.jpeg') # frame is a PIL image
55
+
56
+ ```
57
+
58
+ ```python
59
+
60
+ # display your webcam with the classic emboss effect applied
61
+
62
+ from candyfloss import Pipeline
63
+ from PIL import ImageFilter
64
+
65
+ with Pipeline() as p:
66
+ p >> 'autovideosrc' >> p.map(lambda frame: frame.filter(ImageFilter.EMBOSS)) >> 'autovideosink'
67
+
68
+ ```
69
+
70
+ ```python
71
+
72
+ # display random noise frames in a window
73
+
74
+ from candyfloss import Pipeline
75
+ from candyfloss.utils import display_video
76
+ from PIL import Image
77
+ import numpy as np
78
+
79
+ def random_frames():
80
+ rgb_shape = (300, 300, 3)
81
+ while 1:
82
+ mat = np.random.randint(0, 256, dtype=np.uint8, size=rgb_shape)
83
+ yield Image.fromarray(mat)
84
+
85
+ with Pipeline() as p:
86
+ p.from_iter(random_frames(), (300, 300)) >> display_video()
87
+
88
+ ```
@@ -0,0 +1,75 @@
1
+
2
+ # Candyfloss
3
+
4
+ Candyfloss is an ergonomic interface to GStreamer. It allows users to build and run pipelines to decode and encode video files, extract video frames to use from python code, map python code over video frames, etc.
5
+
6
+ ## Installation
7
+
8
+ Candyfloss is installable by running setup.py in the normal way. It should also be available on PyPI soon.
9
+
10
+ Candyfloss requires that gstreamer is installed. Most desktop linux distros have it installed already. If you aren't on linux or don't have it installed check the GStreamer install docs [here](https://gstreamer.freedesktop.org/documentation/installing/index.html?gi-language=c). In addition to the installation methods mentioned there if you're on macos you can install it with homebrew by running `brew install gstreamer`.
11
+
12
+ ## Examples
13
+
14
+ ```python
15
+
16
+ # scale a video file to 300x300
17
+
18
+ from candyfloss import Pipeline
19
+ from candyfloss.utils import decode_file
20
+
21
+ with Pipeline() as p:
22
+
23
+ inp_file = p >> decode_file('input.mp4')
24
+ scaled_video = inp_file >> 'videoconvert' >> 'videoscale' >> ('video/x-raw', {'width':300,'height':300})
25
+
26
+ mux = p >> 'mp4mux'
27
+ scaled_video >> 'x264enc' >> mux
28
+ inp_file >> 'avenc_aac' >> mux
29
+ mux >> ['filesink', {'location':'output.mp4'}]
30
+
31
+ ```
32
+
33
+
34
+ ```python
35
+
36
+ # iterate over frames from a video file
37
+
38
+ from candyfloss import Pipeline
39
+
40
+ for frame in Pipeline(lambda p: p >> decode_file('input.webm')):
41
+ frame.save('frame.jpeg') # frame is a PIL image
42
+
43
+ ```
44
+
45
+ ```python
46
+
47
+ # display your webcam with the classic emboss effect applied
48
+
49
+ from candyfloss import Pipeline
50
+ from PIL import ImageFilter
51
+
52
+ with Pipeline() as p:
53
+ p >> 'autovideosrc' >> p.map(lambda frame: frame.filter(ImageFilter.EMBOSS)) >> 'autovideosink'
54
+
55
+ ```
56
+
57
+ ```python
58
+
59
+ # display random noise frames in a window
60
+
61
+ from candyfloss import Pipeline
62
+ from candyfloss.utils import display_video
63
+ from PIL import Image
64
+ import numpy as np
65
+
66
+ def random_frames():
67
+ rgb_shape = (300, 300, 3)
68
+ while 1:
69
+ mat = np.random.randint(0, 256, dtype=np.uint8, size=rgb_shape)
70
+ yield Image.fromarray(mat)
71
+
72
+ with Pipeline() as p:
73
+ p.from_iter(random_frames(), (300, 300)) >> display_video()
74
+
75
+ ```
@@ -0,0 +1 @@
1
+ from .core import *
@@ -0,0 +1,264 @@
1
+ import c_candyfloss as cc
2
+
3
+ class PipelineError(Exception):
4
+
5
+ def __init__(self, *messages):
6
+ self.message = ' @ '.join(messages)
7
+ self.stack_info = None
8
+
9
+ def add_stack_info(self, v):
10
+ self.stack_info = '\n'.join(traceback.StackSummary(v).format())
11
+
12
+ def __str__(self):
13
+ info = self.stack_info or ''
14
+ return '%s %s' % (info, self.message)
15
+
16
+ def __repr__(self):
17
+ return 'PipelineError: %s' % str(self)
18
+
19
+ cc.set_exception_class(PipelineError)
20
+
21
+ import os
22
+ import threading
23
+ import queue
24
+ from inspect import signature
25
+ import traceback
26
+ import time
27
+
28
+ from PIL import Image
29
+
30
+ def parse_dict(inp):
31
+ res = []
32
+ for k, v in inp.items():
33
+ if type(v) == str:
34
+ v = v.encode('utf-8')
35
+ res.append((str(k), v))
36
+ return res
37
+
38
+
39
+ class IteratorSourceWrapper:
40
+
41
+ def __init__(self, pipeline, inp_iter, outp_shape, framerate=30):
42
+ self.outp_shape = outp_shape
43
+ self.inp_iter = inp_iter
44
+ self.pipeline = pipeline
45
+ self.nanos_per_frame = int(1000000000 / framerate)
46
+
47
+ def __iter__(self):
48
+ try:
49
+ for frame_i, img in enumerate(self.inp_iter):
50
+ if img.width != self.outp_shape[0] or img.height != self.outp_shape[1]:
51
+ img = img.resize(self.outp_shape)
52
+ if img.format != 'RGB':
53
+ img = img.convert('RGB')
54
+ yield (
55
+ img.tobytes(),
56
+ int(self.nanos_per_frame * frame_i),
57
+ self.nanos_per_frame,
58
+ frame_i)
59
+ except Exception as e:
60
+ self.pipeline.exc = e
61
+
62
+ class UserCallback:
63
+
64
+ def __init__(self, pipeline, f):
65
+ self.n_args = len(signature(f).parameters)
66
+ if self.n_args not in (1, 2):
67
+ raise ValueError('invalid callback signature')
68
+ self.f = f
69
+ self.pipeline = pipeline
70
+
71
+ def __call__(self, data, inp_shape, outp_shape):
72
+ try:
73
+ img = Image.frombytes('RGB', inp_shape, data)
74
+ if self.n_args == 1:
75
+ res = self.f(img)
76
+ elif self.n_args == 2:
77
+ res = self.f(img, outp_shape)
78
+
79
+ if res.width != outp_shape[0] or res.height != outp_shape[1]:
80
+ res = res.resize(outp_shape)
81
+
82
+ return res.tobytes()
83
+ except Exception as e:
84
+ self.pipeline.exc = e
85
+
86
+
87
+ def util_fn(f):
88
+ def _res(*args, **kwargs):
89
+ def _r2(pipeline):
90
+ root = PipelineEl(pipeline, None)
91
+ return f(root, *args, **kwargs)
92
+ return _r2
93
+ _res.__name__ = f.__name__
94
+ return _res
95
+
96
+ class PipelineEl:
97
+
98
+ def __init__(self, pipeline, arg):
99
+ self.pipeline = pipeline
100
+
101
+ if arg is None:
102
+ self.obj = None
103
+ elif type(arg) == tuple:
104
+ self.obj = cc.make_capsfilter(pipeline.pipeline, arg[0], parse_dict(arg[1]))
105
+ elif type(arg) == str:
106
+ self.obj, name = cc.construct_element(pipeline.pipeline, arg, [])
107
+ self.pipeline.stacks[name] = traceback.extract_stack()
108
+ elif type(arg) == list:
109
+ self.obj, name = cc.construct_element(pipeline.pipeline, arg[0], parse_dict(arg[1]))
110
+ self.pipeline.stacks[name] = traceback.extract_stack()
111
+ else:
112
+ raise TypeError('invalid argument type: %r' % type(arg))
113
+
114
+ def link(self, other):
115
+ if self.obj is not None:
116
+ cc.link_elements(self.obj, other.obj)
117
+
118
+ @classmethod
119
+ def to_el(cls, upstream, pipeline, v, rec=False):
120
+ if isinstance(v, cls):
121
+ return v
122
+ else:
123
+ try:
124
+ return cls(pipeline, v)
125
+ except TypeError as e:
126
+ if callable(v):
127
+ return v(pipeline)
128
+ else:
129
+ raise e
130
+
131
+ def __rshift__(self, other):
132
+ other = self.to_el(self, self.pipeline, other)
133
+ self.link(other)
134
+ return other
135
+
136
+ def from_iter(self, inp, outp_shape, framerate=30, **kwargs):
137
+ res = PipelineEl(self.pipeline, None)
138
+ kwargs['width'] = outp_shape[0]
139
+ kwargs['height'] = outp_shape[1]
140
+ res.obj = cc.make_iterator_source(
141
+ self.pipeline.pipeline,
142
+ IteratorSourceWrapper(self.pipeline, inp, outp_shape, framerate=framerate),
143
+ list(kwargs.items()))
144
+ return res
145
+
146
+ def map(self, f):
147
+ res = PipelineEl(self.pipeline, None)
148
+ res.obj = cc.make_callback_transform(self.pipeline.pipeline, UserCallback(self.pipeline, f))
149
+ return res
150
+
151
+
152
+ class CallbackSink:
153
+
154
+ def __init__(self, pipeline, end_el):
155
+ self.pipeline = pipeline
156
+ self.el = cc.make_callback_sink(pipeline.pipeline)
157
+ end_el = end_el >> 'videoconvert'
158
+ cc.link_elements(end_el.obj, self.el)
159
+
160
+ def __iter__(self):
161
+ while 1:
162
+ v = cc.appsink_pull_buffer(self.el, 10000000) # 100ms (in nanos)
163
+ if v is False:
164
+ if self.pipeline.is_done:
165
+ if self.pipeline.exc is not None:
166
+ raise self.pipeline.exc
167
+ break
168
+ else:
169
+ continue
170
+ if v is None:
171
+ break
172
+ buf, w, h = v
173
+ yield Image.frombytes('RGB', (w, h), buf)
174
+
175
+ import webbrowser, socketserver
176
+ def browser_open(data, mime=None):
177
+ class Handler(socketserver.BaseRequestHandler):
178
+
179
+ def handle(self):
180
+ self.request.send(b'HTTP/1.1 200 OK\r\n')
181
+ if mime is not None:
182
+ self.request.send(b'Content-Type: %s\r\n' % mime.encode('utf-8'))
183
+ self.request.send(b'\r\n')
184
+ self.request.send(data)
185
+
186
+ with socketserver.TCPServer(('127.0.0.1', 0), Handler) as server:
187
+ webbrowser.open('http://127.0.0.1:%d/' % server.server_address[1])
188
+ server.handle_request()
189
+
190
+ class Pipeline:
191
+
192
+ def __init__(self, gen_fn=None, name=None, debug_viz=False):
193
+ if name is None:
194
+ name = str(hash(self))
195
+ self.stacks = {}
196
+ self.error_stack = None
197
+ self.exc = None
198
+ self.run_lock = threading.Lock()
199
+ self.run_lock.acquire(blocking=False)
200
+ self.pipeline = cc.make_pipeline(name, self._on_done)
201
+ self.is_done = False
202
+ self.gen_el = None
203
+ self.debug_viz = debug_viz
204
+
205
+ if gen_fn is not None:
206
+ self.gen_el = gen_fn(PipelineEl(self, None))
207
+
208
+ def _on_done(self, exc, el_names):
209
+ if not self.is_done:
210
+ if el_names is not None:
211
+ for name in reversed(el_names):
212
+ try:
213
+ self.error_stack = self.stacks[name]
214
+ break
215
+ except KeyError:
216
+ pass
217
+ if self.exc is None:
218
+ self.exc = exc
219
+ self.is_done = True
220
+ self.run_lock.release()
221
+
222
+ def run_async(self):
223
+ if self.is_done:
224
+ return
225
+ if self.debug_viz:
226
+ import graphviz
227
+ debug_dot = cc.dot_viz(self.pipeline)
228
+ svg = graphviz.pipe('dot', 'svg', debug_dot)
229
+ browser_open(svg, 'image/svg+xml')
230
+ cc.run_pipeline(self.pipeline)
231
+
232
+ def run(self):
233
+ self.run_async()
234
+ self.run_lock.acquire()
235
+ self.run_lock.release()
236
+ self._post_run()
237
+
238
+ def _post_run(self):
239
+ if self.exc is not None:
240
+ if self.error_stack is not None:
241
+ try:
242
+ self.exc.add_stack_info(self.error_stack)
243
+ except AttributeError:
244
+ pass
245
+ raise self.exc
246
+
247
+ def __enter__(self):
248
+ return PipelineEl(self, None)
249
+
250
+ def __exit__(self, exc_type, exc_val, exc_tb):
251
+ if exc_type is not None:
252
+ raise exc_val
253
+
254
+ self.run()
255
+
256
+ def __iter__(self):
257
+ if self.gen_el is None:
258
+ return []
259
+
260
+ res = CallbackSink(self, self.gen_el)
261
+ self.run_async()
262
+ for frame in res:
263
+ yield frame
264
+ self._post_run()
@@ -0,0 +1,47 @@
1
+ from .core import util_fn
2
+ import os
3
+
4
+ @util_fn
5
+ def decode_file(p, fname):
6
+ return p >> ['filesrc', {'location':os.path.abspath(fname)}] >> 'decodebin'
7
+
8
+ @util_fn
9
+ def decode_uri(p, uri):
10
+ return p >> ['uridecodebin', {'uri':uri}]
11
+
12
+ @util_fn
13
+ def display_video(p):
14
+ res = p >> 'videoconvert'
15
+ res >> 'autovideosink'
16
+ return res
17
+
18
+ @util_fn
19
+ def scale_video(p, w, h):
20
+ return p >> 'videoscale' >> ('video/x-raw', {'width':w, 'height':h})
21
+
22
+ @util_fn
23
+ def play_audio(p):
24
+ res = p >> 'audioconvert'
25
+ res >> 'autoaudiosink'
26
+ return res
27
+
28
+ FRAG_SHADER_TEMPLATE = '''
29
+ #version 100
30
+ #ifdef GL_ES
31
+ precision mediump float;
32
+ #endif
33
+ varying vec2 v_texcoord;
34
+ uniform sampler2D tex;
35
+ uniform float time;
36
+ uniform float width;
37
+ uniform float height;
38
+
39
+ void main () {
40
+ gl_FragColor = %s
41
+ }
42
+ '''
43
+
44
+ @util_fn
45
+ def gl_shader(p, shader_expr):
46
+ shader_code = FRAG_SHADER_TEMPLATE % shader_expr
47
+ return p >> 'glupload' >> ['glshader', {'fragment':shader_code}]
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: candyfloss
3
+ Version: 0.0.1
4
+ Summary: An ergonomic interface over GStreamer
5
+ Author-email: Bob Poekert <bob@poekert.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://git.hella.cheap/bob/candyfloss
8
+ Classifier: Programming Language :: Python :: 3
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+
15
+ # Candyfloss
16
+
17
+ Candyfloss is an ergonomic interface to GStreamer. It allows users to build and run pipelines to decode and encode video files, extract video frames to use from python code, map python code over video frames, etc.
18
+
19
+ ## Installation
20
+
21
+ Candyfloss is installable by running setup.py in the normal way. It should also be available on PyPI soon.
22
+
23
+ Candyfloss requires that gstreamer is installed. Most desktop linux distros have it installed already. If you aren't on linux or don't have it installed check the GStreamer install docs [here](https://gstreamer.freedesktop.org/documentation/installing/index.html?gi-language=c). In addition to the installation methods mentioned there if you're on macos you can install it with homebrew by running `brew install gstreamer`.
24
+
25
+ ## Examples
26
+
27
+ ```python
28
+
29
+ # scale a video file to 300x300
30
+
31
+ from candyfloss import Pipeline
32
+ from candyfloss.utils import decode_file
33
+
34
+ with Pipeline() as p:
35
+
36
+ inp_file = p >> decode_file('input.mp4')
37
+ scaled_video = inp_file >> 'videoconvert' >> 'videoscale' >> ('video/x-raw', {'width':300,'height':300})
38
+
39
+ mux = p >> 'mp4mux'
40
+ scaled_video >> 'x264enc' >> mux
41
+ inp_file >> 'avenc_aac' >> mux
42
+ mux >> ['filesink', {'location':'output.mp4'}]
43
+
44
+ ```
45
+
46
+
47
+ ```python
48
+
49
+ # iterate over frames from a video file
50
+
51
+ from candyfloss import Pipeline
52
+
53
+ for frame in Pipeline(lambda p: p >> decode_file('input.webm')):
54
+ frame.save('frame.jpeg') # frame is a PIL image
55
+
56
+ ```
57
+
58
+ ```python
59
+
60
+ # display your webcam with the classic emboss effect applied
61
+
62
+ from candyfloss import Pipeline
63
+ from PIL import ImageFilter
64
+
65
+ with Pipeline() as p:
66
+ p >> 'autovideosrc' >> p.map(lambda frame: frame.filter(ImageFilter.EMBOSS)) >> 'autovideosink'
67
+
68
+ ```
69
+
70
+ ```python
71
+
72
+ # display random noise frames in a window
73
+
74
+ from candyfloss import Pipeline
75
+ from candyfloss.utils import display_video
76
+ from PIL import Image
77
+ import numpy as np
78
+
79
+ def random_frames():
80
+ rgb_shape = (300, 300, 3)
81
+ while 1:
82
+ mat = np.random.randint(0, 256, dtype=np.uint8, size=rgb_shape)
83
+ yield Image.fromarray(mat)
84
+
85
+ with Pipeline() as p:
86
+ p.from_iter(random_frames(), (300, 300)) >> display_video()
87
+
88
+ ```
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ candyfloss/__init__.py
6
+ candyfloss/core.py
7
+ candyfloss/utils.py
8
+ candyfloss.egg-info/PKG-INFO
9
+ candyfloss.egg-info/SOURCES.txt
10
+ candyfloss.egg-info/dependency_links.txt
11
+ candyfloss.egg-info/top_level.txt
12
+ native/py_entrypoint.c
@@ -0,0 +1,2 @@
1
+ c_candyfloss
2
+ candyfloss