From 63d660f1297ea0eec6b822f6f6552515f508cd24 Mon Sep 17 00:00:00 2001 From: Davide Depau <davide@depau.eu> Date: Thu, 25 Jan 2018 12:25:20 +0100 Subject: [PATCH 1/6] Implement SourceNode --- ffmpeg/_ffmpeg.py | 35 ++++++++++++++++++++++++++++++-- ffmpeg/_run.py | 7 ++++--- ffmpeg/nodes.py | 51 +++++++++++++++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/ffmpeg/_ffmpeg.py b/ffmpeg/_ffmpeg.py index 31e2b90..a7b038d 100644 --- a/ffmpeg/_ffmpeg.py +++ b/ffmpeg/_ffmpeg.py @@ -10,7 +10,7 @@ from .nodes import ( MergeOutputsNode, OutputNode, output_operator, -) + SourceNode) def input(filename, **kwargs): @@ -32,6 +32,30 @@ def input(filename, **kwargs): return InputNode(input.__name__, kwargs=kwargs).stream() + +def source_multi_output(filter_name, *args, **kwargs): + """Apply custom filter with one or more outputs. + + This is the same as ``filter_`` except that the filter can produce more than one output. + + To reference an output stream, use either the ``.stream`` operator or bracket shorthand: + + Example: + + ``` + split = ffmpeg.input('in.mp4').filter_multi_output('split') + split0 = split.stream(0) + split1 = split[1] + ffmpeg.concat(split0, split1).output('out.mp4').run() + ``` + """ + return SourceNode(filter_name, args=args, kwargs=kwargs) + + +def source(filter_name, *args, **kwargs): + return source_multi_output(filter_name, *args, **kwargs).stream() + + @output_operator() def global_args(stream, *args): """Add extra global command-line argument(s), e.g. ``-progress``. @@ -94,4 +118,11 @@ def output(*streams_and_filename, **kwargs): return OutputNode(streams, output.__name__, kwargs=kwargs).stream() -__all__ = ['input', 'merge_outputs', 'output', 'overwrite_output'] +__all__ = [ + 'input', + 'source_multi_output', + 'source', + 'merge_outputs', + 'output', + 'overwrite_output', +] diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index c9cbb7c..10e203f 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -16,7 +16,7 @@ from .nodes import ( InputNode, OutputNode, output_operator, -) + SourceNode) class Error(Exception): @@ -156,10 +156,11 @@ def get_args(stream_spec, overwrite_output=False): input_nodes = [node for node in sorted_nodes if isinstance(node, InputNode)] output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)] global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] - filter_nodes = [node for node in sorted_nodes if isinstance(node, FilterNode)] + filter_nodes = [node for node in sorted_nodes if isinstance(node, (FilterNode, SourceNode))] stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)} filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map) - args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) + if len(input_nodes) > 0: + args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) if filter_arg: args += ['-filter_complex', filter_arg] args += reduce( diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py index cacab8e..aa86be9 100644 --- a/ffmpeg/nodes.py +++ b/ffmpeg/nodes.py @@ -234,9 +234,7 @@ class Node(KwargReprNode): class FilterableStream(Stream): def __init__(self, upstream_node, upstream_label, upstream_selector=None): - super(FilterableStream, self).__init__( - upstream_node, upstream_label, {InputNode, FilterNode}, upstream_selector - ) + super(FilterableStream, self).__init__(upstream_node, upstream_label, {InputNode, FilterNode, SourceNode}, upstream_selector) # noinspection PyMethodOverriding @@ -261,20 +259,8 @@ class InputNode(Node): # noinspection PyMethodOverriding -class FilterNode(Node): - def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): - super(FilterNode, self).__init__( - stream_spec=stream_spec, - name=name, - incoming_stream_types={FilterableStream}, - outgoing_stream_type=FilterableStream, - min_inputs=1, - max_inputs=max_inputs, - args=args, - kwargs=kwargs, - ) - - """FilterNode""" +class FilterableNode(Node): + """FilterableNode""" def _get_filter(self, outgoing_edges): args = self.args @@ -300,6 +286,37 @@ class FilterNode(Node): return escape_chars(params_text, '\\\'[],;') +# noinspection PyMethodOverriding +class FilterNode(FilterableNode): + """FilterNode""" + def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): + super(FilterNode, self).__init__( + stream_spec=stream_spec, + name=name, + incoming_stream_types={FilterableStream}, + outgoing_stream_type=FilterableStream, + min_inputs=1, + max_inputs=max_inputs, + args=args, + kwargs=kwargs, + ) + + +# noinspection PyMethodOverriding +class SourceNode(FilterableNode): + def __init__(self, name, args=[], kwargs={}): + super(SourceNode, self).__init__( + stream_spec=None, + name=name, + incoming_stream_types={}, + outgoing_stream_type=FilterableStream, + min_inputs=0, + max_inputs=0, + args=args, + kwargs=kwargs + ) + + # noinspection PyMethodOverriding class OutputNode(Node): def __init__(self, stream, name, args=[], kwargs={}): From ac20491324cbf8fe56e7a5469bf9f96355b333ad Mon Sep 17 00:00:00 2001 From: Davide Depau <davide@depau.eu> Date: Thu, 25 Jan 2018 12:25:42 +0100 Subject: [PATCH 2/6] Add tests for SourceNode --- ffmpeg/tests/test_ffmpeg.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 51ee258..03c9ce7 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -650,6 +650,29 @@ def test_mixed_passthrough_selectors(): ] +def test_sources(): + out = (ffmpeg + .overlay( + ffmpeg.source("testsrc"), + ffmpeg.source("color", color="red@.3"), + ) + .trim(end=5) + .output(TEST_OUTPUT_FILE1) + ) + + assert out.get_args() == [ + '-filter_complex', + 'testsrc[s0];' + 'color=color=red@.3[s1];' + '[s0][s1]overlay=eof_action=repeat[s2];' + '[s2]trim=end=5[s3]', + '-map', + '[s3]', + TEST_OUTPUT_FILE1 + ] + + + def test_pipe(): width = 32 height = 32 From 6a8245e0c7ebf9d6a0d8d0c309893d6033906882 Mon Sep 17 00:00:00 2001 From: Davide Depau <davide@depau.eu> Date: Thu, 25 Jan 2018 12:39:14 +0100 Subject: [PATCH 3/6] Fix docstrings for SourceNode --- ffmpeg/_ffmpeg.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/ffmpeg/_ffmpeg.py b/ffmpeg/_ffmpeg.py index a7b038d..d869595 100644 --- a/ffmpeg/_ffmpeg.py +++ b/ffmpeg/_ffmpeg.py @@ -34,25 +34,22 @@ def input(filename, **kwargs): def source_multi_output(filter_name, *args, **kwargs): - """Apply custom filter with one or more outputs. + """Source filter with one or more outputs. - This is the same as ``filter_`` except that the filter can produce more than one output. + This is the same as ``source`` except that the filter can produce more than one output. - To reference an output stream, use either the ``.stream`` operator or bracket shorthand: - - Example: - - ``` - split = ffmpeg.input('in.mp4').filter_multi_output('split') - split0 = split.stream(0) - split1 = split[1] - ffmpeg.concat(split0, split1).output('out.mp4').run() - ``` + To reference an output stream, use either the ``.stream`` operator or bracket shorthand. """ return SourceNode(filter_name, args=args, kwargs=kwargs) def source(filter_name, *args, **kwargs): + """Source filter. + + It works like `input`, but takes a source filter name instead of a file URL as the first argument. + + Official documentation: `Sources <https://ffmpeg.org/ffmpeg-filters.html#Video-Sources>`__ + """ return source_multi_output(filter_name, *args, **kwargs).stream() From df18dacda2ced03ae8863e1e256c0ddf242c9d6f Mon Sep 17 00:00:00 2001 From: Silvio Tomatis <silviot@gmail.com> Date: Fri, 11 Sep 2020 23:57:56 +0200 Subject: [PATCH 4/6] Use id as hash value for source nodes to make sure they're not singletons --- ffmpeg/nodes.py | 9 +++++++++ ffmpeg/tests/test_ffmpeg.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py index aa86be9..fae16ab 100644 --- a/ffmpeg/nodes.py +++ b/ffmpeg/nodes.py @@ -5,6 +5,7 @@ from .dag import KwargReprNode from ._utils import escape_chars, get_hash_int from builtins import object import os +import uuid def _is_of_types(obj, types): @@ -305,6 +306,7 @@ class FilterNode(FilterableNode): # noinspection PyMethodOverriding class SourceNode(FilterableNode): def __init__(self, name, args=[], kwargs={}): + self.source_node_id = uuid.uuid4() super(SourceNode, self).__init__( stream_spec=None, name=name, @@ -316,6 +318,13 @@ class SourceNode(FilterableNode): kwargs=kwargs ) + def __hash__(self): + """Two source nodes with the same options should _not_ be considered + the same node. For this reason we create a uuid4 on node instantiation, + and use that as our hash. + """ + return self.source_node_id.int + # noinspection PyMethodOverriding class OutputNode(Node): diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 03c9ce7..b0f7ee9 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -673,6 +673,27 @@ def test_sources(): +def test_same_source_multiple_times(): + out = (ffmpeg + .concat( + ffmpeg.source("testsrc").trim(end=5), + ffmpeg.source("testsrc").trim(start=10, end=14).filter( + "setpts", "PTS-STARTPTS" + ), + ) + .output(TEST_OUTPUT_FILE1) + ) + + assert out.get_args() == [ + '-filter_complex', + 'testsrc[s0];[s0]trim=end=5[s1];testsrc[s2];[s2]trim=end=14:start=10[s3];[s3]setpts=PTS-STARTPTS[s4];[s1][s4]concat=n=2[s5]', + '-map', + '[s5]', + TEST_OUTPUT_FILE1 + ] + + + def test_pipe(): width = 32 height = 32 From 0a37ab83f89ee80d380d22f9c090222d340d3527 Mon Sep 17 00:00:00 2001 From: Silvio Tomatis <silviot@gmail.com> Date: Wed, 16 Sep 2020 12:47:19 +0200 Subject: [PATCH 5/6] Remove unnecessary if statement As requested here: https://github.com/kkroening/ffmpeg-python/pull/62#discussion_r164289616 --- ffmpeg/_run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index 10e203f..e5b22c6 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -159,8 +159,7 @@ def get_args(stream_spec, overwrite_output=False): filter_nodes = [node for node in sorted_nodes if isinstance(node, (FilterNode, SourceNode))] stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)} filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map) - if len(input_nodes) > 0: - args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) + args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) if filter_arg: args += ['-filter_complex', filter_arg] args += reduce( From 61e533abd90c88522bfa8a0e1ee452c1e1cb81d4 Mon Sep 17 00:00:00 2001 From: Silvio Tomatis <silviot@gmail.com> Date: Tue, 22 Sep 2020 15:41:26 +0200 Subject: [PATCH 6/6] Fix tests --- ffmpeg/_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index e5b22c6..a3c7987 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -159,7 +159,7 @@ def get_args(stream_spec, overwrite_output=False): filter_nodes = [node for node in sorted_nodes if isinstance(node, (FilterNode, SourceNode))] stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)} filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map) - args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) + args += reduce(operator.add, [_get_input_args(node) for node in input_nodes], []) if filter_arg: args += ['-filter_complex', filter_arg] args += reduce(