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(