mirror of
https://github.com/kkroening/ffmpeg-python.git
synced 2025-04-02 10:02:48 +08:00
Add overlay, hflip, and drawbox operators; use a more real-world example in docs
This commit is contained in:
parent
1f7736ddb6
commit
f3b32d6d41
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
.cache
|
||||
dist/
|
||||
ffmpeg/tests/dummy2.mp4
|
||||
ffmpeg/tests/sample_data/dummy2.mp4
|
||||
venv
|
||||
|
23
README
23
README
@ -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()
|
||||
```
|
||||
|
||||
|
BIN
doc/graph1.png
BIN
doc/graph1.png
Binary file not shown.
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 51 KiB |
@ -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)
|
||||
|
BIN
ffmpeg/tests/sample_data/overlay.png
Normal file
BIN
ffmpeg/tests/sample_data/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
@ -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'
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,3 +1,2 @@
|
||||
[pytest]
|
||||
testpaths = ffmpeg/tests
|
||||
#norecursedirs = venv .git
|
||||
|
Loading…
x
Reference in New Issue
Block a user