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.
- candyfloss-0.0.1/LICENSE +22 -0
- candyfloss-0.0.1/PKG-INFO +88 -0
- candyfloss-0.0.1/README.md +75 -0
- candyfloss-0.0.1/candyfloss/__init__.py +1 -0
- candyfloss-0.0.1/candyfloss/core.py +264 -0
- candyfloss-0.0.1/candyfloss/utils.py +47 -0
- candyfloss-0.0.1/candyfloss.egg-info/PKG-INFO +88 -0
- candyfloss-0.0.1/candyfloss.egg-info/SOURCES.txt +12 -0
- candyfloss-0.0.1/candyfloss.egg-info/dependency_links.txt +1 -0
- candyfloss-0.0.1/candyfloss.egg-info/top_level.txt +2 -0
- candyfloss-0.0.1/native/py_entrypoint.c +918 -0
- candyfloss-0.0.1/pyproject.toml +21 -0
- candyfloss-0.0.1/setup.cfg +4 -0
- candyfloss-0.0.1/setup.py +42 -0
candyfloss-0.0.1/LICENSE
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|