From faca0ee87b01742d08b30469451ccef4bfaad065 Mon Sep 17 00:00:00 2001 From: Michal Trybus Date: Mon, 1 Jul 2019 15:39:55 +0200 Subject: [PATCH 01/32] Test against wrong order of outputs from multiple-output filters --- ffmpeg/tests/test_ffmpeg.py | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 97638121..b40d624a 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -716,3 +716,51 @@ def test__probe__exception(): def test__probe__extra_args(): data = ffmpeg.probe(TEST_INPUT_FILE1, show_frames=None) assert set(data.keys()) == {'format', 'streams', 'frames'} + +def get_filter_complex_input(flt, name): + m = re.search(r'\[([^]]+)\]{}(?=[[;]|$)'.format(name), flt) + if m: + return m.group(1) + else: + return None + +def get_filter_complex_outputs(flt, name): + m = re.search(r'(^|[];]){}((\[[^]]+\])+)(?=;|$)'.format(name), flt) + if m: + return m.group(2)[1:-1].split('][') + else: + return None + +def test__get_filter_complex_input(): + assert get_filter_complex_input("", "scale") is None + assert get_filter_complex_input("scale", "scale") is None + assert get_filter_complex_input("scale[s3][s4];etc", "scale") is None + assert get_filter_complex_input("[s2]scale", "scale") == "s2" + assert get_filter_complex_input("[s2]scale;etc", "scale") == "s2" + assert get_filter_complex_input("[s2]scale[s3][s4];etc", "scale") == "s2" + +def test__get_filter_complex_outputs(): + assert get_filter_complex_outputs("", "scale") is None + assert get_filter_complex_outputs("scale", "scale") is None + assert get_filter_complex_outputs("scalex[s0][s1]", "scale") is None + assert get_filter_complex_outputs("scale[s0][s1]", "scale") == ['s0', 's1'] + assert get_filter_complex_outputs("[s5]scale[s0][s1]", "scale") == ['s0', 's1'] + assert get_filter_complex_outputs("[s5]scale[s1][s0]", "scale") == ['s1', 's0'] + assert get_filter_complex_outputs("[s5]scale[s1]", "scale") == ['s1'] + assert get_filter_complex_outputs("[s5]scale[s1];x", "scale") == ['s1'] + assert get_filter_complex_outputs("y;[s5]scale[s1];x", "scale") == ['s1'] + +def test__multi_output_edge_label_order(): + scale2ref = ffmpeg.filter_multi_output([ffmpeg.input('x'), ffmpeg.input('y')], 'scale2ref') + out = ( + ffmpeg.merge_outputs( + scale2ref[1].filter('scale').output('a'), + scale2ref[10000].filter('hflip').output('b') + ) + ) + + args = out.get_args() + flt_cmpl = args[args.index('-filter_complex')+1] + out1, out2 = get_filter_complex_outputs(flt_cmpl, 'scale2ref') + assert out1 == get_filter_complex_input(flt_cmpl, 'scale') + assert out2 == get_filter_complex_input(flt_cmpl, 'hflip') From 732bf213974a7e433f97fac6aacf48e70c302452 Mon Sep 17 00:00:00 2001 From: Michal Trybus Date: Mon, 1 Jul 2019 15:55:10 +0200 Subject: [PATCH 02/32] Label-based order of outputs from multiple-output filters --- ffmpeg/_run.py | 2 +- ffmpeg/dag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index 3167634d..afc504dc 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -84,7 +84,7 @@ def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_ stream_count = 0 for upstream_node in filter_nodes: outgoing_edge_map = outgoing_edge_maps[upstream_node] - for upstream_label, downstreams in list(outgoing_edge_map.items()): + for upstream_label, downstreams in sorted(outgoing_edge_map.items()): if len(downstreams) > 1: # TODO: automatically insert `splits` ahead of time via graph transformation. raise ValueError( diff --git a/ffmpeg/dag.py b/ffmpeg/dag.py index bb398238..9564d7f8 100644 --- a/ffmpeg/dag.py +++ b/ffmpeg/dag.py @@ -100,7 +100,7 @@ def get_incoming_edges(downstream_node, incoming_edge_map): def get_outgoing_edges(upstream_node, outgoing_edge_map): edges = [] - for upstream_label, downstream_infos in list(outgoing_edge_map.items()): + for upstream_label, downstream_infos in sorted(outgoing_edge_map.items()): for downstream_info in downstream_infos: downstream_node, downstream_label, downstream_selector = downstream_info edges += [ From 1c9695d2a0b079e5d1e9d6fb3ecbeaf248b365a9 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Fri, 5 Jul 2019 19:16:43 -0500 Subject: [PATCH 03/32] Run Black code formatter --- ffmpeg/tests/test_ffmpeg.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index b40d624a..279a323e 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -717,6 +717,7 @@ def test__probe__extra_args(): data = ffmpeg.probe(TEST_INPUT_FILE1, show_frames=None) assert set(data.keys()) == {'format', 'streams', 'frames'} + def get_filter_complex_input(flt, name): m = re.search(r'\[([^]]+)\]{}(?=[[;]|$)'.format(name), flt) if m: @@ -724,6 +725,7 @@ def get_filter_complex_input(flt, name): else: return None + def get_filter_complex_outputs(flt, name): m = re.search(r'(^|[];]){}((\[[^]]+\])+)(?=;|$)'.format(name), flt) if m: @@ -731,6 +733,7 @@ def get_filter_complex_outputs(flt, name): else: return None + def test__get_filter_complex_input(): assert get_filter_complex_input("", "scale") is None assert get_filter_complex_input("scale", "scale") is None @@ -739,6 +742,7 @@ def test__get_filter_complex_input(): assert get_filter_complex_input("[s2]scale;etc", "scale") == "s2" assert get_filter_complex_input("[s2]scale[s3][s4];etc", "scale") == "s2" + def test__get_filter_complex_outputs(): assert get_filter_complex_outputs("", "scale") is None assert get_filter_complex_outputs("scale", "scale") is None @@ -750,17 +754,18 @@ def test__get_filter_complex_outputs(): assert get_filter_complex_outputs("[s5]scale[s1];x", "scale") == ['s1'] assert get_filter_complex_outputs("y;[s5]scale[s1];x", "scale") == ['s1'] + def test__multi_output_edge_label_order(): - scale2ref = ffmpeg.filter_multi_output([ffmpeg.input('x'), ffmpeg.input('y')], 'scale2ref') - out = ( - ffmpeg.merge_outputs( - scale2ref[1].filter('scale').output('a'), - scale2ref[10000].filter('hflip').output('b') - ) + scale2ref = ffmpeg.filter_multi_output( + [ffmpeg.input('x'), ffmpeg.input('y')], 'scale2ref' + ) + out = ffmpeg.merge_outputs( + scale2ref[1].filter('scale').output('a'), + scale2ref[10000].filter('hflip').output('b'), ) args = out.get_args() - flt_cmpl = args[args.index('-filter_complex')+1] + flt_cmpl = args[args.index('-filter_complex') + 1] out1, out2 = get_filter_complex_outputs(flt_cmpl, 'scale2ref') assert out1 == get_filter_complex_input(flt_cmpl, 'scale') assert out2 == get_filter_complex_input(flt_cmpl, 'hflip') From 78fb6cf2f11cb93c6071c978a92a640f5743a9fb Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Fri, 5 Jul 2019 19:17:30 -0500 Subject: [PATCH 04/32] Release 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9d0057be..0282c67e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup from textwrap import dedent -version = '0.1.18' +version = '0.2.0' download_url = 'https://github.com/kkroening/ffmpeg-python/archive/v{}.zip'.format( version ) From c6c2dfdc2882bcf166ed35e8517aad25a6c2bfe8 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 31 Jul 2019 06:30:22 +0200 Subject: [PATCH 05/32] Travis CI: Add Python 3.7 to the tests Python 3.4 is end of life... Should we drop support for it? --- .travis.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index c58274b8..e4d7d758 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,20 +9,18 @@ before_install: matrix: include: - python: 2.7 - env: - - TOX_ENV=py27 + env: TOX_ENV=py27 - python: 3.4 - env: - - TOX_ENV=py34 + env: TOX_ENV=py34 - python: 3.5 - env: - - TOX_ENV=py35 + env: TOX_ENV=py35 - python: 3.6 - env: - - TOX_ENV=py36 + env: TOX_ENV=py36 + - python: 3.7 + dist: xenial # required for Python >= 3.7 + env: TOX_ENV=py37 - python: pypy - env: - - TOX_ENV=pypy + env: TOX_ENV=pypy install: - pip install tox script: From ed9b7f8804fb07f5bf4e7f6d7373f0107d404c13 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 31 Jul 2019 06:32:29 +0200 Subject: [PATCH 06/32] tox.ini: Add py37 to the testing Python 3.4 is end of life... Should we drop support for it? --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f86ec4bb..1e3ba533 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36, pypy +envlist = py27, py34, py35, py36, py37, pypy [testenv] commands = py.test -vv From bbd56a35a3fc1c23e0e7c181c5a3d6f9f0db3397 Mon Sep 17 00:00:00 2001 From: Kyle McDonald Date: Sun, 4 Aug 2019 15:09:40 -0700 Subject: [PATCH 07/32] tensorflow streaming typo --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 4ec816e2..f30c59fe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -161,7 +161,7 @@ while True: out_frame = deep_dream.process_frame(in_frame) process2.stdin.write( - in_frame + out_frame .astype(np.uint8) .tobytes() ) From c99e97b6871105a886026027b18e8e3906aaaee4 Mon Sep 17 00:00:00 2001 From: Kyle McDonald Date: Sun, 4 Aug 2019 15:18:55 -0700 Subject: [PATCH 08/32] added mono to stereo example --- examples/README.md | 30 +++++++++++++++++++++++++++++ examples/graphs/mono-to-stereo.png | Bin 0 -> 17191 bytes 2 files changed, 30 insertions(+) create mode 100644 examples/graphs/mono-to-stereo.png diff --git a/examples/README.md b/examples/README.md index 4ec816e2..d9981f5d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -115,6 +115,36 @@ out = ffmpeg.output(v3, a3, 'out.mp4') out.run() ``` +## Mono to stereo with offsets and video + +mono-to-stereo graph + +```python +audio_left = ( + ffmpeg + .input('audio-left.wav') + .filter('atrim', start=5) + .filter('asetpts', 'PTS-STARTPTS') +) + +audio_right = ( + ffmpeg + .input('audio-right.wav') + .filter('atrim', start=10) + .filter('asetpts', 'PTS-STARTPTS') +) + +input_video = ffmpeg.input('input-video.mp4') + +( + ffmpeg + .filter((audio_left, audio_right), 'join', inputs=2, channel_layout='stereo') + .output(input_video.video, 'output-video.mp4', shortest=None, vcodec='copy') + .overwrite_output() + .run() +) +``` + ## [Jupyter Frame Viewer](https://github.com/kkroening/ffmpeg-python/blob/master/examples/ffmpeg-numpy.ipynb) jupyter screenshot diff --git a/examples/graphs/mono-to-stereo.png b/examples/graphs/mono-to-stereo.png new file mode 100644 index 0000000000000000000000000000000000000000..92ce0e5b47a26c88dfbc88baf84bc764b4c3ddd1 GIT binary patch literal 17191 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 Aq5uE@ literal 0 HcmV?d00001 From 82a00e48492f2d787c980c434d58e249c210818e Mon Sep 17 00:00:00 2001 From: magnusvmt Date: Mon, 28 Oct 2019 16:28:51 +0100 Subject: [PATCH 09/32] Add optional timeout argument to probe Popen.communicate() supports a timeout argument which is useful in case there is a risk that the probe hangs. --- ffmpeg/_probe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffmpeg/_probe.py b/ffmpeg/_probe.py index 41e81680..3b8e3a64 100644 --- a/ffmpeg/_probe.py +++ b/ffmpeg/_probe.py @@ -4,7 +4,7 @@ from ._utils import convert_kwargs_to_cmd_line_args -def probe(filename, cmd='ffprobe', **kwargs): +def probe(filename, cmd='ffprobe', timeout=None, **kwargs): """Run ffprobe on the specified file and return a JSON representation of the output. Raises: @@ -18,7 +18,7 @@ def probe(filename, cmd='ffprobe', **kwargs): args += [filename] p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() + out, err = p.communicate(timeout=timeout) if p.returncode != 0: raise Error('ffprobe', out, err) return json.loads(out.decode('utf-8')) From 2a7a2d78c97125d5d55faf35a319ccdf080646a0 Mon Sep 17 00:00:00 2001 From: Christophe Mehay Date: Thu, 31 Oct 2019 12:02:09 +0100 Subject: [PATCH 10/32] Duplicate parameters can be set in kwargs with an iterator For instance, add multiple `streamid` in the output can be done like this: ffmpeg.input(url).output('video.mp4', streamid=['0:0x101', '1:0x102']) will output this command line ffmpeg -i video.mp4 -streamid 0:0x101 -streamid 1:0x102 video.mp4 --- ffmpeg/_utils.py | 8 ++++++++ ffmpeg/tests/test_ffmpeg.py | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/ffmpeg/_utils.py b/ffmpeg/_utils.py index d41f2fd7..92d76110 100644 --- a/ffmpeg/_utils.py +++ b/ffmpeg/_utils.py @@ -3,6 +3,8 @@ from past.builtins import basestring import hashlib import sys +import collections + if sys.version_info.major == 2: # noinspection PyUnresolvedReferences,PyShadowingBuiltins @@ -91,6 +93,12 @@ def convert_kwargs_to_cmd_line_args(kwargs): args = [] for k in sorted(kwargs.keys()): v = kwargs[k] + if isinstance(v, collections.Iterable) and not isinstance(v, str): + for value in v: + args.append('-{}'.format(k)) + if value is not None: + args.append('{}'.format(value)) + continue args.append('-{}'.format(k)) if v is not None: args.append('{}'.format(v)) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 279a323e..0d600f21 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -9,6 +9,7 @@ import re import subprocess + try: import mock # python 2 except ImportError: @@ -114,6 +115,10 @@ def test_stream_repr(): dummy_out.label, dummy_out.node.short_hash ) +def test_repeated_args(): + out_file = ffmpeg.input('dummy.mp4').output('dummy2.mp4', streamid=['0:0x101', '1:0x102']) + assert out_file.get_args() == ['-i', 'dummy.mp4', '-streamid', '0:0x101', '-streamid', '1:0x102', 'dummy2.mp4'] + def test__get_args__simple(): out_file = ffmpeg.input('dummy.mp4').output('dummy2.mp4') From a8e0954f411a8467d2108ca36cab56d1bc3e0a1e Mon Sep 17 00:00:00 2001 From: Harshna Patel Date: Thu, 31 Oct 2019 20:07:38 +0000 Subject: [PATCH 11/32] Fixed typographical error --- ffmpeg/_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index afc504dc..c9cbb7ce 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -175,7 +175,7 @@ def get_args(stream_spec, overwrite_output=False): def compile(stream_spec, cmd='ffmpeg', overwrite_output=False): """Build command-line for invoking ffmpeg. - The :meth:`run` function uses this to build the commnad line + The :meth:`run` function uses this to build the command line arguments and should work in most cases, but calling this function directly is useful for debugging or if you need to invoke ffmpeg manually for whatever reason. From 2d3a078f24309f8d189ea196b54011632e3afb20 Mon Sep 17 00:00:00 2001 From: magnusvmt Date: Sat, 2 Nov 2019 16:17:17 +0100 Subject: [PATCH 12/32] Add test for probe timeout and fix for Python2 Fix for Python2 so that timeout is only used as keyword argument if it is provided Added a test for the new timeout argument that will run for Python > 3.3. --- ffmpeg/_probe.py | 5 ++++- ffmpeg/tests/test_ffmpeg.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ffmpeg/_probe.py b/ffmpeg/_probe.py index 3b8e3a64..090d7abf 100644 --- a/ffmpeg/_probe.py +++ b/ffmpeg/_probe.py @@ -18,7 +18,10 @@ def probe(filename, cmd='ffprobe', timeout=None, **kwargs): args += [filename] p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate(timeout=timeout) + communicate_kwargs = {} + if timeout is not None: + communicate_kwargs['timeout'] = timeout + out, err = p.communicate(**communicate_kwargs) if p.returncode != 0: raise Error('ffprobe', out, err) return json.loads(out.decode('utf-8')) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 279a323e..a5a1a2d3 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -8,6 +8,7 @@ import random import re import subprocess +import sys try: import mock # python 2 @@ -706,6 +707,13 @@ def test__probe(): assert data['format']['duration'] == '7.036000' +@pytest.mark.skipif(sys.version_info < (3, 3), reason="requires python3.3 or higher") +def test__probe_timeout(): + with pytest.raises(subprocess.TimeoutExpired) as excinfo: + data = ffmpeg.probe(TEST_INPUT_FILE1, timeout=0) + assert 'timed out after 0 seconds' in str(excinfo.value) + + def test__probe__exception(): with pytest.raises(ffmpeg.Error) as excinfo: ffmpeg.probe(BOGUS_INPUT_FILE) From c12e8890ad9314dd29162c2bb51e876972d60e65 Mon Sep 17 00:00:00 2001 From: Tercio Gaudencio Filho Date: Tue, 15 Sep 2020 22:08:51 -0300 Subject: [PATCH 13/32] Fix issue #195. Redirect stdout/stderr to DEVNULL when quiet is requested. --- ffmpeg/_run.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index c9cbb7ce..559a0f41 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -279,8 +279,10 @@ def run_async( """ args = compile(stream_spec, cmd, overwrite_output=overwrite_output) stdin_stream = subprocess.PIPE if pipe_stdin else None - stdout_stream = subprocess.PIPE if pipe_stdout or quiet else None - stderr_stream = subprocess.PIPE if pipe_stderr or quiet else None + stdout_stream = subprocess.PIPE if pipe_stdout else None + stderr_stream = subprocess.PIPE if pipe_stderr else None + if quiet: + stdout_stream = stderr_stream = subprocess.DEVNULL return subprocess.Popen( args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream ) From 96fb3ff0506821f006ce9d110aa0d92356d41909 Mon Sep 17 00:00:00 2001 From: Jacotsu Date: Wed, 7 Oct 2020 18:52:15 +0200 Subject: [PATCH 14/32] Added parameter to set ffmpeg's working directory --- ffmpeg/_run.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index c9cbb7ce..bebf178f 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -199,6 +199,7 @@ def run_async( pipe_stderr=False, quiet=False, overwrite_output=False, + cwd=None ): """Asynchronously invoke ffmpeg for the supplied node graph. @@ -282,7 +283,8 @@ def run_async( stdout_stream = subprocess.PIPE if pipe_stdout or quiet else None stderr_stream = subprocess.PIPE if pipe_stderr or quiet else None return subprocess.Popen( - args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream + args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream, + cwd=cwd ) @@ -295,6 +297,7 @@ def run( input=None, quiet=False, overwrite_output=False, + cwd=None ): """Invoke ffmpeg for the supplied node graph. @@ -318,6 +321,7 @@ def run( pipe_stderr=capture_stderr, quiet=quiet, overwrite_output=overwrite_output, + cwd ) out, err = process.communicate(input) retcode = process.poll() From b64f40a8b56d8ccd822f18be2be6adaf1dcf4db8 Mon Sep 17 00:00:00 2001 From: Jacotsu Date: Wed, 7 Oct 2020 18:56:05 +0200 Subject: [PATCH 15/32] Fixed typo in cwd parameter usage --- ffmpeg/_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index bebf178f..c9128858 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -321,7 +321,7 @@ def run( pipe_stderr=capture_stderr, quiet=quiet, overwrite_output=overwrite_output, - cwd + cwd=cwd ) out, err = process.communicate(input) retcode = process.poll() From 17995f5ff3ab77f3b6736783c4b180283ec0fc15 Mon Sep 17 00:00:00 2001 From: Jacotsu Date: Fri, 9 Oct 2020 16:53:17 +0200 Subject: [PATCH 16/32] Updated test to check the new cwd parameter --- ffmpeg/tests/test_ffmpeg.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 51ee2587..4a8183ca 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -441,12 +441,14 @@ def test__compile(): @pytest.mark.parametrize('pipe_stdin', [True, False]) @pytest.mark.parametrize('pipe_stdout', [True, False]) @pytest.mark.parametrize('pipe_stderr', [True, False]) -def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr): +@pytest.mark.parametrize('cwd', [None, '/tmp']) +def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr, cwd): process__mock = mock.Mock() popen__mock = mocker.patch.object(subprocess, 'Popen', return_value=process__mock) stream = _get_simple_example() process = ffmpeg.run_async( - stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, pipe_stderr=pipe_stderr + stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, + pipe_stderr=pipe_stderr, cwd=cwd ) assert process is process__mock @@ -456,7 +458,8 @@ def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr): (args,), kwargs = popen__mock.call_args assert args == ffmpeg.compile(stream) assert kwargs == dict( - stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr + stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr, + cwd=cwd ) From 0ec6e69d885f9f75a3220f94b79273f288005049 Mon Sep 17 00:00:00 2001 From: Tercio Gaudencio Filho Date: Fri, 30 Oct 2020 17:29:44 -0300 Subject: [PATCH 17/32] Redirect stderr to stdout and stdout to DEVNULL when quiet is requested. --- ffmpeg/_run.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index 559a0f41..c3d5550b 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -282,7 +282,8 @@ def run_async( stdout_stream = subprocess.PIPE if pipe_stdout else None stderr_stream = subprocess.PIPE if pipe_stderr else None if quiet: - stdout_stream = stderr_stream = subprocess.DEVNULL + stderr_stream = subprocess.STDOUT + stdout_stream = subprocess.DEVNULL return subprocess.Popen( args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream ) From 15ffcc0c7275443e2c4f13e3e4a9ebd6dd5503dc Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 30 Oct 2020 18:41:59 -0300 Subject: [PATCH 18/32] adding http server example --- examples/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/examples/README.md b/examples/README.md index 190b8979..68fd8c03 100644 --- a/examples/README.md +++ b/examples/README.md @@ -214,6 +214,32 @@ process2.wait() ) ``` +## Stream from a local video to HTTP server + +```python +video_format = "flv" +server_url = "http://localhost:8080" + +process = ( + ffmpeg + .input("input.mp4") + .output( + server_url, + codec = "copy", # use same codecs of the original video + listen=1, # enables HTTP server + f=video_format) + .global_args("-re") # argument to act as a live stream + .run() +) + +``` + +to receive the video you can use ffplay in the terminal: + +``` +$ ffplay -f flv http://localhost:8080 +``` + ## Stream from RTSP server to TCP socket ```python From 08e50ac02c3e2188135ddcb27f98749cb2772a09 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei <6d0847b9@opayq.com> Date: Sun, 8 Nov 2020 12:51:35 +0200 Subject: [PATCH 19/32] Add command line arguments FAQ --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5989ae66..8cfe89ae 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,10 @@ This dilemma is intrinsic to ffmpeg, and ffmpeg-python tries to stay out of the As usual, take a look at the [examples](https://github.com/kkroening/ffmpeg-python/tree/master/examples#audiovideo-pipeline) (*Audio/video pipeline* in particular). +**How can I find out the used command line arguments?** + +You can run `stream.get_args()` before `stream.run()` to retrieve the command line arguments that will be passed to `ffmpeg`. You can also run `stream.compile()` that also includes the `ffmpeg` executable as the first argument. + **How do I do XYZ?** Take a look at each of the links in the [Additional Resources](https://kkroening.github.io/ffmpeg-python/) section at the end of this README. If you look everywhere and can't find what you're looking for and have a question that may be relevant to other users, you may open an issue asking how to do it, while providing a thorough explanation of what you're trying to do and what you've tried so far. From 5b6b58308fc81e8671e433f340f8648b081a2b5b Mon Sep 17 00:00:00 2001 From: raulpy271 <56479285+raulpy271@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:19:53 -0300 Subject: [PATCH 20/32] Replacing `server_url` content, `"http://localhost:8080"` to `"http://127.0.0.1:8080"`. --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 68fd8c03..dabc7398 100644 --- a/examples/README.md +++ b/examples/README.md @@ -218,7 +218,7 @@ process2.wait() ```python video_format = "flv" -server_url = "http://localhost:8080" +server_url = "http://127.0.0.1:8080" process = ( ffmpeg From 861b453b43675620c7a4ab30723a3c67ec6c8830 Mon Sep 17 00:00:00 2001 From: 372046933 <372046933@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:18:00 +0800 Subject: [PATCH 21/32] Fix typo in _run.py docstring --- ffmpeg/_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index c9cbb7ce..cc050834 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -233,7 +233,7 @@ def run_async( process = ( ffmpeg .input(in_filename) - .output('pipe':, format='rawvideo', pix_fmt='rgb24') + .output('pipe:', format='rawvideo', pix_fmt='rgb24') .run_async(pipe_stdout=True, pipe_stderr=True) ) out, err = process.communicate() From c764166f442a4b726c05a4d340f3e53cb11a497a Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Tue, 16 Feb 2021 22:54:27 +0100 Subject: [PATCH 22/32] Revert "Implemented cwd parameter" --- ffmpeg/_run.py | 6 +----- ffmpeg/tests/test_ffmpeg.py | 9 +++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index eadb90a4..43d6cf7d 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -199,7 +199,6 @@ def run_async( pipe_stderr=False, quiet=False, overwrite_output=False, - cwd=None ): """Asynchronously invoke ffmpeg for the supplied node graph. @@ -286,8 +285,7 @@ def run_async( stderr_stream = subprocess.STDOUT stdout_stream = subprocess.DEVNULL return subprocess.Popen( - args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream, - cwd=cwd + args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream ) @@ -300,7 +298,6 @@ def run( input=None, quiet=False, overwrite_output=False, - cwd=None ): """Invoke ffmpeg for the supplied node graph. @@ -324,7 +321,6 @@ def run( pipe_stderr=capture_stderr, quiet=quiet, overwrite_output=overwrite_output, - cwd=cwd ) out, err = process.communicate(input) retcode = process.poll() diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 4a8183ca..51ee2587 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -441,14 +441,12 @@ def test__compile(): @pytest.mark.parametrize('pipe_stdin', [True, False]) @pytest.mark.parametrize('pipe_stdout', [True, False]) @pytest.mark.parametrize('pipe_stderr', [True, False]) -@pytest.mark.parametrize('cwd', [None, '/tmp']) -def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr, cwd): +def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr): process__mock = mock.Mock() popen__mock = mocker.patch.object(subprocess, 'Popen', return_value=process__mock) stream = _get_simple_example() process = ffmpeg.run_async( - stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, - pipe_stderr=pipe_stderr, cwd=cwd + stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, pipe_stderr=pipe_stderr ) assert process is process__mock @@ -458,8 +456,7 @@ def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr, cwd): (args,), kwargs = popen__mock.call_args assert args == ffmpeg.compile(stream) assert kwargs == dict( - stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr, - cwd=cwd + stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr ) From 807aaccb14ac495608b51425b03e7e46227180e3 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Tue, 16 Feb 2021 23:26:53 +0100 Subject: [PATCH 23/32] Revert "Revert "Implemented cwd parameter"" --- ffmpeg/_run.py | 6 +++++- ffmpeg/tests/test_ffmpeg.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index 43d6cf7d..eadb90a4 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -199,6 +199,7 @@ def run_async( pipe_stderr=False, quiet=False, overwrite_output=False, + cwd=None ): """Asynchronously invoke ffmpeg for the supplied node graph. @@ -285,7 +286,8 @@ def run_async( stderr_stream = subprocess.STDOUT stdout_stream = subprocess.DEVNULL return subprocess.Popen( - args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream + args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream, + cwd=cwd ) @@ -298,6 +300,7 @@ def run( input=None, quiet=False, overwrite_output=False, + cwd=None ): """Invoke ffmpeg for the supplied node graph. @@ -321,6 +324,7 @@ def run( pipe_stderr=capture_stderr, quiet=quiet, overwrite_output=overwrite_output, + cwd=cwd ) out, err = process.communicate(input) retcode = process.poll() diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 51ee2587..4a8183ca 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -441,12 +441,14 @@ def test__compile(): @pytest.mark.parametrize('pipe_stdin', [True, False]) @pytest.mark.parametrize('pipe_stdout', [True, False]) @pytest.mark.parametrize('pipe_stderr', [True, False]) -def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr): +@pytest.mark.parametrize('cwd', [None, '/tmp']) +def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr, cwd): process__mock = mock.Mock() popen__mock = mocker.patch.object(subprocess, 'Popen', return_value=process__mock) stream = _get_simple_example() process = ffmpeg.run_async( - stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, pipe_stderr=pipe_stderr + stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, + pipe_stderr=pipe_stderr, cwd=cwd ) assert process is process__mock @@ -456,7 +458,8 @@ def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr): (args,), kwargs = popen__mock.call_args assert args == ffmpeg.compile(stream) assert kwargs == dict( - stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr + stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr, + cwd=cwd ) From fd1da13f110ad4d20ab768bfafaeef36dce2862f Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Sun, 6 Mar 2022 13:24:40 -0800 Subject: [PATCH 24/32] Re-apply Black formatting, and wrap docstrings at ~88 columns. (#639) --- ffmpeg/_ffmpeg.py | 6 +- ffmpeg/_filters.py | 355 ++++++++++++++++++++---------------- ffmpeg/_run.py | 25 ++- ffmpeg/_utils.py | 4 +- ffmpeg/_view.py | 4 +- ffmpeg/dag.py | 47 +++-- ffmpeg/nodes.py | 11 +- ffmpeg/tests/test_ffmpeg.py | 96 ++++++---- pyproject.toml | 15 ++ 9 files changed, 340 insertions(+), 223 deletions(-) create mode 100644 pyproject.toml diff --git a/ffmpeg/_ffmpeg.py b/ffmpeg/_ffmpeg.py index 31e2b906..007624bb 100644 --- a/ffmpeg/_ffmpeg.py +++ b/ffmpeg/_ffmpeg.py @@ -34,8 +34,7 @@ def input(filename, **kwargs): @output_operator() def global_args(stream, *args): - """Add extra global command-line argument(s), e.g. ``-progress``. - """ + """Add extra global command-line argument(s), e.g. ``-progress``.""" return GlobalNode(stream, global_args.__name__, args).stream() @@ -50,8 +49,7 @@ def overwrite_output(stream): @output_operator() def merge_outputs(*streams): - """Include all given outputs in one ffmpeg command line - """ + """Include all given outputs in one ffmpeg command line""" return MergeOutputsNode(streams, merge_outputs.__name__).stream() diff --git a/ffmpeg/_filters.py b/ffmpeg/_filters.py index 2691220a..cb1438e2 100644 --- a/ffmpeg/_filters.py +++ b/ffmpeg/_filters.py @@ -8,9 +8,11 @@ def filter_multi_output(stream_spec, 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. + 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: + To reference an output stream, use either the ``.stream`` operator or bracket + shorthand: Example: @@ -30,9 +32,10 @@ def filter_multi_output(stream_spec, filter_name, *args, **kwargs): def filter(stream_spec, filter_name, *args, **kwargs): """Apply custom filter. - ``filter_`` is normally used by higher-level filter functions such as ``hflip``, but if a filter implementation - is missing from ``ffmpeg-python``, you can call ``filter_`` directly to have ``ffmpeg-python`` pass the filter name - and arguments to ffmpeg verbatim. + ``filter_`` is normally used by higher-level filter functions such as ``hflip``, + but if a filter implementation is missing from ``ffmpeg-python``, you can call + ``filter_`` directly to have ``ffmpeg-python`` pass the filter name and arguments + to ffmpeg verbatim. Args: stream_spec: a Stream, list of Streams, or label-to-Stream dictionary mapping @@ -40,7 +43,8 @@ def filter(stream_spec, filter_name, *args, **kwargs): *args: list of args to pass to ffmpeg verbatim **kwargs: list of keyword-args to pass to ffmpeg verbatim - The function name is suffixed with ``_`` in order avoid confusion with the standard python ``filter`` function. + The function name is suffixed with ``_`` in order avoid confusion with the standard + python ``filter`` function. Example: @@ -72,7 +76,8 @@ def setpts(stream, expr): """Change the PTS (presentation timestamp) of the input frames. Args: - expr: The expression which is evaluated for each frame to construct its timestamp. + expr: The expression which is evaluated for each frame to construct its + timestamp. Official documentation: `setpts, asetpts `__ """ @@ -84,14 +89,15 @@ def trim(stream, **kwargs): """Trim the input so that the output contains one continuous subpart of the input. Args: - start: Specify the time of the start of the kept section, i.e. the frame with the timestamp start will be the - first frame in the output. - end: Specify the time of the first frame that will be dropped, i.e. the frame immediately preceding the one - with the timestamp end will be the last frame in the output. - start_pts: This is the same as start, except this option sets the start timestamp in timebase units instead of - seconds. - end_pts: This is the same as end, except this option sets the end timestamp in timebase units instead of - seconds. + start: Specify the time of the start of the kept section, i.e. the frame with + the timestamp start will be the first frame in the output. + end: Specify the time of the first frame that will be dropped, i.e. the frame + immediately preceding the one with the timestamp end will be the last frame + in the output. + start_pts: This is the same as start, except this option sets the start + timestamp in timebase units instead of seconds. + end_pts: This is the same as end, except this option sets the end timestamp in + timebase units instead of seconds. duration: The maximum duration of the output in seconds. start_frame: The number of the first frame that should be passed to the output. end_frame: The number of the first frame that should be dropped. @@ -106,14 +112,16 @@ def overlay(main_parent_node, overlay_parent_node, eof_action='repeat', **kwargs """Overlay one video on top of another. Args: - x: Set the expression for the x coordinates of the overlaid video on the main video. Default value is 0. In - case the expression is invalid, it is set to a huge value (meaning that the overlay will not be displayed - within the output visible area). - y: Set the expression for the y coordinates of the overlaid video on the main video. Default value is 0. In - case the expression is invalid, it is set to a huge value (meaning that the overlay will not be displayed - within the output visible area). - eof_action: The action to take when EOF is encountered on the secondary input; it accepts one of the following - values: + x: Set the expression for the x coordinates of the overlaid video on the main + video. Default value is 0. In case the expression is invalid, it is set to + a huge value (meaning that the overlay will not be displayed within the + output visible area). + y: Set the expression for the y coordinates of the overlaid video on the main + video. Default value is 0. In case the expression is invalid, it is set to + a huge value (meaning that the overlay will not be displayed within the + output visible area). + eof_action: The action to take when EOF is encountered on the secondary input; + it accepts one of the following values: * ``repeat``: Repeat the last frame (the default). * ``endall``: End both streams. @@ -122,12 +130,13 @@ def overlay(main_parent_node, overlay_parent_node, eof_action='repeat', **kwargs eval: Set when the expressions for x, and y are evaluated. It accepts the following values: - * ``init``: only evaluate expressions once during the filter initialization or when a command is - processed + * ``init``: only evaluate expressions once during the filter initialization + or when a command is processed * ``frame``: evaluate expressions for each incoming frame Default value is ``frame``. - shortest: If set to 1, force the output to terminate when the shortest input terminates. Default value is 0. + shortest: If set to 1, force the output to terminate when the shortest input + terminates. Default value is 0. format: Set the format for the output video. It accepts the following values: @@ -138,10 +147,12 @@ def overlay(main_parent_node, overlay_parent_node, eof_action='repeat', **kwargs * ``gbrp``: force planar RGB output Default value is ``yuv420``. - rgb (deprecated): If set to 1, force the filter to accept inputs in the RGB color space. Default value is 0. - This option is deprecated, use format instead. - repeatlast: If set to 1, force the filter to draw the last overlay frame over the main input until the end of - the stream. A value of 0 disables this behavior. Default value is 1. + rgb (deprecated): If set to 1, force the filter to accept inputs in the RGB + color space. Default value is 0. This option is deprecated, use format + instead. + repeatlast: If set to 1, force the filter to draw the last overlay frame over + the main input until the end of the stream. A value of 0 disables this + behavior. Default value is 1. Official documentation: `overlay `__ """ @@ -196,14 +207,20 @@ def drawbox(stream, x, y, width, height, color, thickness=None, **kwargs): """Draw a colored box on the input image. Args: - x: The expression which specifies the top left corner x coordinate of the box. It defaults to 0. - y: The expression which specifies the top left corner y coordinate of the box. It defaults to 0. - width: Specify the width of the box; if 0 interpreted as the input width. It defaults to 0. - heigth: Specify the height of the box; if 0 interpreted as the input height. It defaults to 0. - color: Specify the color of the box to write. For the general syntax of this option, check the "Color" section - in the ffmpeg-utils manual. If the special value invert is used, the box edge color is the same as the - video with inverted luma. - thickness: The expression which sets the thickness of the box edge. Default value is 3. + x: The expression which specifies the top left corner x coordinate of the box. + It defaults to 0. + y: The expression which specifies the top left corner y coordinate of the box. + It defaults to 0. + width: Specify the width of the box; if 0 interpreted as the input width. It + defaults to 0. + heigth: Specify the height of the box; if 0 interpreted as the input height. It + defaults to 0. + color: Specify the color of the box to write. For the general syntax of this + option, check the "Color" section in the ffmpeg-utils manual. If the + special value invert is used, the box edge color is the same as the video + with inverted luma. + thickness: The expression which sets the thickness of the box edge. Default + value is 3. w: Alias for ``width``. h: Alias for ``height``. c: Alias for ``color``. @@ -220,46 +237,57 @@ def drawbox(stream, x, y, width, height, color, thickness=None, **kwargs): @filter_operator() def drawtext(stream, text=None, x=0, y=0, escape_text=True, **kwargs): - """Draw a text string or text from a specified file on top of a video, using the libfreetype library. + """Draw a text string or text from a specified file on top of a video, using the + libfreetype library. - To enable compilation of this filter, you need to configure FFmpeg with ``--enable-libfreetype``. To enable default - font fallback and the font option you need to configure FFmpeg with ``--enable-libfontconfig``. To enable the + To enable compilation of this filter, you need to configure FFmpeg with + ``--enable-libfreetype``. To enable default font fallback and the font option you + need to configure FFmpeg with ``--enable-libfontconfig``. To enable the text_shaping option, you need to configure FFmpeg with ``--enable-libfribidi``. Args: - box: Used to draw a box around text using the background color. The value must be either 1 (enable) or 0 - (disable). The default value of box is 0. - boxborderw: Set the width of the border to be drawn around the box using boxcolor. The default value of - boxborderw is 0. - boxcolor: The color to be used for drawing box around text. For the syntax of this option, check the "Color" - section in the ffmpeg-utils manual. The default value of boxcolor is "white". - line_spacing: Set the line spacing in pixels of the border to be drawn around the box using box. The default - value of line_spacing is 0. - borderw: Set the width of the border to be drawn around the text using bordercolor. The default value of - borderw is 0. - bordercolor: Set the color to be used for drawing border around text. For the syntax of this option, check the - "Color" section in the ffmpeg-utils manual. The default value of bordercolor is "black". - expansion: Select how the text is expanded. Can be either none, strftime (deprecated) or normal (default). See - the Text expansion section below for details. - basetime: Set a start time for the count. Value is in microseconds. Only applied in the deprecated strftime - expansion mode. To emulate in normal expansion mode use the pts function, supplying the start time (in - seconds) as the second argument. + box: Used to draw a box around text using the background color. The value must + be either 1 (enable) or 0 (disable). The default value of box is 0. + boxborderw: Set the width of the border to be drawn around the box using + boxcolor. The default value of boxborderw is 0. + boxcolor: The color to be used for drawing box around text. For the syntax of + this option, check the "Color" section in the ffmpeg-utils manual. The + default value of boxcolor is "white". + line_spacing: Set the line spacing in pixels of the border to be drawn around + the box using box. The default value of line_spacing is 0. + borderw: Set the width of the border to be drawn around the text using + bordercolor. The default value of borderw is 0. + bordercolor: Set the color to be used for drawing border around text. For the + syntax of this option, check the "Color" section in the ffmpeg-utils + manual. The default value of bordercolor is "black". + expansion: Select how the text is expanded. Can be either none, strftime + (deprecated) or normal (default). See the Text expansion section below for + details. + basetime: Set a start time for the count. Value is in microseconds. Only + applied in the deprecated strftime expansion mode. To emulate in normal + expansion mode use the pts function, supplying the start time (in seconds) + as the second argument. fix_bounds: If true, check and fix text coords to avoid clipping. - fontcolor: The color to be used for drawing fonts. For the syntax of this option, check the "Color" section in - the ffmpeg-utils manual. The default value of fontcolor is "black". - fontcolor_expr: String which is expanded the same way as text to obtain dynamic fontcolor value. By default - this option has empty value and is not processed. When this option is set, it overrides fontcolor option. + fontcolor: The color to be used for drawing fonts. For the syntax of this + option, check the "Color" section in the ffmpeg-utils manual. The default + value of fontcolor is "black". + fontcolor_expr: String which is expanded the same way as text to obtain dynamic + fontcolor value. By default this option has empty value and is not + processed. When this option is set, it overrides fontcolor option. font: The font family to be used for drawing text. By default Sans. - fontfile: The font file to be used for drawing text. The path must be included. This parameter is mandatory if - the fontconfig support is disabled. - alpha: Draw the text applying alpha blending. The value can be a number between 0.0 and 1.0. The expression - accepts the same variables x, y as well. The default value is 1. Please see fontcolor_expr. - fontsize: The font size to be used for drawing text. The default value of fontsize is 16. - text_shaping: If set to 1, attempt to shape the text (for example, reverse the order of right-to-left text and - join Arabic characters) before drawing it. Otherwise, just draw the text exactly as given. By default 1 (if - supported). - ft_load_flags: The flags to be used for loading the fonts. The flags map the corresponding flags supported by - libfreetype, and are a combination of the following values: + fontfile: The font file to be used for drawing text. The path must be included. + This parameter is mandatory if the fontconfig support is disabled. + alpha: Draw the text applying alpha blending. The value can be a number between + 0.0 and 1.0. The expression accepts the same variables x, y as well. The + default value is 1. Please see fontcolor_expr. + fontsize: The font size to be used for drawing text. The default value of + fontsize is 16. + text_shaping: If set to 1, attempt to shape the text (for example, reverse the + order of right-to-left text and join Arabic characters) before drawing it. + Otherwise, just draw the text exactly as given. By default 1 (if supported). + ft_load_flags: The flags to be used for loading the fonts. The flags map the + corresponding flags supported by libfreetype, and are a combination of the + following values: * ``default`` * ``no_scale`` @@ -277,75 +305,89 @@ def drawtext(stream, text=None, x=0, y=0, escape_text=True, **kwargs): * ``linear_design`` * ``no_autohint`` - Default value is "default". For more information consult the documentation for the FT_LOAD_* libfreetype - flags. - shadowcolor: The color to be used for drawing a shadow behind the drawn text. For the syntax of this option, - check the "Color" section in the ffmpeg-utils manual. The default value of shadowcolor is "black". - shadowx: The x offset for the text shadow position with respect to the position of the text. It can be either - positive or negative values. The default value is "0". - shadowy: The y offset for the text shadow position with respect to the position of the text. It can be either - positive or negative values. The default value is "0". - start_number: The starting frame number for the n/frame_num variable. The default value is "0". - tabsize: The size in number of spaces to use for rendering the tab. Default value is 4. - timecode: Set the initial timecode representation in "hh:mm:ss[:;.]ff" format. It can be used with or without - text parameter. timecode_rate option must be specified. + Default value is "default". For more information consult the documentation + for the FT_LOAD_* libfreetype flags. + shadowcolor: The color to be used for drawing a shadow behind the drawn text. + For the syntax of this option, check the "Color" section in the ffmpeg-utils + manual. The default value of shadowcolor is "black". + shadowx: The x offset for the text shadow position with respect to the position + of the text. It can be either positive or negative values. The default value + is "0". + shadowy: The y offset for the text shadow position with respect to the position + of the text. It can be either positive or negative values. The default value + is "0". + start_number: The starting frame number for the n/frame_num variable. The + default value is "0". + tabsize: The size in number of spaces to use for rendering the tab. Default + value is 4. + timecode: Set the initial timecode representation in "hh:mm:ss[:;.]ff" format. + It can be used with or without text parameter. timecode_rate option must be + specified. rate: Set the timecode frame rate (timecode only). timecode_rate: Alias for ``rate``. r: Alias for ``rate``. - tc24hmax: If set to 1, the output of the timecode option will wrap around at 24 hours. Default is 0 (disabled). - text: The text string to be drawn. The text must be a sequence of UTF-8 encoded characters. This parameter is - mandatory if no file is specified with the parameter textfile. - textfile: A text file containing text to be drawn. The text must be a sequence of UTF-8 encoded characters. - This parameter is mandatory if no text string is specified with the parameter text. If both text and - textfile are specified, an error is thrown. - reload: If set to 1, the textfile will be reloaded before each frame. Be sure to update it atomically, or it - may be read partially, or even fail. - x: The expression which specifies the offset where text will be drawn within the video frame. It is relative to - the left border of the output image. The default value is "0". - y: The expression which specifies the offset where text will be drawn within the video frame. It is relative to - the top border of the output image. The default value is "0". See below for the list of accepted constants - and functions. + tc24hmax: If set to 1, the output of the timecode option will wrap around at 24 + hours. Default is 0 (disabled). + text: The text string to be drawn. The text must be a sequence of UTF-8 encoded + characters. This parameter is mandatory if no file is specified with the + parameter textfile. + textfile: A text file containing text to be drawn. The text must be a sequence + of UTF-8 encoded characters. This parameter is mandatory if no text string + is specified with the parameter text. If both text and textfile are + specified, an error is thrown. + reload: If set to 1, the textfile will be reloaded before each frame. Be sure + to update it atomically, or it may be read partially, or even fail. + x: The expression which specifies the offset where text will be drawn within + the video frame. It is relative to the left border of the output image. The + default value is "0". + y: The expression which specifies the offset where text will be drawn within + the video frame. It is relative to the top border of the output image. The + default value is "0". See below for the list of accepted constants and + functions. Expression constants: - The parameters for x and y are expressions containing the following constants and functions: - - dar: input display aspect ratio, it is the same as ``(w / h) * sar`` - - hsub: horizontal chroma subsample values. For example for the pixel format "yuv422p" hsub is 2 and vsub - is 1. - - vsub: vertical chroma subsample values. For example for the pixel format "yuv422p" hsub is 2 and vsub - is 1. - - line_h: the height of each text line - - lh: Alias for ``line_h``. - - main_h: the input height - - h: Alias for ``main_h``. - - H: Alias for ``main_h``. - - main_w: the input width - - w: Alias for ``main_w``. - - W: Alias for ``main_w``. - - ascent: the maximum distance from the baseline to the highest/upper grid coordinate used to place a glyph - outline point, for all the rendered glyphs. It is a positive value, due to the grid's orientation with the Y - axis upwards. - - max_glyph_a: Alias for ``ascent``. - - descent: the maximum distance from the baseline to the lowest grid coordinate used to place a glyph outline - point, for all the rendered glyphs. This is a negative value, due to the grid's orientation, with the Y axis - upwards. - - max_glyph_d: Alias for ``descent``. - - max_glyph_h: maximum glyph height, that is the maximum height for all the glyphs contained in the rendered - text, it is equivalent to ascent - descent. - - max_glyph_w: maximum glyph width, that is the maximum width for all the glyphs contained in the rendered - text. - - n: the number of input frame, starting from 0 - - rand(min, max): return a random number included between min and max - - sar: The input sample aspect ratio. - - t: timestamp expressed in seconds, NAN if the input timestamp is unknown - - text_h: the height of the rendered text - - th: Alias for ``text_h``. - - text_w: the width of the rendered text - - tw: Alias for ``text_w``. - - x: the x offset coordinates where the text is drawn. - - y: the y offset coordinates where the text is drawn. - - These parameters allow the x and y expressions to refer each other, so you can for example specify - ``y=x/dar``. + The parameters for x and y are expressions containing the following constants + and functions: + - dar: input display aspect ratio, it is the same as ``(w / h) * sar`` + - hsub: horizontal chroma subsample values. For example for the pixel format + "yuv422p" hsub is 2 and vsub is 1. + - vsub: vertical chroma subsample values. For example for the pixel format + "yuv422p" hsub is 2 and vsub is 1. + - line_h: the height of each text line + - lh: Alias for ``line_h``. + - main_h: the input height + - h: Alias for ``main_h``. + - H: Alias for ``main_h``. + - main_w: the input width + - w: Alias for ``main_w``. + - W: Alias for ``main_w``. + - ascent: the maximum distance from the baseline to the highest/upper grid + coordinate used to place a glyph outline point, for all the rendered glyphs. + It is a positive value, due to the grid's orientation with the Y axis + upwards. + - max_glyph_a: Alias for ``ascent``. + - descent: the maximum distance from the baseline to the lowest grid + coordinate used to place a glyph outline + point, for all the rendered glyphs. This is a negative value, due to the + grid's orientation, with the Y axis upwards. + - max_glyph_d: Alias for ``descent``. + - max_glyph_h: maximum glyph height, that is the maximum height for all the + glyphs contained in the rendered text, it is equivalent to ascent - descent. + - max_glyph_w: maximum glyph width, that is the maximum width for all the + glyphs contained in the rendered text. + - n: the number of input frame, starting from 0 + - rand(min, max): return a random number included between min and max + - sar: The input sample aspect ratio. + - t: timestamp expressed in seconds, NAN if the input timestamp is unknown + - text_h: the height of the rendered text + - th: Alias for ``text_h``. + - text_w: the width of the rendered text + - tw: Alias for ``text_w``. + - x: the x offset coordinates where the text is drawn. + - y: the y offset coordinates where the text is drawn. + + These parameters allow the x and y expressions to refer each other, so you can + for example specify ``y=x/dar``. Official documentation: `drawtext `__ """ @@ -364,25 +406,28 @@ def drawtext(stream, text=None, x=0, y=0, escape_text=True, **kwargs): def concat(*streams, **kwargs): """Concatenate audio and video streams, joining them together one after the other. - The filter works on segments of synchronized video and audio streams. All segments must have the same number of - streams of each type, and that will also be the number of streams at output. + The filter works on segments of synchronized video and audio streams. All segments + must have the same number of streams of each type, and that will also be the number + of streams at output. Args: unsafe: Activate unsafe mode: do not fail if segments have a different format. - Related streams do not always have exactly the same duration, for various reasons including codec frame size or - sloppy authoring. For that reason, related synchronized streams (e.g. a video and its audio track) should be - concatenated at once. The concat filter will use the duration of the longest stream in each segment (except the - last one), and if necessary pad shorter audio streams with silence. + Related streams do not always have exactly the same duration, for various reasons + including codec frame size or sloppy authoring. For that reason, related + synchronized streams (e.g. a video and its audio track) should be concatenated at + once. The concat filter will use the duration of the longest stream in each segment + (except the last one), and if necessary pad shorter audio streams with silence. For this filter to work correctly, all segments must start at timestamp 0. - All corresponding streams must have the same parameters in all segments; the filtering system will automatically - select a common pixel format for video streams, and a common sample format, sample rate and channel layout for - audio streams, but other settings, such as resolution, must be converted explicitly by the user. + All corresponding streams must have the same parameters in all segments; the + filtering system will automatically select a common pixel format for video streams, + and a common sample format, sample rate and channel layout for audio streams, but + other settings, such as resolution, must be converted explicitly by the user. - Different frame rates are acceptable but will result in variable frame rate at output; be sure to configure the - output file to handle it. + Different frame rates are acceptable but will result in variable frame rate at + output; be sure to configure the output file to handle it. Official documentation: `concat `__ """ @@ -407,8 +452,8 @@ def zoompan(stream, **kwargs): zoom: Set the zoom expression. Default is 1. x: Set the x expression. Default is 0. y: Set the y expression. Default is 0. - d: Set the duration expression in number of frames. This sets for how many number of frames effect will last - for single input image. + d: Set the duration expression in number of frames. This sets for how many + number of frames effect will last for single input image. s: Set the output image size, default is ``hd720``. fps: Set the output frame rate, default is 25. z: Alias for ``zoom``. @@ -423,10 +468,14 @@ def hue(stream, **kwargs): """Modify the hue and/or the saturation of the input. Args: - h: Specify the hue angle as a number of degrees. It accepts an expression, and defaults to "0". - s: Specify the saturation in the [-10,10] range. It accepts an expression and defaults to "1". - H: Specify the hue angle as a number of radians. It accepts an expression, and defaults to "0". - b: Specify the brightness in the [-10,10] range. It accepts an expression and defaults to "0". + h: Specify the hue angle as a number of degrees. It accepts an expression, and + defaults to "0". + s: Specify the saturation in the [-10,10] range. It accepts an expression and + defaults to "1". + H: Specify the hue angle as a number of radians. It accepts an expression, and + defaults to "0". + b: Specify the brightness in the [-10,10] range. It accepts an expression and + defaults to "0". Official documentation: `hue `__ """ diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index eadb90a4..5f25a347 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -88,8 +88,8 @@ def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_ if len(downstreams) > 1: # TODO: automatically insert `splits` ahead of time via graph transformation. raise ValueError( - 'Encountered {} with multiple outgoing edges with same upstream label {!r}; a ' - '`split` filter is probably required'.format( + 'Encountered {} with multiple outgoing edges with same upstream ' + 'label {!r}; a `split` filter is probably required'.format( upstream_node, upstream_label ) ) @@ -199,7 +199,7 @@ def run_async( pipe_stderr=False, quiet=False, overwrite_output=False, - cwd=None + cwd=None, ): """Asynchronously invoke ffmpeg for the supplied node graph. @@ -286,8 +286,11 @@ def run_async( stderr_stream = subprocess.STDOUT stdout_stream = subprocess.DEVNULL return subprocess.Popen( - args, stdin=stdin_stream, stdout=stdout_stream, stderr=stderr_stream, - cwd=cwd + args, + stdin=stdin_stream, + stdout=stdout_stream, + stderr=stderr_stream, + cwd=cwd, ) @@ -300,7 +303,7 @@ def run( input=None, quiet=False, overwrite_output=False, - cwd=None + cwd=None, ): """Invoke ffmpeg for the supplied node graph. @@ -324,7 +327,7 @@ def run( pipe_stderr=capture_stderr, quiet=quiet, overwrite_output=overwrite_output, - cwd=cwd + cwd=cwd, ) out, err = process.communicate(input) retcode = process.poll() @@ -333,4 +336,10 @@ def run( return out, err -__all__ = ['compile', 'Error', 'get_args', 'run', 'run_async'] +__all__ = [ + 'compile', + 'Error', + 'get_args', + 'run', + 'run_async', +] diff --git a/ffmpeg/_utils.py b/ffmpeg/_utils.py index 92d76110..55682a28 100644 --- a/ffmpeg/_utils.py +++ b/ffmpeg/_utils.py @@ -49,8 +49,8 @@ class basestring(with_metaclass(BaseBaseString)): def _recursive_repr(item): """Hack around python `repr` to deterministically represent dictionaries. - This is able to represent more things than json.dumps, since it does not require things to be JSON serializable - (e.g. datetimes). + This is able to represent more things than json.dumps, since it does not require + things to be JSON serializable (e.g. datetimes). """ if isinstance(item, basestring): result = str(item) diff --git a/ffmpeg/_view.py b/ffmpeg/_view.py index fb129fa8..31955afd 100644 --- a/ffmpeg/_view.py +++ b/ffmpeg/_view.py @@ -35,8 +35,8 @@ def view(stream_spec, detail=False, filename=None, pipe=False, **kwargs): import graphviz except ImportError: raise ImportError( - 'failed to import graphviz; please make sure graphviz is installed (e.g. `pip install ' - 'graphviz`)' + 'failed to import graphviz; please make sure graphviz is installed (e.g. ' + '`pip install graphviz`)' ) show_labels = kwargs.pop('show_labels', True) diff --git a/ffmpeg/dag.py b/ffmpeg/dag.py index 9564d7f8..3508dd49 100644 --- a/ffmpeg/dag.py +++ b/ffmpeg/dag.py @@ -9,38 +9,45 @@ class DagNode(object): """Node in a directed-acyclic graph (DAG). Edges: - DagNodes are connected by edges. An edge connects two nodes with a label for each side: + DagNodes are connected by edges. An edge connects two nodes with a label for + each side: - ``upstream_node``: upstream/parent node - ``upstream_label``: label on the outgoing side of the upstream node - ``downstream_node``: downstream/child node - ``downstream_label``: label on the incoming side of the downstream node - For example, DagNode A may be connected to DagNode B with an edge labelled "foo" on A's side, and "bar" on B's - side: + For example, DagNode A may be connected to DagNode B with an edge labelled + "foo" on A's side, and "bar" on B's side: _____ _____ | | | | | A >[foo]---[bar]> B | |_____| |_____| - Edge labels may be integers or strings, and nodes cannot have more than one incoming edge with the same label. + Edge labels may be integers or strings, and nodes cannot have more than one + incoming edge with the same label. - DagNodes may have any number of incoming edges and any number of outgoing edges. DagNodes keep track only of - their incoming edges, but the entire graph structure can be inferred by looking at the furthest downstream - nodes and working backwards. + DagNodes may have any number of incoming edges and any number of outgoing + edges. DagNodes keep track only of their incoming edges, but the entire graph + structure can be inferred by looking at the furthest downstream nodes and + working backwards. Hashing: - DagNodes must be hashable, and two nodes are considered to be equivalent if they have the same hash value. + DagNodes must be hashable, and two nodes are considered to be equivalent if + they have the same hash value. - Nodes are immutable, and the hash should remain constant as a result. If a node with new contents is required, - create a new node and throw the old one away. + Nodes are immutable, and the hash should remain constant as a result. If a + node with new contents is required, create a new node and throw the old one + away. String representation: - In order for graph visualization tools to show useful information, nodes must be representable as strings. The - ``repr`` operator should provide a more or less "full" representation of the node, and the ``short_repr`` - property should be a shortened, concise representation. + In order for graph visualization tools to show useful information, nodes must + be representable as strings. The ``repr`` operator should provide a more or + less "full" representation of the node, and the ``short_repr`` property should + be a shortened, concise representation. - Again, because nodes are immutable, the string representations should remain constant. + Again, because nodes are immutable, the string representations should remain + constant. """ def __hash__(self): @@ -48,7 +55,9 @@ def __hash__(self): raise NotImplementedError() def __eq__(self, other): - """Compare two nodes; implementations should return True if (and only if) hashes match.""" + """Compare two nodes; implementations should return True if (and only if) + hashes match. + """ raise NotImplementedError() def __repr__(self, other): @@ -64,8 +73,9 @@ def short_repr(self): def incoming_edge_map(self): """Provides information about all incoming edges that connect to this node. - The edge map is a dictionary that maps an ``incoming_label`` to ``(outgoing_node, outgoing_label)``. Note that - implicity, ``incoming_node`` is ``self``. See "Edges" section above. + The edge map is a dictionary that maps an ``incoming_label`` to + ``(outgoing_node, outgoing_label)``. Note that implicity, ``incoming_node`` is + ``self``. See "Edges" section above. """ raise NotImplementedError() @@ -116,8 +126,7 @@ def get_outgoing_edges(upstream_node, outgoing_edge_map): class KwargReprNode(DagNode): - """A DagNode that can be represented as a set of args+kwargs. - """ + """A DagNode that can be represented as a set of args+kwargs.""" @property def __upstream_hashes(self): diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py index cacab8ee..e8b28385 100644 --- a/ffmpeg/nodes.py +++ b/ffmpeg/nodes.py @@ -21,7 +21,9 @@ def _get_types_str(types): class Stream(object): - """Represents the outgoing edge of an upstream node; may be used to create more downstream nodes.""" + """Represents the outgoing edge of an upstream node; may be used to create more + downstream nodes. + """ def __init__( self, upstream_node, upstream_label, node_types, upstream_selector=None @@ -214,9 +216,10 @@ def stream(self, label=None, selector=None): return self.__outgoing_stream_type(self, label, upstream_selector=selector) def __getitem__(self, item): - """Create an outgoing stream originating from this node; syntactic sugar for ``self.stream(label)``. - It can also be used to apply a selector: e.g. ``node[0:'a']`` returns a stream with label 0 and - selector ``'a'``, which is the same as ``node.stream(label=0, selector='a')``. + """Create an outgoing stream originating from this node; syntactic sugar for + ``self.stream(label)``. It can also be used to apply a selector: e.g. + ``node[0:'a']`` returns a stream with label 0 and selector ``'a'``, which is + the same as ``node.stream(label=0, selector='a')``. Example: Process the audio and video portions of a stream independently:: diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 4a8183ca..ba1fa361 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -116,9 +116,20 @@ def test_stream_repr(): dummy_out.label, dummy_out.node.short_hash ) + def test_repeated_args(): - out_file = ffmpeg.input('dummy.mp4').output('dummy2.mp4', streamid=['0:0x101', '1:0x102']) - assert out_file.get_args() == ['-i', 'dummy.mp4', '-streamid', '0:0x101', '-streamid', '1:0x102', 'dummy2.mp4'] + out_file = ffmpeg.input('dummy.mp4').output( + 'dummy2.mp4', streamid=['0:0x101', '1:0x102'] + ) + assert out_file.get_args() == [ + '-i', + 'dummy.mp4', + '-streamid', + '0:0x101', + '-streamid', + '1:0x102', + 'dummy2.mp4', + ] def test__get_args__simple(): @@ -332,8 +343,13 @@ def test_filter_asplit(): '-i', TEST_INPUT_FILE1, '-filter_complex', - '[0]vflip[s0];[s0]asplit=2[s1][s2];[s1]atrim=end=20:start=10[s3];[s2]atrim=end=40:start=30[s4];[s3]' - '[s4]concat=n=2[s5]', + ( + '[0]vflip[s0];' + '[s0]asplit=2[s1][s2];' + '[s1]atrim=end=20:start=10[s3];' + '[s2]atrim=end=40:start=30[s4];' + '[s3][s4]concat=n=2[s5]' + ), '-map', '[s5]', TEST_OUTPUT_FILE1, @@ -357,10 +373,14 @@ def test__output__video_size(video_size): def test_filter_normal_arg_escape(): - """Test string escaping of normal filter args (e.g. ``font`` param of ``drawtext`` filter).""" + """Test string escaping of normal filter args (e.g. ``font`` param of ``drawtext`` + filter). + """ def _get_drawtext_font_repr(font): - """Build a command-line arg using drawtext ``font`` param and extract the ``-filter_complex`` arg.""" + """Build a command-line arg using drawtext ``font`` param and extract the + ``-filter_complex`` arg. + """ args = ( ffmpeg.input('in') .drawtext('test', font='a{}b'.format(font)) @@ -370,7 +390,9 @@ def _get_drawtext_font_repr(font): assert args[:3] == ['-i', 'in', '-filter_complex'] assert args[4:] == ['-map', '[s0]', 'out'] match = re.match( - r'\[0\]drawtext=font=a((.|\n)*)b:text=test\[s0\]', args[3], re.MULTILINE + r'\[0\]drawtext=font=a((.|\n)*)b:text=test\[s0\]', + args[3], + re.MULTILINE, ) assert match is not None, 'Invalid -filter_complex arg: {!r}'.format(args[3]) return match.group(1) @@ -394,10 +416,14 @@ def _get_drawtext_font_repr(font): def test_filter_text_arg_str_escape(): - """Test string escaping of normal filter args (e.g. ``text`` param of ``drawtext`` filter).""" + """Test string escaping of normal filter args (e.g. ``text`` param of ``drawtext`` + filter). + """ def _get_drawtext_text_repr(text): - """Build a command-line arg using drawtext ``text`` param and extract the ``-filter_complex`` arg.""" + """Build a command-line arg using drawtext ``text`` param and extract the + ``-filter_complex`` arg. + """ args = ffmpeg.input('in').drawtext('a{}b'.format(text)).output('out').get_args() assert args[:3] == ['-i', 'in', '-filter_complex'] assert args[4:] == ['-map', '[s0]', 'out'] @@ -447,8 +473,11 @@ def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr, cwd): popen__mock = mocker.patch.object(subprocess, 'Popen', return_value=process__mock) stream = _get_simple_example() process = ffmpeg.run_async( - stream, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout, - pipe_stderr=pipe_stderr, cwd=cwd + stream, + pipe_stdin=pipe_stdin, + pipe_stdout=pipe_stdout, + pipe_stderr=pipe_stderr, + cwd=cwd, ) assert process is process__mock @@ -458,8 +487,10 @@ def test__run_async(mocker, pipe_stdin, pipe_stdout, pipe_stderr, cwd): (args,), kwargs = popen__mock.call_args assert args == ffmpeg.compile(stream) assert kwargs == dict( - stdin=expected_stdin, stdout=expected_stdout, stderr=expected_stderr, - cwd=cwd + stdin=expected_stdin, + stdout=expected_stdout, + stderr=expected_stderr, + cwd=cwd, ) @@ -695,7 +726,10 @@ def test_pipe(): cmd = ['ffmpeg'] + args p = subprocess.Popen( - cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) in_data = bytes( @@ -715,10 +749,10 @@ def test__probe(): assert data['format']['duration'] == '7.036000' -@pytest.mark.skipif(sys.version_info < (3, 3), reason="requires python3.3 or higher") +@pytest.mark.skipif(sys.version_info < (3, 3), reason='requires python3.3 or higher') def test__probe_timeout(): with pytest.raises(subprocess.TimeoutExpired) as excinfo: - data = ffmpeg.probe(TEST_INPUT_FILE1, timeout=0) + ffmpeg.probe(TEST_INPUT_FILE1, timeout=0) assert 'timed out after 0 seconds' in str(excinfo.value) @@ -751,24 +785,24 @@ def get_filter_complex_outputs(flt, name): def test__get_filter_complex_input(): - assert get_filter_complex_input("", "scale") is None - assert get_filter_complex_input("scale", "scale") is None - assert get_filter_complex_input("scale[s3][s4];etc", "scale") is None - assert get_filter_complex_input("[s2]scale", "scale") == "s2" - assert get_filter_complex_input("[s2]scale;etc", "scale") == "s2" - assert get_filter_complex_input("[s2]scale[s3][s4];etc", "scale") == "s2" + assert get_filter_complex_input('', 'scale') is None + assert get_filter_complex_input('scale', 'scale') is None + assert get_filter_complex_input('scale[s3][s4];etc', 'scale') is None + assert get_filter_complex_input('[s2]scale', 'scale') == 's2' + assert get_filter_complex_input('[s2]scale;etc', 'scale') == 's2' + assert get_filter_complex_input('[s2]scale[s3][s4];etc', 'scale') == 's2' def test__get_filter_complex_outputs(): - assert get_filter_complex_outputs("", "scale") is None - assert get_filter_complex_outputs("scale", "scale") is None - assert get_filter_complex_outputs("scalex[s0][s1]", "scale") is None - assert get_filter_complex_outputs("scale[s0][s1]", "scale") == ['s0', 's1'] - assert get_filter_complex_outputs("[s5]scale[s0][s1]", "scale") == ['s0', 's1'] - assert get_filter_complex_outputs("[s5]scale[s1][s0]", "scale") == ['s1', 's0'] - assert get_filter_complex_outputs("[s5]scale[s1]", "scale") == ['s1'] - assert get_filter_complex_outputs("[s5]scale[s1];x", "scale") == ['s1'] - assert get_filter_complex_outputs("y;[s5]scale[s1];x", "scale") == ['s1'] + assert get_filter_complex_outputs('', 'scale') is None + assert get_filter_complex_outputs('scale', 'scale') is None + assert get_filter_complex_outputs('scalex[s0][s1]', 'scale') is None + assert get_filter_complex_outputs('scale[s0][s1]', 'scale') == ['s0', 's1'] + assert get_filter_complex_outputs('[s5]scale[s0][s1]', 'scale') == ['s0', 's1'] + assert get_filter_complex_outputs('[s5]scale[s1][s0]', 'scale') == ['s1', 's0'] + assert get_filter_complex_outputs('[s5]scale[s1]', 'scale') == ['s1'] + assert get_filter_complex_outputs('[s5]scale[s1];x', 'scale') == ['s1'] + assert get_filter_complex_outputs('y;[s5]scale[s1];x', 'scale') == ['s1'] def test__multi_output_edge_label_order(): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..de71e58d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.black] +skip-string-normalization = true +target_version = ['py27'] # TODO: drop Python 2 support (... "Soon"). +include = '\.pyi?$' +exclude = ''' +( + /( + \.eggs + | \.git + | \.tox + | \venv + | dist + )/ +) +''' From 29b6f0929805b09a20184a1d4500bf31e4dbc138 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 7 Mar 2022 00:05:43 -0800 Subject: [PATCH 25/32] Use GitHub Actions for CI. (#641) This sets up GitHub Actions (GHA) to run in place of the currently broken Travis CI. Initially, this only covers running tox/pytest and Black, but may eventually be extended to run pylint, mypy, flake8, etc. - see #605, for example. Notes: * Python 3.10 is not yet supported due to the `collections.Iterable` issue discussed in #330, #624, etc. * The Black CI step acts as a linting step, rather than attempting to have the GHA job automatically update/commit/push the reformarted code. * Black is currently pinned to an older version that supports `--target-version py27` until Python 2 compatibility can be dropped in the final Python 2 compatibility release of ffmpeg-python. * Only the main source directory (`ffmpeg/`) is checked with Black at the moment. The `examples/` directory should also be checked, but will be done as a separate PR. Co-authored by: Christian Clauss --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 32 ----------------------------- README.md | 5 ++++- ffmpeg/_utils.py | 1 - setup.py | 2 -- tox.ini | 11 +++++++++- 6 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6289d8bc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + python-version: + - "2.7" + - "3.5" + - "3.6" + - "3.7" + - "3.8" + - "3.9" + # - "3.10" # FIXME: broken due to `collections.Iterable` issue; see #330 / #624 / etc. + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install ffmpeg + run: | + sudo apt update + sudo apt install ffmpeg + - name: Setup pip + tox + run: | + python -m pip install --upgrade \ + "pip==20.3.4; python_version < '3.6'" \ + "pip==21.3.1; python_version >= '3.6'" + python -m pip install tox==3.24.5 tox-gh-actions==2.9.1 + - name: Test with tox + run: tox + black: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: psf/black@21.12b0 # TODO: upgrade after dropping Python 2 support. + with: + src: ffmpeg # TODO: also format `examples`. + version: 21.12b0 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e4d7d758..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: python -before_install: - - > - [ -f ffmpeg-release/ffmpeg ] || ( - curl -O https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && - mkdir -p ffmpeg-release && - tar Jxf ffmpeg-release-amd64-static.tar.xz --strip-components=1 -C ffmpeg-release - ) -matrix: - include: - - python: 2.7 - env: TOX_ENV=py27 - - python: 3.4 - env: TOX_ENV=py34 - - python: 3.5 - env: TOX_ENV=py35 - - python: 3.6 - env: TOX_ENV=py36 - - python: 3.7 - dist: xenial # required for Python >= 3.7 - env: TOX_ENV=py37 - - python: pypy - env: TOX_ENV=pypy -install: - - pip install tox -script: - - export PATH=$(readlink -f ffmpeg-release):$PATH - - tox -e $TOX_ENV -cache: - directories: - - .tox - - ffmpeg-release diff --git a/README.md b/README.md index 8cfe89ae..2ac9d628 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # ffmpeg-python: Python bindings for FFmpeg -[![Build status](https://travis-ci.org/kkroening/ffmpeg-python.svg?branch=master)](https://travis-ci.org/kkroening/ffmpeg-python) +[![CI][ci-badge]][ci] + +[ci-badge]: https://github.com/kkroening/ffmpeg-python/actions/workflows/ci.yml/badge.svg +[ci]: https://github.com/kkroening/ffmpeg-python/actions/workflows/ci.yml ffmpeg-python logo diff --git a/ffmpeg/_utils.py b/ffmpeg/_utils.py index 55682a28..94ef9d07 100644 --- a/ffmpeg/_utils.py +++ b/ffmpeg/_utils.py @@ -40,7 +40,6 @@ def __new__(cls, name, this_bases, d): class basestring(with_metaclass(BaseBaseString)): pass - else: # noinspection PyUnresolvedReferences,PyCompatibility from builtins import basestring diff --git a/setup.py b/setup.py index 0282c67e..743deb22 100644 --- a/setup.py +++ b/setup.py @@ -60,8 +60,6 @@ setup( name='ffmpeg-python', packages=['ffmpeg'], - setup_requires=['pytest-runner'], - tests_require=['pytest', 'pytest-mock'], version=version, description='Python bindings for FFmpeg - with complex filtering support', author='Karl Kroening', diff --git a/tox.ini b/tox.ini index 1e3ba533..e317207f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,16 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36, py37, pypy +envlist = py27, py35, py36, py37, py38, py39 + +[gh-actions] +python = + 2.7: py27 + 3.5: py35 + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 [testenv] commands = py.test -vv From cb9d400467014dd371ff5bb24d86be3fa6df8a2b Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 7 Mar 2022 01:19:09 -0800 Subject: [PATCH 26/32] Add FFmpeg installation instructions (#642) Co-authored-by: digitalcircuits <59550818+digitalcircuits@users.noreply.github.com> Co-authored-by: digitalcircuits --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ac9d628..b8ee9221 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,11 @@ Real-world signal graphs can get a heck of a lot more complex, but `ffmpeg-pytho ## Installation +### Installing `ffmpeg-python` + The latest version of `ffmpeg-python` can be acquired via a typical pip install: -``` +```bash pip install ffmpeg-python ``` @@ -93,6 +95,24 @@ git clone git@github.com:kkroening/ffmpeg-python.git pip install -e ./ffmpeg-python ``` +> **Note**: `ffmpeg-python` makes no attempt to download/install FFmpeg, as `ffmpeg-python` is merely a pure-Python wrapper - whereas FFmpeg installation is platform-dependent/environment-specific, and is thus the responsibility of the user, as described below. + +### Installing FFmpeg + +Before using `ffmpeg-python`, FFmpeg must be installed and accessible via the `$PATH` environment variable. + +There are a variety of ways to install FFmpeg, such as the [official download links](https://ffmpeg.org/download.html), or using your package manager of choice (e.g. `sudo apt install ffmpeg` on Debian/Ubuntu, `brew install ffmpeg` on OS X, etc.). + +Regardless of how FFmpeg is installed, you can check if your environment path is set correctly by running the `ffmpeg` command from the terminal, in which case the version information should appear, as in the following example (truncated for brevity): + +``` +$ ffmpeg +ffmpeg version 4.2.4-1ubuntu0.1 Copyright (c) 2000-2020 the FFmpeg developers + built with gcc 9 (Ubuntu 9.3.0-10ubuntu2) +``` + +> **Note**: The actual version information displayed here may vary from one system to another; but if a message such as `ffmpeg: command not found` appears instead of the version information, FFmpeg is not properly installed. + ## [Examples](https://github.com/kkroening/ffmpeg-python/tree/master/examples) When in doubt, take a look at the [examples](https://github.com/kkroening/ffmpeg-python/tree/master/examples) to see if there's something that's close to whatever you're trying to do. @@ -197,7 +217,7 @@ When in doubt, refer to the [existing filters](https://github.com/kkroening/ffmp **Why do I get an import/attribute/etc. error from `import ffmpeg`?** -Make sure you ran `pip install ffmpeg-python` and not `pip install ffmpeg` or `pip install python-ffmpeg`. +Make sure you ran `pip install ffmpeg-python` and _**not**_ `pip install ffmpeg` (wrong) or `pip install python-ffmpeg` (also wrong). **Why did my audio stream get dropped?** From 6189cd6861a90f6f52e6a8ba2db0fada54134194 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Mon, 7 Mar 2022 15:16:52 +0530 Subject: [PATCH 27/32] Import ABC from collections.abc for Python 3.9+ compatibility (#330) * Import ABC from collections.abc instead of collections for Python 3.9 compatibility. * Fix deprecation warnings due to invalid escape sequences. * Support Python 3.10 Co-authored-by: Karl Kroening --- .github/workflows/ci.yml | 2 +- examples/split_silence.py | 6 +++--- ffmpeg/_run.py | 10 ++++++---- ffmpeg/_utils.py | 8 ++++++-- ffmpeg/tests/test_ffmpeg.py | 2 +- setup.py | 4 ++++ tox.ini | 3 ++- 7 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6289d8bc..cf65206d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - "3.7" - "3.8" - "3.9" - # - "3.10" # FIXME: broken due to `collections.Iterable` issue; see #330 / #624 / etc. + - "3.10" steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} diff --git a/examples/split_silence.py b/examples/split_silence.py index a889db10..90b46d95 100755 --- a/examples/split_silence.py +++ b/examples/split_silence.py @@ -27,10 +27,10 @@ parser.add_argument('--end-time', type=float, help='End time (seconds)') parser.add_argument('-v', dest='verbose', action='store_true', help='Verbose mode') -silence_start_re = re.compile(' silence_start: (?P[0-9]+(\.?[0-9]*))$') -silence_end_re = re.compile(' silence_end: (?P[0-9]+(\.?[0-9]*)) ') +silence_start_re = re.compile(r' silence_start: (?P[0-9]+(\.?[0-9]*))$') +silence_end_re = re.compile(r' silence_end: (?P[0-9]+(\.?[0-9]*)) ') total_duration_re = re.compile( - 'size=[^ ]+ time=(?P[0-9]{2}):(?P[0-9]{2}):(?P[0-9\.]{5}) bitrate=') + r'size=[^ ]+ time=(?P[0-9]{2}):(?P[0-9]{2}):(?P[0-9\.]{5}) bitrate=') def _logged_popen(cmd_line, *args, **kwargs): diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index 5f25a347..f42d1d73 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -3,7 +3,6 @@ from ._utils import basestring, convert_kwargs_to_cmd_line_args from builtins import str from functools import reduce -import collections import copy import operator import subprocess @@ -18,6 +17,11 @@ output_operator, ) +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable + class Error(Exception): def __init__(self, cmd, stdout, stderr): @@ -136,9 +140,7 @@ def _get_output_args(node, stream_name_map): args += ['-b:a', str(kwargs.pop('audio_bitrate'))] if 'video_size' in kwargs: video_size = kwargs.pop('video_size') - if not isinstance(video_size, basestring) and isinstance( - video_size, collections.Iterable - ): + if not isinstance(video_size, basestring) and isinstance(video_size, Iterable): video_size = '{}x{}'.format(video_size[0], video_size[1]) args += ['-video_size', video_size] args += convert_kwargs_to_cmd_line_args(kwargs) diff --git a/ffmpeg/_utils.py b/ffmpeg/_utils.py index 94ef9d07..9baa2c78 100644 --- a/ffmpeg/_utils.py +++ b/ffmpeg/_utils.py @@ -3,13 +3,17 @@ from past.builtins import basestring import hashlib import sys -import collections if sys.version_info.major == 2: # noinspection PyUnresolvedReferences,PyShadowingBuiltins str = str +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable + # `past.builtins.basestring` module can't be imported on Python3 in some environments (Ubuntu). # This code is copy-pasted from it to avoid crashes. @@ -92,7 +96,7 @@ def convert_kwargs_to_cmd_line_args(kwargs): args = [] for k in sorted(kwargs.keys()): v = kwargs[k] - if isinstance(v, collections.Iterable) and not isinstance(v, str): + if isinstance(v, Iterable) and not isinstance(v, str): for value in v: args.append('-{}'.format(k)) if value is not None: diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index ba1fa361..8dbc271a 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -30,7 +30,7 @@ def test_escape_chars(): - assert ffmpeg._utils.escape_chars('a:b', ':') == 'a\:b' + assert ffmpeg._utils.escape_chars('a:b', ':') == r'a\:b' assert ffmpeg._utils.escape_chars('a\\:b', ':\\') == 'a\\\\\\:b' assert ( ffmpeg._utils.escape_chars('a:b,c[d]e%{}f\'g\'h\\i', '\\\':,[]%') diff --git a/setup.py b/setup.py index 743deb22..72f381cb 100644 --- a/setup.py +++ b/setup.py @@ -92,5 +92,9 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], ) diff --git a/tox.ini b/tox.ini index e317207f..98814078 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38, py39 +envlist = py27, py35, py36, py37, py38, py39, py310 [gh-actions] python = @@ -14,6 +14,7 @@ python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [testenv] commands = py.test -vv From fc41f4aa84084bfae6e2db6a5a1fe7949bb28bae Mon Sep 17 00:00:00 2001 From: lcjh <120989324@qq.com> Date: Mon, 7 Mar 2022 17:55:30 +0800 Subject: [PATCH 28/32] Fix `heigth` -> `height` typo (#596) Co-authored-by: Karl Kroening --- doc/html/index.html | 6 +++--- ffmpeg/_filters.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/html/index.html b/doc/html/index.html index eac8967c..55d31a94 100644 --- a/doc/html/index.html +++ b/doc/html/index.html @@ -168,7 +168,7 @@

ffmpeg-python: Python bindings for FFmpeg ffmpeg.compile(stream_spec, cmd='ffmpeg', overwrite_output=False)

Build command-line for invoking ffmpeg.

-

The run() function uses this to build the commnad line +

The run() function uses this to build the command line arguments and should work in most cases, but calling this function directly is useful for debugging or if you need to invoke ffmpeg manually for whatever reason.

@@ -340,7 +340,7 @@

ffmpeg-python: Python bindings for FFmpeg Date: Mon, 11 Jul 2022 22:39:36 +0200 Subject: [PATCH 29/32] Upgrade GitHub Actions (#643) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf65206d..bb9842ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,9 @@ jobs: - "3.9" - "3.10" steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install ffmpeg From ef00863269588f79031a56a17509198ded8b8da2 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 11 Jul 2022 13:51:06 -0700 Subject: [PATCH 30/32] Fix Black in GHA for Python 2.7 (#680) (At least until Python 2.7 support is finally eliminated) --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb9842ba..0bd614b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,8 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - - uses: psf/black@21.12b0 # TODO: upgrade after dropping Python 2 support. - with: - src: ffmpeg # TODO: also format `examples`. - version: 21.12b0 + - name: Black + run: | + # TODO: use standard `psf/black` action after dropping Python 2 support. + pip install black==21.12b0 click==8.0.2 # https://stackoverflow.com/questions/71673404 + black ffmpeg --check --color --diff From 35886c970c7b3a757115f5a7b6fd1753e64832ce Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 11 Jul 2022 23:02:31 +0200 Subject: [PATCH 31/32] Upgrade GitHub Actions again (#679) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bd614b7..90ae317c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,9 @@ jobs: - "3.9" - "3.10" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install ffmpeg @@ -37,7 +37,7 @@ jobs: black: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Black run: | # TODO: use standard `psf/black` action after dropping Python 2 support. From df129c7ba30aaa9ffffb81a48f53aa7253b0b4e6 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 11 Jul 2022 23:03:07 +0200 Subject: [PATCH 32/32] Let's implicitly fix a typo (#681) --- ffmpeg/dag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffmpeg/dag.py b/ffmpeg/dag.py index 3508dd49..4bdac44d 100644 --- a/ffmpeg/dag.py +++ b/ffmpeg/dag.py @@ -74,7 +74,7 @@ def incoming_edge_map(self): """Provides information about all incoming edges that connect to this node. The edge map is a dictionary that maps an ``incoming_label`` to - ``(outgoing_node, outgoing_label)``. Note that implicity, ``incoming_node`` is + ``(outgoing_node, outgoing_label)``. Note that implicitly, ``incoming_node`` is ``self``. See "Edges" section above. """ raise NotImplementedError()