diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cf65206 --- /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" + 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 e4d7d75..0000000 --- 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 8cfe89a..b8ee922 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 @@ -78,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 ``` @@ -90,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. @@ -194,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?** diff --git a/examples/split_silence.py b/examples/split_silence.py index a889db1..90b46d9 100755 --- a/examples/split_silence.py +++ b/examples/split_silence.py @@ -27,10 +27,10 @@ parser.add_argument('--start-time', type=float, help='Start time (seconds)') 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/_ffmpeg.py b/ffmpeg/_ffmpeg.py index 31e2b90..007624b 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 c5a6ae9..5bca23d 100644 --- a/ffmpeg/_filters.py +++ b/ffmpeg/_filters.py @@ -8,9 +8,11 @@ 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: + 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. - height: 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. + height: 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. + 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``. + 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 eadb90a..f42d1d7 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -3,7 +3,6 @@ from .dag import get_outgoing_edges, topo_sort 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 @@ from .nodes import ( output_operator, ) +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable + class Error(Exception): def __init__(self, cmd, stdout, stderr): @@ -88,8 +92,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 ) ) @@ -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) @@ -199,7 +201,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 +288,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 +305,7 @@ def run( input=None, quiet=False, overwrite_output=False, - cwd=None + cwd=None, ): """Invoke ffmpeg for the supplied node graph. @@ -324,7 +329,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 +338,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 92d7611..9baa2c7 100644 --- a/ffmpeg/_utils.py +++ b/ffmpeg/_utils.py @@ -3,13 +3,17 @@ from builtins import str 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. @@ -40,7 +44,6 @@ if sys.version_info.major >= 3: class basestring(with_metaclass(BaseBaseString)): pass - else: # noinspection PyUnresolvedReferences,PyCompatibility from builtins import basestring @@ -49,8 +52,8 @@ else: 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) @@ -93,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/_view.py b/ffmpeg/_view.py index fb129fa..31955af 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 9564d7f..3508dd4 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 @@ class DagNode(object): 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 @@ class DagNode(object): 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 cacab8e..e8b2838 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 @@ class Node(KwargReprNode): 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 4a8183c..8dbc271 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -30,7 +30,7 @@ subprocess.check_call(['ffmpeg', '-version']) 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', '\\\':,[]%') @@ -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 test_filter_normal_arg_escape(): 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 test_filter_normal_arg_escape(): 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 0000000..de71e58 --- /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 + )/ +) +''' diff --git a/setup.py b/setup.py index 0282c67..72f381c 100644 --- a/setup.py +++ b/setup.py @@ -60,8 +60,6 @@ keywords = misc_keywords + file_formats 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', @@ -94,5 +92,9 @@ setup( '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 1e3ba53..9881407 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,17 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36, py37, pypy +envlist = py27, py35, py36, py37, py38, py39, py310 + +[gh-actions] +python = + 2.7: py27 + 3.5: py35 + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 [testenv] commands = py.test -vv