8000 Add overlay, hflip, and drawbox operators; use a more real-world exam… · Powercoder64/ffmpeg-python@f3b32d6 · GitHub
[go: up one dir, main page]

Skip to content

Commit f3b32d6

Browse files
committed
Add overlay, hflip, and drawbox operators; use a more real-world example in docs
1 parent 1f7736d commit f3b32d6

File tree

9 files changed

+101
-56
lines changed

9 files changed

+101
-56
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.cache
22
dist/
3-
ffmpeg/tests/dummy2.mp4
3+
ffmpeg/tests/sample_data/dummy2.mp4
44
venv

README

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ ffmpeg -i input.mp4 \
3535
-filter_complex "\
3636
[0]trim=start_frame=10:end_frame=20,setpts=PTS-STARTPTS[v0];\
3737
[0]trim=start_frame=30:end_frame=40,setpts=PTS-STARTPTS[v1];\
38-
[0]trim=start_frame=50:end_frame=60,setpts=PTS-STARTPTS[v2];\
39-
[v0][v1][v2]concat=n=3[v3]"\
40-
-map [v3] 8000 output.mp4
38+
[v0][v1]concat=n=2[v2];\
39+
[1]hflip[v3];\
40+
[v2][v3]overlay=eof_action=repeat[v4];\
41+
[v4]drawbox=50:50:120:120:red:t=5[v5]"\
42+
-map [v5] output.mp4
4143
```
4244

4345
Maybe this looks great to you, but if you haven't worked with FFmpeg before, it probably looks pretty alien.
@@ -46,13 +48,16 @@ If you're like me and find Python to be powerful and readable, it's easy with `f
4648
```
4749
import ffmpeg
4850

49-
in_file = ffmpeg.file_input('input.mp4') \
50-
ffmpeg.concat(
51-
in_file.trim(start_frame=10, end_frame=20),
52-
in_file.trim(start_frame=30, end_frame=40),
53-
in_file.trim(start_frame=50, end_frame=60)
51+
in_file = ffmpeg.file_input(TEST_INPUT_FILE)
52+
overlay_file = ffmpeg.file_input(TEST_OVERLAY_FILE)
53+
ffmpeg \
54+
.concat(
55+
in_file.trim(10, 20),
56+
in_file.trim(30, 40),
5457
) \
55-
.file_output('output.mp4') \
58+
.overlay(overlay_file.hflip()) \
59+
.drawbox(50, 50, 120, 120, color='red', thickness=5) \
60+
.file_output(TEST_OUTPUT_FILE) \
5661
.run()
5762
```
5863

doc/graph1.png

20.7 KB
Loading

ffmpeg/__init__.py

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def _create_root_node(node_class, *args, **kwargs):
1515

1616

1717
def _create_child_node(node_class, parent, *args, **kwargs):
18-
child = node_class([parent], *args, **kwargs)
18+
child = node_class(parent, *args, **kwargs)
1919
child._update_hash()
2020
return child
2121

@@ -31,11 +31,7 @@ def _add_operator(cls, node_class):
3131
if not getattr(node_class, 'STATIC', False):
3232
def func(self, *args, **kwargs):
3333
return _create_child_node(node_class, self, *args, **kwargs)
34-
else:
35-
@classmethod
36-
def func(cls2, *args, **kwargs):
37-
return _create_root_node(node_class, *args, **kwargs)
38-
setattr(cls, node_class.NAME, func)
34+
setattr(cls, node_class.NAME, func)
3935

4036
@classmethod
4137
def _add_operators(cls, node_classes):
@@ -75,18 +71,59 @@ def __init__(self, filename):
7571

7672

7773
class _FilterNode(_Node):
78-
pass
74+
def _get_filter(self):
75+
raise NotImplementedError()
7976

8077

81-
class _TrimFilterNode(_FilterNode):
78+
class _TrimNode(_FilterNode):
8279
NAME = 'trim'
8380

84-
def __init__(self, parents, start_frame, end_frame, setpts='PTS-STARTPTS'):
85-
super(_TrimFilterNode, self).__init__(parents)
81+
def __init__(self, parent, start_frame, end_frame, setpts='PTS-STARTPTS'):
82+
super(_TrimNode, self).__init__([parent])
8683
self.start_frame = start_frame
8784
self.end_frame = end_frame
8885
self.setpts = setpts
8986

87+
def _get_filter(self):
88+
return 'trim=start_frame={}:end_frame={},setpts={}'.format(self.start_frame, self.end_frame, self.setpts)
89+
90+
91+
class _OverlayNode(_FilterNode):
92+
NAME = 'overlay'
93+
94+
def __init__(self, main_parent, overlay_parent, eof_action='repeat'):
95+
super(_OverlayNode, self).__init__([main_parent, overlay_parent])
96+
self.eof_action = eof_action
97+
98+
def _get_filter(self):
99+
return 'overlay=eof_action={}'.format(self.eof_action)
100+
101+
102+
class _HFlipNode(_FilterNode):
103+
NAME = 'hflip'
104+
105+
def __init__(self, parent):
106+
super(_HFlipNode, self).__init__([parent])
107+
108+
def _get_filter(self):
109+
return 'hflip'
110+
111+
112+
class _DrawBoxNode(_FilterNode):
113+
NAME = 'drawbox'
114+
115+
def __init__(self, parent, x, y, width, height, color, thickness=1):
116+
super(_DrawBoxNode, self).__init__([parent])
117+
self.x = x
118+
self.y = y
119+
self.width = width
120+
self.height = height
121+
self.color = color
122+
self.thickness = thickness
123+
124+
def _get_filter(self):
125+
return 'drawbox={}:{}:{}:{}:{}:t={}'.format(self.x, self.y, self.width, self.height, self.color, self.thickness)
126+
90127

91128
class _ConcatNode(_Node):
92129
NAME = 'concat'
@@ -95,6 +132,9 @@ class _ConcatNode(_Node):
95132
def __init__(self, *parents):
96133
super(_ConcatNode, self).__init__(parents)
97134

135+
def _get_filter(self):
136+
return 'concat=n={}'.format(len(self.parents))
137+
98138

99139
class _OutputNode(_Node):
100140
@classmethod
@@ -130,22 +170,12 @@ def visit(node, child):
130170
visit(unmarked_nodes.pop(), None)
131171
return sorted_nodes, child_map
132172

133-
@classmethod
134-
def _get_filter(cls, node):
135-
# TODO: find a better way to do this instead of ugly if/elifs.
136-
if isinstance(node, _TrimFilterNode):
137-
return 'trim=start_frame={}:end_frame={},setpts={}'.format(node.start_frame, node.end_frame, node.setpts)
138-
elif isinstance(node, _ConcatNode):
139-
return 'concat=n={}'.format(len(node.parents))
140-
else:
141-
assert False, 'Unsupported filter node: {}'.format(node)
142-
143173
@classmethod
144174
def _get_filter_spec(cls, i, node, stream_name_map):
145175
stream_name = cls._get_stream_name('v{}'.format(i))
146176
stream_name_map[node] = stream_name
147177
inputs = [stream_name_map[parent] for parent in node.parents]
148-
filter_spec = '{}{}{}'.format(''.join(inputs), cls._get_filter(node), stream_name)
178+
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), stream_name)
149179
return filter_spec
150180

151181
@classmethod
@@ -197,19 +227,18 @@ def run(self):
197227

198228

199229
class _GlobalNode(_OutputNode):
200-
def __init__(self, parents):
201-
assert len(parents) == 1
202-
assert isinstance(parents[0], _OutputNode), 'Global nodes can only be attached after output nodes'
203-
super(_GlobalNode, self).__init__(parents)
230+
def __init__(self, parent):
231+
assert isinstance(parent, _OutputNode), 'Global nodes can only be attached after output nodes'
232+
super(_GlobalNode, self).__init__([parent])
204233

205234

206235
class _OverwriteOutputNode(_GlobalNode):
207236
NAME = 'overwrite_output'
208237

209238

210-
211239
class _MergeOutputsNode(_OutputNode):
212240
NAME = 'merge_outputs'
241+
STATIC = True
213242

214243
def __init__(self, *parents):
215244
assert not any([not isinstance(parent, _OutputNode) for parent in parents]), 'Can only merge output streams'
@@ -219,17 +248,20 @@ def __init__(self, *parents):
219248
class _FileOutputNode(_OutputNode):
220249
NAME = 'file_output'
221250

222-
def __init__(self, parents, filename):
223-
super(_FileOutputNode, self).__init__(parents)
251+
def __init__(self, parent, filename):
252+
super(_FileOutputNode, self).__init__([parent])
224253
self.filename = filename
225254

226255

227256
NODE_CLASSES = [
257+
_HFlipNode,
258+
_DrawBoxNode,
228259
_ConcatNode,
229260
_FileInputNode,
230261
_FileOutputNode,
262+
_OverlayNode,
231263
_OverwriteOutputNode,
232-
_TrimFilterNode,
264+
_TrimNode,
233265
]
234266

235267
_Node._add_operators(NODE_CLASSES)
File renamed without changes.

ffmpeg/tests/sample_data/overlay.png

2.16 KB
Loading

ffmpeg/tests/test_ffmpeg.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44

55
TEST_DIR = os.path.dirname(__file__)
6-
TEST_INPUT_FILE = os.path.join(TEST_DIR, 'dummy.mp4')
7-
TEST_OUTPUT_FILE = os.path.join(TEST_DIR, 'dummy2.mp4')
6+
SAMPLE_DATA_DIR = os.path.join(TEST_DIR, 'sample_data')
7+
TEST_INPUT_FILE = os.path.join(SAMPLE_DATA_DIR, 'dummy.mp4')
8+
TEST_OVERLAY_FILE = os.path.join(SAMPLE_DATA_DIR, 'overlay.png')
9+
TEST_OUTPUT_FILE = os.path.join(SAMPLE_DATA_DIR, 'dummy2.mp4')
810

911

1012
def test_fluent_equality():
@@ -77,26 +79,33 @@ def test_get_args_simple():
7779

7880
def _get_complex_filter_example():
7981
in_file = ffmpeg.file_input(TEST_INPUT_FILE)
80-
concatted = ffmpeg.concat(
81-
ffmpeg.trim(in_file, 10, 20),
82-
ffmpeg.trim(in_file, 30, 40),
83-
ffmpeg.trim(in_file, 50, 60),
84-
)
85-
out = ffmpeg.file_output(concatted, TEST_OUTPUT_FILE)
86-
return ffmpeg.overwrite_output(out)
82+
overlay_file = ffmpeg.file_input(TEST_OVERLAY_FILE)
83+
return ffmpeg \
84+
.concat(
85+
in_file.trim(10, 20),
86+
in_file.trim(30, 40),
87+
) \
88+
.overlay(overlay_file.hflip()) \
89+
.drawbox(50, 50, 120, 120, color='red', thickness=5) \
90+
.file_output(TEST_OUTPUT_FILE) \
91+
.overwrite_output()
8792

8893

8994
def test_get_args_complex_filter():
9095
out = _get_complex_filter_example()
91-
assert ffmpeg.get_args(out) == [
96+
args = ffmpeg.get_args(out)
97+
assert args == [
9298
'-i', TEST_INPUT_FILE,
93-
'-filter_complex',
99+
'-i', TEST_OVERLAY_FILE,
100+
'-filter_complex',
94101
'[0]trim=start_frame=10:end_frame=20,setpts=PTS-STARTPTS[v0];' \
95102
'[0]trim=start_frame=30:end_frame=40,setpts=PTS-STARTPTS[v1];' \
96-
'[0]trim=start_frame=50:end_frame=60,setpts=PTS-STARTPTS[v2];' \
97-
'[v0][v1][v2]concat=n=3[v3]',
98-
'-map', '[v3]', TEST_OUTPUT_FILE,
99-
'-y',
103+
'[v0][v1]concat=n=2[v2];' \
104+
'[1]hflip[v3];' \
105+
'[v2][v3]overlay=eof_action=repeat[v4];' \
106+
'[v4]drawbox=50:50:120:120:red:t=5[v5]',
107+
'-map', '[v5]', '/Users/karlk/src/ffmpeg_wrapper/ffmpeg/tests/sample_data/dummy2.mp4',
108+
'-y'
100109
]
101110

102111

pytest.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
[pytest]
22
testpaths = ffmpeg/tests
3-
#norecursedirs = venv .git

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
setup(
33
name = 'ffmpeg-python',
44
packages = ['ffmpeg'],
5-
version = '0.1',
5+
version = '0.1.1',
66
description = 'FFmpeg Python wrapper with support for complex filtering',
77
author = 'Karl Kroening',
88
author_email = 'karlk@kralnet.us',

0 commit comments

Comments
 (0)
0