diff --git a/examples/README.md b/examples/README.md index 8ceade8..4ec816e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -104,10 +104,10 @@ With additional filtering: ```python in1 = ffmpeg.input('in1.mp4') in2 = ffmpeg.input('in2.mp4') -v1 = in1['v'].hflip() -a1 = in1['a'] -v2 = in2['v'].filter('reverse').filter('hue', s=0) -a2 = in2['a'].filter('areverse').filter('aphaser') +v1 = in1.video.hflip() +a1 = in1.audio +v2 = in2.video.filter('reverse').filter('hue', s=0) +a2 = in2.audio.filter('areverse').filter('aphaser') joined = ffmpeg.concat(v1, a1, v2, a2, v=1, a=1).node v3 = joined[0] a3 = joined[1].filter('volume', 0.8) diff --git a/ffmpeg/__init__.py b/ffmpeg/__init__.py index 7e913d3..a88d344 100644 --- a/ffmpeg/__init__.py +++ b/ffmpeg/__init__.py @@ -1,10 +1,22 @@ from __future__ import unicode_literals - -from . import _filters, _ffmpeg, _run, _probe -from ._filters import * +from . import nodes +from . import _ffmpeg +from . import _filters +from . import _probe +from . import _run +from . import _view +from .nodes import * from ._ffmpeg import * +from ._filters import * +from ._probe import * from ._run import * from ._view import * -from ._probe import * -__all__ = _filters.__all__ + _ffmpeg.__all__ + _run.__all__ + _view.__all__ + _probe.__all__ +__all__ = ( + nodes.__all__ + + _ffmpeg.__all__ + + _probe.__all__ + + _run.__all__ + + _view.__all__ + + _filters.__all__ +) diff --git a/ffmpeg/_filters.py b/ffmpeg/_filters.py index 2cbdb36..cc550bf 100644 --- a/ffmpeg/_filters.py +++ b/ffmpeg/_filters.py @@ -8,7 +8,7 @@ from ._utils import escape_chars 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: diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py index ae50763..34d1e44 100644 --- a/ffmpeg/nodes.py +++ b/ffmpeg/nodes.py @@ -53,8 +53,8 @@ class Stream(object): Process the audio and video portions of a stream independently:: input = ffmpeg.input('in.mp4') - audio = input[:'a'].filter("aecho", 0.8, 0.9, 1000, 0.3) - video = input[:'v'].hflip() + audio = input['a'].filter("aecho", 0.8, 0.9, 1000, 0.3) + video = input['v'].hflip() out = ffmpeg.output(audio, video, 'out.mp4') """ if self.selector is not None: @@ -63,6 +63,56 @@ class Stream(object): raise TypeError("Expected string index (e.g. 'a'); got {!r}".format(index)) return self.node.stream(label=self.label, selector=index) + @property + def audio(self): + """Select the audio-portion of a stream. + + Some ffmpeg filters drop audio streams, and care must be taken + to preserve the audio in the final output. The ``.audio`` and + ``.video`` operators can be used to reference the audio/video + portions of a stream so that they can be processed separately + and then re-combined later in the pipeline. This dilemma is + intrinsic to ffmpeg, and ffmpeg-python tries to stay out of the + way while users may refer to the official ffmpeg documentation + as to why certain filters drop audio. + + ``stream.audio`` is a shorthand for ``stream['a']``. + + Example: + Process the audio and video portions of a stream independently:: + + input = ffmpeg.input('in.mp4') + audio = input.audio.filter("aecho", 0.8, 0.9, 1000, 0.3) + video = input.video.hflip() + out = ffmpeg.output(audio, video, 'out.mp4') + """ + return self['a'] + + @property + def video(self): + """Select the video-portion of a stream. + + Some ffmpeg filters drop audio streams, and care must be taken + to preserve the audio in the final output. The ``.audio`` and + ``.video`` operators can be used to reference the audio/video + portions of a stream so that they can be processed separately + and then re-combined later in the pipeline. This dilemma is + intrinsic to ffmpeg, and ffmpeg-python tries to stay out of the + way while users may refer to the official ffmpeg documentation + as to why certain filters drop audio. + + ``stream.video`` is a shorthand for ``stream['v']``. + + Example: + Process the audio and video portions of a stream independently:: + + input = ffmpeg.input('in.mp4') + audio = input.audio.filter("aecho", 0.8, 0.9, 1000, 0.3) + video = input.video.hflip() + out = ffmpeg.output(audio, video, 'out.mp4') + """ + return self['v'] + def get_stream_map(stream_spec): if stream_spec is None: @@ -286,3 +336,8 @@ def filter_operator(name=None): def output_operator(name=None): return stream_operator(stream_classes={OutputStream}, name=name) + + +__all__ = [ + 'Stream', +] diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index b9e5d3a..8bb3975 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -175,10 +175,15 @@ def test_combined_output(): ] -def test_filter_with_selector(): +@pytest.mark.parametrize('use_shorthand', [True, False]) +def test_filter_with_selector(use_shorthand): i = ffmpeg.input(TEST_INPUT_FILE1) - v1 = i['v'].hflip() - a1 = i['a'].filter('aecho', 0.8, 0.9, 1000, 0.3) + if use_shorthand: + v1 = i.video.hflip() + a1 = i.audio.filter('aecho', 0.8, 0.9, 1000, 0.3) + else: + v1 = i['v'].hflip() + a1 = i['a'].filter('aecho', 0.8, 0.9, 1000, 0.3) out = ffmpeg.output(a1, v1, TEST_OUTPUT_FILE1) assert out.get_args() == [ '-i', TEST_INPUT_FILE1, @@ -273,7 +278,7 @@ def test_filter_concat__audio_only(): def test_filter_concat__audio_video(): in1 = ffmpeg.input('in1.mp4') in2 = ffmpeg.input('in2.mp4') - joined = ffmpeg.concat(in1['v'], in1['a'], in2.hflip(), in2['a'], v=1, a=1).node + joined = ffmpeg.concat(in1.video, in1.audio, in2.hflip(), in2['a'], v=1, a=1).node args = ( ffmpeg .output(joined[0], joined[1], 'out.mp4') @@ -298,7 +303,7 @@ def test_filter_concat__wrong_stream_count(): in1 = ffmpeg.input('in1.mp4') in2 = ffmpeg.input('in2.mp4') with pytest.raises(ValueError) as excinfo: - ffmpeg.concat(in1['v'], in1['a'], in2.hflip(), v=1, a=1).node + ffmpeg.concat(in1.video, in1.audio, in2.hflip(), v=1, a=1).node assert str(excinfo.value) == \ 'Expected concat input streams to have length multiple of 2 (v=1, a=1); got 3' diff --git a/requirements-base.txt b/requirements-base.txt deleted file mode 100644 index 6afd152..0000000 --- a/requirements-base.txt +++ /dev/null @@ -1,6 +0,0 @@ -future -pytest -pytest-mock -pytest-runner -sphinx -tox diff --git a/requirements.txt b/requirements.txt index dfec4a1..bf565e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,39 @@ -alabaster==0.7.10 -apipkg==1.4 -Babel==2.5.1 -certifi==2017.7.27.1 +alabaster==0.7.12 +atomicwrites==1.3.0 +attrs==19.1.0 +Babel==2.7.0 +certifi==2019.3.9 chardet==3.0.4 docutils==0.14 -execnet==1.5.0 -funcsigs==1.0.2 -future==0.16.0 -idna==2.6 -imagesize==0.7.1 -Jinja2==2.9.6 -MarkupSafe==1.0 -mock==2.0.0 -pbr==4.0.3 -pluggy==0.5.2 -py==1.4.34 -Pygments==2.2.0 -pytest==3.2.3 -pytest-forked==0.2 -pytest-mock==1.10.0 -pytest-runner==3.0 -pytest-xdist==1.22.2 -pytz==2017.3 -requests==2.18.4 -six==1.11.0 +filelock==3.0.12 +future==0.17.1 +idna==2.8 +imagesize==1.1.0 +importlib-metadata==0.17 +Jinja2==2.10.1 +MarkupSafe==1.1.1 +more-itertools==7.0.0 +packaging==19.0 +pluggy==0.12.0 +py==1.8.0 +Pygments==2.4.2 +pyparsing==2.4.0 +pytest==4.6.1 +pytest-mock==1.10.4 +pytz==2019.1 +requests==2.22.0 +six==1.12.0 snowballstemmer==1.2.1 -Sphinx==1.6.5 -sphinxcontrib-websupport==1.0.1 -tox==2.9.1 -typing==3.6.2 -urllib3==1.22 -virtualenv==15.1.0 +Sphinx==2.1.0 +sphinxcontrib-applehelp==1.0.1 +sphinxcontrib-devhelp==1.0.1 +sphinxcontrib-htmlhelp==1.0.2 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.2 +sphinxcontrib-serializinghtml==1.1.3 +toml==0.10.0 +tox==3.12.1 +urllib3==1.25.3 +virtualenv==16.6.0 +wcwidth==0.1.7 +zipp==0.5.1 diff --git a/setup.py b/setup.py index 367b74e..8d54b9d 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,15 @@ setup( keywords=keywords, long_description=long_description, install_requires=['future'], + extras_require={ + 'dev': [ + 'future==0.17.1', + 'pytest-mock==1.10.4', + 'pytest==4.6.1', + 'Sphinx==2.1.0', + 'tox==3.12.1', + ], + }, classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License',