Add overlay, hflip, and drawbox operators; use a more real-world example in docs

This commit is contained in:
Karl Kroening 2017-05-14 00:18:09 -10:00
parent 1f7736ddb6
commit f3b32d6d41
9 changed files with 101 additions and 56 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.cache
dist/
ffmpeg/tests/dummy2.mp4
ffmpeg/tests/sample_data/dummy2.mp4
venv

23
README
View File

@ -35,9 +35,11 @@ ffmpeg -i input.mp4 \
-filter_complex "\
[0]trim=start_frame=10:end_frame=20,setpts=PTS-STARTPTS[v0];\
[0]trim=start_frame=30:end_frame=40,setpts=PTS-STARTPTS[v1];\
[0]trim=start_frame=50:end_frame=60,setpts=PTS-STARTPTS[v2];\
[v0][v1][v2]concat=n=3[v3]"\
-map [v3] output.mp4
[v0][v1]concat=n=2[v2];\
[1]hflip[v3];\
[v2][v3]overlay=eof_action=repeat[v4];\
[v4]drawbox=50:50:120:120:red:t=5[v5]"\
-map [v5] output.mp4
```
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
```
import ffmpeg
in_file = ffmpeg.file_input('input.mp4') \
ffmpeg.concat(
in_file.trim(start_frame=10, end_frame=20),
in_file.trim(start_frame=30, end_frame=40),
in_file.trim(start_frame=50, end_frame=60)
in_file = ffmpeg.file_input(TEST_INPUT_FILE)
overlay_file = ffmpeg.file_input(TEST_OVERLAY_FILE)
ffmpeg \
.concat(
in_file.trim(10, 20),
in_file.trim(30, 40),
) \
.file_output('output.mp4') \
.overlay(overlay_file.hflip()) \
.drawbox(50, 50, 120, 120, color='red', thickness=5) \
.file_output(TEST_OUTPUT_FILE) \
.run()
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -15,7 +15,7 @@ def _create_root_node(node_class, *args, **kwargs):
def _create_child_node(node_class, parent, *args, **kwargs):
child = node_class([parent], *args, **kwargs)
child = node_class(parent, *args, **kwargs)
child._update_hash()
return child
@ -31,11 +31,7 @@ class _Node(object):
if not getattr(node_class, 'STATIC', False):
def func(self, *args, **kwargs):
return _create_child_node(node_class, self, *args, **kwargs)
else:
@classmethod
def func(cls2, *args, **kwargs):
return _create_root_node(node_class, *args, **kwargs)
setattr(cls, node_class.NAME, func)
setattr(cls, node_class.NAME, func)
@classmethod
def _add_operators(cls, node_classes):
@ -75,18 +71,59 @@ class _FileInputNode(_InputNode):
class _FilterNode(_Node):
pass
def _get_filter(self):
raise NotImplementedError()
class _TrimFilterNode(_FilterNode):
class _TrimNode(_FilterNode):
NAME = 'trim'
def __init__(self, parents, start_frame, end_frame, setpts='PTS-STARTPTS'):
super(_TrimFilterNode, self).__init__(parents)
def __init__(self, parent, start_frame, end_frame, setpts='PTS-STARTPTS'):
super(_TrimNode, self).__init__([parent])
self.start_frame = start_frame
self.end_frame = end_frame
self.setpts = setpts
def _get_filter(self):
return 'trim=start_frame={}:end_frame={},setpts={}'.format(self.start_frame, self.end_frame, self.setpts)
class _OverlayNode(_FilterNode):
NAME = 'overlay'
def __init__(self, main_parent, overlay_parent, eof_action='repeat'):
super(_OverlayNode, self).__init__([main_parent, overlay_parent])
self.eof_action = eof_action
def _get_filter(self):
return 'overlay=eof_action={}'.format(self.eof_action)
class _HFlipNode(_FilterNode):
NAME = 'hflip'
def __init__(self, parent):
super(_HFlipNode, self).__init__([parent])
def _get_filter(self):
return 'hflip'
class _DrawBoxNode(_FilterNode):
NAME = 'drawbox'
def __init__(self, parent, x, y, width, height, color, thickness=1):
super(_DrawBoxNode, self).__init__([parent])
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
self.thickness = thickness
def _get_filter(self):
return 'drawbox={}:{}:{}:{}:{}:t={}'.format(self.x, self.y, self.width, self.height, self.color, self.thickness)
class _ConcatNode(_Node):
NAME = 'concat'
@ -95,6 +132,9 @@ class _ConcatNode(_Node):
def __init__(self, *parents):
super(_ConcatNode, self).__init__(parents)
def _get_filter(self):
return 'concat=n={}'.format(len(self.parents))
class _OutputNode(_Node):
@classmethod
@ -130,22 +170,12 @@ class _OutputNode(_Node):
visit(unmarked_nodes.pop(), None)
return sorted_nodes, child_map
@classmethod
def _get_filter(cls, node):
# TODO: find a better way to do this instead of ugly if/elifs.
if isinstance(node, _TrimFilterNode):
return 'trim=start_frame={}:end_frame={},setpts={}'.format(node.start_frame, node.end_frame, node.setpts)
elif isinstance(node, _ConcatNode):
return 'concat=n={}'.format(len(node.parents))
else:
assert False, 'Unsupported filter node: {}'.format(node)
@classmethod
def _get_filter_spec(cls, i, node, stream_name_map):
stream_name = cls._get_stream_name('v{}'.format(i))
stream_name_map[node] = stream_name
inputs = [stream_name_map[parent] for parent in node.parents]
filter_spec = '{}{}{}'.format(''.join(inputs), cls._get_filter(node), stream_name)
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), stream_name)
return filter_spec
@classmethod
@ -197,19 +227,18 @@ class _OutputNode(_Node):
class _GlobalNode(_OutputNode):
def __init__(self, parents):
assert len(parents) == 1
assert isinstance(parents[0], _OutputNode), 'Global nodes can only be attached after output nodes'
super(_GlobalNode, self).__init__(parents)
def __init__(self, parent):
assert isinstance(parent, _OutputNode), 'Global nodes can only be attached after output nodes'
super(_GlobalNode, self).__init__([parent])
class _OverwriteOutputNode(_GlobalNode):
NAME = 'overwrite_output'
class _MergeOutputsNode(_OutputNode):
NAME = 'merge_outputs'
STATIC = True
def __init__(self, *parents):
assert not any([not isinstance(parent, _OutputNode) for parent in parents]), 'Can only merge output streams'
@ -219,17 +248,20 @@ class _MergeOutputsNode(_OutputNode):
class _FileOutputNode(_OutputNode):
NAME = 'file_output'
def __init__(self, parents, filename):
super(_FileOutputNode, self).__init__(parents)
def __init__(self, parent, filename):
super(_FileOutputNode, self).__init__([parent])
self.filename = filename
NODE_CLASSES = [
_HFlipNode,
_DrawBoxNode,
_ConcatNode,
_FileInputNode,
_FileOutputNode,
_OverlayNode,
_OverwriteOutputNode,
_TrimFilterNode,
_TrimNode,
]
_Node._add_operators(NODE_CLASSES)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -3,8 +3,10 @@ import os
TEST_DIR = os.path.dirname(__file__)
TEST_INPUT_FILE = os.path.join(TEST_DIR, 'dummy.mp4')
TEST_OUTPUT_FILE = os.path.join(TEST_DIR, 'dummy2.mp4')
SAMPLE_DATA_DIR = os.path.join(TEST_DIR, 'sample_data')
TEST_INPUT_FILE = os.path.join(SAMPLE_DATA_DIR, 'dummy.mp4')
TEST_OVERLAY_FILE = os.path.join(SAMPLE_DATA_DIR, 'overlay.png')
TEST_OUTPUT_FILE = os.path.join(SAMPLE_DATA_DIR, 'dummy2.mp4')
def test_fluent_equality():
@ -77,26 +79,33 @@ def test_get_args_simple():
def _get_complex_filter_example():
in_file = ffmpeg.file_input(TEST_INPUT_FILE)
concatted = ffmpeg.concat(
ffmpeg.trim(in_file, 10, 20),
ffmpeg.trim(in_file, 30, 40),
ffmpeg.trim(in_file, 50, 60),
)
out = ffmpeg.file_output(concatted, TEST_OUTPUT_FILE)
return ffmpeg.overwrite_output(out)
overlay_file = ffmpeg.file_input(TEST_OVERLAY_FILE)
return ffmpeg \
.concat(
in_file.trim(10, 20),
in_file.trim(30, 40),
) \
.overlay(overlay_file.hflip()) \
.drawbox(50, 50, 120, 120, color='red', thickness=5) \
.file_output(TEST_OUTPUT_FILE) \
.overwrite_output()
def test_get_args_complex_filter():
out = _get_complex_filter_example()
assert ffmpeg.get_args(out) == [
args = ffmpeg.get_args(out)
assert args == [
'-i', TEST_INPUT_FILE,
'-filter_complex',
'-i', TEST_OVERLAY_FILE,
'-filter_complex',
'[0]trim=start_frame=10:end_frame=20,setpts=PTS-STARTPTS[v0];' \
'[0]trim=start_frame=30:end_frame=40,setpts=PTS-STARTPTS[v1];' \
'[0]trim=start_frame=50:end_frame=60,setpts=PTS-STARTPTS[v2];' \
'[v0][v1][v2]concat=n=3[v3]',
'-map', '[v3]', TEST_OUTPUT_FILE,
'-y',
'[v0][v1]concat=n=2[v2];' \
'[1]hflip[v3];' \
'[v2][v3]overlay=eof_action=repeat[v4];' \
'[v4]drawbox=50:50:120:120:red:t=5[v5]',
'-map', '[v5]', '/Users/karlk/src/ffmpeg_wrapper/ffmpeg/tests/sample_data/dummy2.mp4',
'-y'
]

View File

@ -1,3 +1,2 @@
[pytest]
testpaths = ffmpeg/tests
#norecursedirs = venv .git

View File

@ -2,7 +2,7 @@ from distutils.core import setup
setup(
name = 'ffmpeg-python',
packages = ['ffmpeg'],
version = '0.1',
version = '0.1.1',
description = 'FFmpeg Python wrapper with support for complex filtering',
author = 'Karl Kroening',
author_email = 'karlk@kralnet.us',