pax_global_header00006660000000000000000000000064147422754230014524gustar00rootroot0000000000000052 comment=ae47d8028c237ca5507ceef1b843ee427b442887 imageio-ffmpeg-0.6.0/000077500000000000000000000000001474227542300144035ustar00rootroot00000000000000imageio-ffmpeg-0.6.0/.github/000077500000000000000000000000001474227542300157435ustar00rootroot00000000000000imageio-ffmpeg-0.6.0/.github/FUNDING.yml000066400000000000000000000000621474227542300175560ustar00rootroot00000000000000github: "imageio" tidelift: "pypi/imageio-ffmpeg" imageio-ffmpeg-0.6.0/.github/workflows/000077500000000000000000000000001474227542300200005ustar00rootroot00000000000000imageio-ffmpeg-0.6.0/.github/workflows/ci.yml000066400000000000000000000044571474227542300211300ustar00rootroot00000000000000name: CI on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] jobs: lint-build: name: Linting runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip pip install invoke black flake8 - name: Lint run: | invoke lint invoke checkformat test-builds: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: # Python versions - name: Linux py39 os: ubuntu-latest pyversion: '3.9' - name: Linux py310 os: ubuntu-latest pyversion: '3.10' - name: Linux py311 os: ubuntu-latest pyversion: '3.11' - name: Linux py312 os: ubuntu-latest pyversion: '3.12' - name: Linux py313 os: ubuntu-latest pyversion: '3.13' - name: Linux pypy os: ubuntu-latest pyversion: 'pypy3.9' # OS's - name: Linux py313 os: ubuntu-latest pyversion: '3.13' - name: Windows py313 os: windows-latest pyversion: '3.13' - name: MacOS py313 os: macos-latest pyversion: '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pyversion }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.pyversion }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install psutil pip install invoke pytest pytest-cov invoke get-ffmpeg-binary pip install . rm -r ./imageio_ffmpeg - name: Test with pytest run: | python -c "import sys; print(sys.version, '\n', sys.prefix)"; python -c 'import imageio_ffmpeg; print(imageio_ffmpeg.get_ffmpeg_version())' pytest tests -v --cov=imageio_ffmpeg --cov-report=term imageio-ffmpeg-0.6.0/.gitignore000066400000000000000000000023721474227542300163770ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class imageio_ffmpeg/binaries/*ffmpeg* imageio_ffmpeg/binaries/*ffprobe* # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ imageio-ffmpeg-0.6.0/LICENSE000066400000000000000000000024451474227542300154150ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2019-2025, imageio All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. imageio-ffmpeg-0.6.0/MANIFEST.in000066400000000000000000000001061474227542300161360ustar00rootroot00000000000000include LICENSE include README.md include imageio_ffmpeg/binaries/*.* imageio-ffmpeg-0.6.0/README.md000066400000000000000000000236571474227542300156770ustar00rootroot00000000000000# imageio-ffmpeg [![Build Status](https://github.com/imageio/imageio-ffmpeg/workflows/CI/badge.svg)](https://github.com/imageio/imageio-ffmpeg/actions) [![PyPI Version](https://img.shields.io/pypi/v/imageio-ffmpeg.svg)](https://pypi.python.org/pypi/imageio-ffmpeg/) FFMPEG wrapper for Python ## Purpose The purpose of this project is to provide a simple and reliable ffmpeg wrapper for working with video files. It implements two simple generator functions for reading and writing data from/to ffmpeg, which reliably terminate the ffmpeg process when done. It also takes care of publishing platform-specific wheels that include the binary ffmpeg executables. This library is used as the basis for the [imageio](https://github.com/imageio/imageio) [ffmpeg plugin](https://imageio.readthedocs.io/en/stable/format_ffmpeg.html), but it can also be used by itself. Imageio provides a higher level API, and adds support for e.g. cameras and seeking. This library was created before [PyAV](https://github.com/PyAV-Org/PyAV) was a thing. But now they have binary wheels for many platforms. You should probably use `PyAV` instead; it is faster and offers more features. ## Installation This library works with any version of Python 3.7+ (including Pypy). There are no further dependencies. The wheels on Pypi include the ffmpeg executable for all common platforms (Windows 7+, Linux kernel 2.6.32+, OSX 10.9+). Install using: ``` $ pip install --upgrade imageio-ffmpeg ``` (On Linux you may want to first `pip install -U pip`, since pip 19 is needed to detect the `manylinux2010` wheels.) If you're using a Conda environment: the conda package does not include the ffmpeg executable, but instead depends on the `ffmpeg` package from `conda-forge`. Install using: ``` $ conda install imageio-ffmpeg -c conda-forge ``` If you don't want to install the included ffmpeg, you can use pip with `--no-binary` or conda with `--no-deps`. Then use the `IMAGEIO_FFMPEG_EXE` environment variable if needed. ## Example usage The `imageio_ffmpeg` library provides low level functionality to read and write video data, using Python generators: ```py # Read a video file reader = read_frames(path) meta = reader.__next__() # meta data, e.g. meta["size"] -> (width, height) for frame in reader: ... # each frame is a bytes object # Write a video file writer = write_frames(path, size) # size is (width, height) writer.send(None) # seed the generator for frame in frames: writer.send(frame) writer.close() # don't forget this ``` (Also see the API section further down.) ## How it works This library calls ffmpeg in a subprocess, and video frames are communicated over pipes. This is certainly not the fastest way to use ffmpeg, but it makes it possible to wrap ffmpeg with pure Python, making distribution and installation *much* easier. And probably the code itself too. In contrast, [PyAV](https://github.com/mikeboers/PyAV) wraps ffmpeg at the C level. Note that because of how `imageio-ffmpeg` works, `read_frames()` and `write_frames()` only accept file names, and not file (like) objects. ## imageio-ffmpeg for enterprise Available as part of the Tidelift Subscription The maintainers of imageio-ffmpeg and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/pypi-imageio-ffmpeg?utm_source=pypi-imageio-ffmpeg&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ## Environment variables The library can be configured at runtime by setting the following environment variables: * `IMAGEIO_FFMPEG_EXE=[file name]` -- override the ffmpeg executable; * `IMAGEIO_FFMPEG_NO_PREVENT_SIGINT=1` -- don't prevent propagation of SIGINT to the ffmpeg process. ## Developers Dev deps: ``` pip install invoke black flake8 ``` We use invoke: ``` invoke autoformat invoke lint invoke -l # to get a list of all tasks invoke update-readme # after changes to the docstrings ``` ## API ```py def read_frames( path, pix_fmt="rgb24", bpp=None, input_params=None, output_params=None, bits_per_pixel=None, ): """ Create a generator to iterate over the frames in a video file. It first yields a small metadata dictionary that contains: * ffmpeg_version: the ffmpeg version in use (as a string). * codec: a hint about the codec used to encode the video, e.g. "h264". * source_size: the width and height of the encoded video frames. * size: the width and height of the frames that will be produced. * fps: the frames per second. Can be zero if it could not be detected. * duration: duration in seconds. Can be zero if it could not be detected. After that, it yields frames until the end of the video is reached. Each frame is a bytes object. This function makes no assumptions about the number of frames in the data. For one because this is hard to predict exactly, but also because it may depend on the provided output_params. If you want to know the number of frames in a video file, use count_frames_and_secs(). It is also possible to estimate the number of frames from the fps and duration, but note that even if both numbers are present, the resulting value is not always correct. Example: gen = read_frames(path) meta = gen.__next__() for frame in gen: print(len(frame)) Parameters: path (str): the filename of the file to read from. pix_fmt (str): the pixel format of the frames to be read. The default is "rgb24" (frames are uint8 RGB images). input_params (list): Additional ffmpeg input command line parameters. output_params (list): Additional ffmpeg output command line parameters. bits_per_pixel (int): The number of bits per pixel in the output frames. This depends on the given pix_fmt. Default is 24 (RGB) bpp (int): DEPRECATED, USE bits_per_pixel INSTEAD. The number of bytes per pixel in the output frames. This depends on the given pix_fmt. Some pixel formats like yuv420p have 12 bits per pixel and cannot be set in bytes as integer. For this reason the bpp argument is deprecated. """ ``` ```py def write_frames( path, size, pix_fmt_in="rgb24", pix_fmt_out="yuv420p", fps=16, quality=5, bitrate=None, codec=None, macro_block_size=16, ffmpeg_log_level="warning", ffmpeg_timeout=None, input_params=None, output_params=None, audio_path=None, audio_codec=None, ): """ Create a generator to write frames (bytes objects) into a video file. The frames are written by using the generator's `send()` method. Frames can be anything that can be written to a file. Typically these are bytes objects, but c-contiguous Numpy arrays also work. Example: gen = write_frames(path, size) gen.send(None) # seed the generator for frame in frames: gen.send(frame) gen.close() # don't forget this Parameters: path (str): the filename to write to. size (tuple): the width and height of the frames. pix_fmt_in (str): the pixel format of incoming frames. E.g. "gray", "gray8a", "rgb24", or "rgba". Default "rgb24". pix_fmt_out (str): the pixel format to store frames. Default yuv420p". fps (float): The frames per second. Default 16. quality (float): A measure for quality between 0 and 10. Default 5. Ignored if bitrate is given. bitrate (str): The bitrate, e.g. "192k". The defaults are pretty good. codec (str): The codec. Default "libx264" for .mp4 (if available from the ffmpeg executable) or "msmpeg4" for .wmv. macro_block_size (int): You probably want to align the size of frames to this value to avoid image resizing. Default 16. Can be set to 1 to avoid block alignment, though this is not recommended. ffmpeg_log_level (str): The ffmpeg logging level. Default "warning". ffmpeg_timeout (float): Timeout in seconds to wait for ffmpeg process to finish. Value of 0 or None will wait forever (default). The time that ffmpeg needs depends on CPU speed, compression, and frame size. input_params (list): Additional ffmpeg input command line parameters. output_params (list): Additional ffmpeg output command line parameters. audio_path (str): A input file path for encoding with an audio stream. Default None, no audio. audio_codec (str): The audio codec to use if audio_path is provided. "copy" will try to use audio_path's audio codec without re-encoding. Default None, but some formats must have certain codecs specified. """ ``` ```py def count_frames_and_secs(path): """ Get the number of frames and number of seconds for the given video file. Note that this operation can be quite slow for large files. Disclaimer: I've seen this produce different results from actually reading the frames with older versions of ffmpeg (2.x). Therefore I cannot say with 100% certainty that the returned values are always exact. """ ``` ```py def get_ffmpeg_exe(): """ Get the ffmpeg executable file. This can be the binary defined by the IMAGEIO_FFMPEG_EXE environment variable, the binary distributed with imageio-ffmpeg, an ffmpeg binary installed with conda, or the system ffmpeg (in that order). A RuntimeError is raised if no valid ffmpeg could be found. """ ``` ```py def get_ffmpeg_version(): """ Get the version of the used ffmpeg executable (as a string). """ ``` imageio-ffmpeg-0.6.0/imageio_ffmpeg/000077500000000000000000000000001474227542300173415ustar00rootroot00000000000000imageio-ffmpeg-0.6.0/imageio_ffmpeg/__init__.py000066400000000000000000000003431474227542300214520ustar00rootroot00000000000000""" imageio_ffmpeg, FFMPEG wrapper for Python. """ # flake8: noqa from ._definitions import __version__ from ._io import count_frames_and_secs, read_frames, write_frames from ._utils import get_ffmpeg_exe, get_ffmpeg_version imageio-ffmpeg-0.6.0/imageio_ffmpeg/_definitions.py000066400000000000000000000037711474227542300223750ustar00rootroot00000000000000import sys import platform __version__ = "0.6.0" def get_platform(): # get_os_string and get_arch are taken from wgpu-py return _get_os_string() + "-" + _get_arch() def _get_os_string(): if sys.platform.startswith("win"): return "windows" elif sys.platform.startswith("darwin"): return "macos" elif sys.platform.startswith("linux"): return "linux" else: return sys.platform def _get_arch(): # See e.g.: https://stackoverflow.com/questions/45124888 is_64_bit = sys.maxsize > 2**32 machine = platform.machine() if machine == "armv7l": # Raspberry pi detected_arch = "armv7" elif is_64_bit and machine.startswith(("arm", "aarch64")): # Includes MacOS M1, arm linux, ... detected_arch = "aarch64" elif is_64_bit: detected_arch = "x86_64" else: detected_arch = "i686" return detected_arch # The Linux static builds (https://johnvansickle.com/ffmpeg/) are build # for Linux kernels 3.2.0 and up (at the time of writing, ffmpeg v7.0.2). # This corresponds to Ubuntu 12.04 / Debian 7. I'm not entirely sure' # what manylinux matches that, but I think manylinux2014 should be safe. # Platform string -> ffmpeg filename FNAME_PER_PLATFORM = { "macos-aarch64": "ffmpeg-macos-aarch64-v7.1", "macos-x86_64": "ffmpeg-macos-x86_64-v7.1", # 10.9+ "windows-x86_64": "ffmpeg-win-x86_64-v7.1.exe", "windows-i686": "ffmpeg-win32-v4.2.2.exe", # Windows 7+ "linux-aarch64": "ffmpeg-linux-aarch64-v7.0.2", # Kernel 3.2.0+ "linux-x86_64": "ffmpeg-linux-x86_64-v7.0.2", } osxplats = "macosx_10_9_intel.macosx_10_9_x86_64" osxarmplats = "macosx_11_0_arm64" # Wheel tag -> platform string WHEEL_BUILDS = { "py3-none-manylinux2014_x86_64": "linux-x86_64", "py3-none-manylinux2014_aarch64": "linux-aarch64", "py3-none-" + osxplats: "macos-x86_64", "py3-none-" + osxarmplats: "macos-aarch64", "py3-none-win32": "windows-i686", "py3-none-win_amd64": "windows-x86_64", } imageio-ffmpeg-0.6.0/imageio_ffmpeg/_io.py000066400000000000000000000646501474227542300204740ustar00rootroot00000000000000import pathlib import subprocess import sys import time from collections import defaultdict from functools import lru_cache from ._parsing import LogCatcher, cvsecs, parse_ffmpeg_header from ._utils import _popen_kwargs, get_ffmpeg_exe, logger ISWIN = sys.platform.startswith("win") h264_encoder_preference = defaultdict(lambda: -1) # The libx264 was the default encoder for a longe time with imageio h264_encoder_preference["libx264"] = 100 # Encoder with the nvidia graphics card dedicated hardware h264_encoder_preference["h264_nvenc"] = 90 # Deprecated names for the same encoder h264_encoder_preference["nvenc_h264"] = 90 h264_encoder_preference["nvenc"] = 90 # vaapi provides hardware encoding with intel integrated graphics chipsets h264_encoder_preference["h264_vaapi"] = 80 # openh264 is cisco's open source encoder h264_encoder_preference["libopenh264"] = 70 h264_encoder_preference["libx264rgb"] = 50 def ffmpeg_test_encoder(encoder): # Use the null streams to validate if we can encode anything # https://trac.ffmpeg.org/wiki/Null cmd = [ get_ffmpeg_exe(), "-hide_banner", "-f", "lavfi", "-i", "nullsrc=s=256x256:d=8", "-vcodec", encoder, "-f", "null", "-", ] p = subprocess.run( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) return p.returncode == 0 def get_compiled_h264_encoders(): cmd = [get_ffmpeg_exe(), "-hide_banner", "-encoders"] p = subprocess.run( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) stdout = p.stdout.decode().replace("\r", "") # 2022/04/08: hmaarrfk # I couldn't find a good way to get the list of available encoders from # the ffmpeg command # The ffmpeg command return a table that looks like # Notice the leading space at the very beginning # On ubuntu with libffmpeg-nvenc-dev we get # $ ffmpeg -hide_banner -encoders | grep -i h.264 # # Encoders: # V..... = Video # A..... = Audio # S..... = Subtitle # .F.... = Frame-level multithreading # ..S... = Slice-level multithreading # ...X.. = Codec is experimental # ....B. = Supports draw_horiz_band # .....D = Supports direct rendering method 1 # ------ # V..... libx264 libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264) # V..... libx264rgb libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 RGB (codec h264) # V....D h264_nvenc NVIDIA NVENC H.264 encoder (codec h264) # V..... h264_omx OpenMAX IL H.264 video encoder (codec h264) # V..... h264_qsv H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video acceleration) (codec h264) # V..... h264_v4l2m2m V4L2 mem2mem H.264 encoder wrapper (codec h264) # V....D h264_vaapi H.264/AVC (VAAPI) (codec h264) # V..... nvenc NVIDIA NVENC H.264 encoder (codec h264) # V..... nvenc_h264 NVIDIA NVENC H.264 encoder (codec h264) # # However, just because ffmpeg was compiled with the options enabled # it doesn't mean that it will be successful header_footer = stdout.split("------") footer = header_footer[1].strip("\n") encoders = [] for line in footer.split("\n"): # Strip to remove any leading spaces line = line.strip() encoder = line.split(" ")[1] if encoder in h264_encoder_preference: # These encoders are known to support H.264 # We forcibly include them in case their description changes to # not include the string "H.264" encoders.append(encoder) elif (line[0] == "V") and ("H.264" in line): encoders.append(encoder) encoders.sort(reverse=True, key=lambda x: h264_encoder_preference[x]) if "h264_nvenc" in encoders: # Remove deprecated names for the same encoder for encoder in ["nvenc", "nvenc_h264"]: if encoder in encoders: encoders.remove(encoder) # Return an immutable tuple to avoid users corrupting the lru_cache return tuple(encoders) @lru_cache() def get_first_available_h264_encoder(): compiled_encoders = get_compiled_h264_encoders() for encoder in compiled_encoders: if ffmpeg_test_encoder(encoder): return encoder else: raise RuntimeError( "No valid H.264 encoder was found with the ffmpeg installation" ) def count_frames_and_secs(path): """ Get the number of frames and number of seconds for the given video file. Note that this operation can be quite slow for large files. Disclaimer: I've seen this produce different results from actually reading the frames with older versions of ffmpeg (2.x). Therefore I cannot say with 100% certainty that the returned values are always exact. """ # https://stackoverflow.com/questions/2017843/fetch-frame-count-with-ffmpeg if isinstance(path, pathlib.PurePath): path = str(path) if not isinstance(path, str): raise TypeError("Video path must be a string or pathlib.Path.") cmd = [ get_ffmpeg_exe(), "-i", path, "-map", "0:v:0", "-vf", "null", "-f", "null", "-", ] try: out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, **_popen_kwargs()) except subprocess.CalledProcessError as err: out = err.output.decode(errors="ignore") raise RuntimeError( "FFMPEG call failed with {}:\n{}".format(err.returncode, out) ) # Note that other than with the subprocess calls below, ffmpeg wont hang here. # Worst case Python will stop/crash and ffmpeg will continue running until done. nframes = nsecs = None for line in reversed(out.splitlines()): if line.startswith(b"frame="): line = line.decode(errors="ignore") i = line.find("frame=") if i >= 0: s = line[i:].split("=", 1)[-1].lstrip().split(" ", 1)[0].strip() nframes = int(s) i = line.find("time=") if i >= 0: s = line[i:].split("=", 1)[-1].lstrip().split(" ", 1)[0].strip() nsecs = cvsecs(*s.split(":")) return nframes, nsecs raise RuntimeError("Could not get number of frames") # pragma: no cover def read_frames( path, pix_fmt="rgb24", bpp=None, input_params=None, output_params=None, bits_per_pixel=None, ): """ Create a generator to iterate over the frames in a video file. It first yields a small metadata dictionary that contains: * ffmpeg_version: the ffmpeg version in use (as a string). * codec: a hint about the codec used to encode the video, e.g. "h264". * source_size: the width and height of the encoded video frames. * size: the width and height of the frames that will be produced. * fps: the frames per second. Can be zero if it could not be detected. * duration: duration in seconds. Can be zero if it could not be detected. After that, it yields frames until the end of the video is reached. Each frame is a bytes object. This function makes no assumptions about the number of frames in the data. For one because this is hard to predict exactly, but also because it may depend on the provided output_params. If you want to know the number of frames in a video file, use count_frames_and_secs(). It is also possible to estimate the number of frames from the fps and duration, but note that even if both numbers are present, the resulting value is not always correct. Example: gen = read_frames(path) meta = gen.__next__() for frame in gen: print(len(frame)) Parameters: path (str): the filename of the file to read from. pix_fmt (str): the pixel format of the frames to be read. The default is "rgb24" (frames are uint8 RGB images). input_params (list): Additional ffmpeg input command line parameters. output_params (list): Additional ffmpeg output command line parameters. bits_per_pixel (int): The number of bits per pixel in the output frames. This depends on the given pix_fmt. Default is 24 (RGB) bpp (int): DEPRECATED, USE bits_per_pixel INSTEAD. The number of bytes per pixel in the output frames. This depends on the given pix_fmt. Some pixel formats like yuv420p have 12 bits per pixel and cannot be set in bytes as integer. For this reason the bpp argument is deprecated. """ # ----- Input args if isinstance(path, pathlib.PurePath): path = str(path) if not isinstance(path, str): raise TypeError("Video path must be a string or pathlib.Path.") # Note: Dont check whether it exists. The source could be e.g. a camera. pix_fmt = pix_fmt or "rgb24" bpp = bpp or 3 bits_per_pixel = bits_per_pixel or bpp * 8 input_params = input_params or [] output_params = output_params or [] assert isinstance(pix_fmt, str), "pix_fmt must be a string" assert isinstance(bits_per_pixel, int), "bpp and bits_per_pixel must be an int" assert isinstance(input_params, list), "input_params must be a list" assert isinstance(output_params, list), "output_params must be a list" # ----- Prepare pre_output_params = ["-pix_fmt", pix_fmt, "-vcodec", "rawvideo", "-f", "image2pipe"] cmd = [get_ffmpeg_exe()] cmd += input_params + ["-i", path] cmd += pre_output_params + output_params + ["-"] process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **_popen_kwargs(prevent_sigint=True) ) log_catcher = LogCatcher(process.stderr) # Init policy by which to terminate ffmpeg. May be set to "kill" later. stop_policy = "timeout" # not wait; ffmpeg should be able to quit quickly # Enter try block directly after opening the process. # We terminate ffmpeg in the finally clause. # Generators are automatically closed when they get deleted, # so the finally block is guaranteed to run. try: # ----- Load meta data # Wait for the log catcher to get the meta information etime = time.time() + 10.0 while log_catcher.is_alive() and not log_catcher.header and time.time() < etime: time.sleep(0.01) # Check whether we have the information if not log_catcher.header: err2 = log_catcher.get_text(0.2) fmt = "Could not load meta information\n=== stderr ===\n{}" raise IOError(fmt.format(err2)) elif "No such file or directory" in log_catcher.header: raise IOError("{} not found! Wrong path?".format(path)) meta = parse_ffmpeg_header(log_catcher.header) yield meta # ----- Read frames width, height = meta["size"] framesize_bits = width * height * bits_per_pixel framesize_bytes = framesize_bits / 8 assert ( framesize_bytes.is_integer() ), "incorrect bits_per_pixel, framesize in bytes must be an int" framesize_bytes = int(framesize_bytes) framenr = 0 while True: framenr += 1 try: bb = bytes() while len(bb) < framesize_bytes: extra_bytes = process.stdout.read(framesize_bytes - len(bb)) if not extra_bytes: if len(bb) == 0: return else: raise RuntimeError( "End of file reached before full frame could be read." ) bb += extra_bytes yield bb except Exception as err: err1 = str(err) err2 = log_catcher.get_text(0.4) fmt = "Could not read frame {}:\n{}\n=== stderr ===\n{}" raise RuntimeError(fmt.format(framenr, err1, err2)) except GeneratorExit: # Note that GeneratorExit does not inherit from Exception but BaseException pass except Exception: # Normal exceptions fall through raise except BaseException: # Detect KeyboardInterrupt / SystemExit: don't wait for ffmpeg to quit stop_policy = "kill" raise finally: # Stop the LogCatcher thread, which reads from stderr. log_catcher.stop_me() # Make sure that ffmpeg is terminated. if process.poll() is None: # Ask ffmpeg to quit try: # I read somewhere that modern ffmpeg on Linux prefers a # "ctrl-c", but tests so far suggests sending q is more robust. # > p.send_signal(signal.SIGINT) # Sending q via communicate works, but can hang (see #17) # > p.communicate(b"q") # So let's do similar to what communicate does, but without # reading stdout (which may block). It looks like only closing # stdout is enough (tried Windows+Linux), but let's play safe. # Found that writing to stdin can cause "Invalid argument" on # Windows # and "Broken Pipe" on Unix. # p.stdin.write(b"q") # commented out in v0.4.1 process.stdout.close() process.stdin.close() # p.stderr.close() -> not here, the log_catcher closes it except Exception as err: # pragma: no cover logger.warning("Error while attempting stop ffmpeg (r): " + str(err)) if stop_policy == "timeout": # Wait until timeout, produce a warning and kill if it still exists try: etime = time.time() + 1.5 while time.time() < etime and process.poll() is None: time.sleep(0.01) finally: if process.poll() is None: # pragma: no cover logger.warning("We had to kill ffmpeg to stop it.") process.kill() else: # stop_policy == "kill" # Just kill it process.kill() def write_frames( path, size, pix_fmt_in="rgb24", pix_fmt_out="yuv420p", fps=16, quality=5, bitrate=None, codec=None, macro_block_size=16, ffmpeg_log_level="warning", ffmpeg_timeout=None, input_params=None, output_params=None, audio_path=None, audio_codec=None, ): """ Create a generator to write frames (bytes objects) into a video file. The frames are written by using the generator's `send()` method. Frames can be anything that can be written to a file. Typically these are bytes objects, but c-contiguous Numpy arrays also work. Example: gen = write_frames(path, size) gen.send(None) # seed the generator for frame in frames: gen.send(frame) gen.close() # don't forget this Parameters: path (str): the filename to write to. size (tuple): the width and height of the frames. pix_fmt_in (str): the pixel format of incoming frames. E.g. "gray", "gray8a", "rgb24", or "rgba". Default "rgb24". pix_fmt_out (str): the pixel format to store frames. Default yuv420p". fps (float): The frames per second. Default 16. quality (float): A measure for quality between 0 and 10. Default 5. Ignored if bitrate is given. bitrate (str): The bitrate, e.g. "192k". The defaults are pretty good. codec (str): The codec. Default "libx264" for .mp4 (if available from the ffmpeg executable) or "msmpeg4" for .wmv. macro_block_size (int): You probably want to align the size of frames to this value to avoid image resizing. Default 16. Can be set to 1 to avoid block alignment, though this is not recommended. ffmpeg_log_level (str): The ffmpeg logging level. Default "warning". ffmpeg_timeout (float): Timeout in seconds to wait for ffmpeg process to finish. Value of 0 or None will wait forever (default). The time that ffmpeg needs depends on CPU speed, compression, and frame size. input_params (list): Additional ffmpeg input command line parameters. output_params (list): Additional ffmpeg output command line parameters. audio_path (str): A input file path for encoding with an audio stream. Default None, no audio. audio_codec (str): The audio codec to use if audio_path is provided. "copy" will try to use audio_path's audio codec without re-encoding. Default None, but some formats must have certain codecs specified. """ # ----- Input args if isinstance(path, pathlib.PurePath): path = str(path) if not isinstance(path, str): raise TypeError("Video path must be a string or pathlib.Path.") # The pix_fmt_out yuv420p is the best for the outpur to work in # QuickTime and most other players. These players only support # the YUV planar color space with 4:2:0 chroma subsampling for # H.264 video. Otherwise, depending on the source, ffmpeg may # output to a pixel format that may be incompatible with these # players. See https://trac.ffmpeg.org/wiki/Encode/H.264#Encodingfordumbplayers pix_fmt_in = pix_fmt_in or "rgb24" pix_fmt_out = pix_fmt_out or "yuv420p" fps = fps or 16 # bitrate, codec, macro_block_size can all be None or ... macro_block_size = macro_block_size or 16 ffmpeg_log_level = ffmpeg_log_level or "warning" input_params = input_params or [] output_params = output_params or [] ffmpeg_timeout = ffmpeg_timeout or 0 floatish = float, int if isinstance(size, (tuple, list)): assert len(size) == 2, "size must be a 2-tuple" assert isinstance(size[0], int) and isinstance( size[1], int ), "size must be ints" sizestr = "{:d}x{:d}".format(*size) # elif isinstance(size, str): # assert "x" in size, "size as string must have format NxM" # sizestr = size else: assert False, "size must be str or tuple" assert isinstance(pix_fmt_in, str), "pix_fmt_in must be str" assert isinstance(pix_fmt_out, str), "pix_fmt_out must be str" assert isinstance(fps, floatish), "fps must be float" if quality is not None: assert isinstance(quality, floatish), "quality must be float" assert 1 <= quality <= 10, "quality must be between 1 and 10 inclusive" assert isinstance(macro_block_size, int), "macro_block_size must be int" assert isinstance(ffmpeg_log_level, str), "ffmpeg_log_level must be str" assert isinstance(ffmpeg_timeout, floatish), "ffmpeg_timeout must be float" assert isinstance(input_params, list), "input_params must be a list" assert isinstance(output_params, list), "output_params must be a list" # ----- Prepare # Get parameters if not codec: if path.lower().endswith(".wmv"): # This is a safer default codec on windows to get videos that # will play in powerpoint and other apps. H264 is not always # available on windows. codec = "msmpeg4" else: codec = get_first_available_h264_encoder() audio_params = ["-an"] if audio_path is not None and not path.lower().endswith(".gif"): audio_params = ["-i", audio_path] if audio_codec is not None: output_params += ["-acodec", audio_codec] output_params += ["-map", "0:v:0", "-map", "1:a:0"] # Get command cmd = [ get_ffmpeg_exe(), "-y", "-f", "rawvideo", "-vcodec", "rawvideo", "-s", sizestr, ] cmd += ["-pix_fmt", pix_fmt_in, "-r", "{:.02f}".format(fps)] + input_params cmd += ["-i", "-"] + audio_params cmd += ["-vcodec", codec, "-pix_fmt", pix_fmt_out] # Add fixed bitrate or variable bitrate compression flags if bitrate is not None: cmd += ["-b:v", str(bitrate)] elif quality is not None: # If None, then we don't add anything quality = 1 - quality / 10.0 if codec == "libx264": # crf ranges 0 to 51, 51 being worst. quality = int(quality * 51) cmd += ["-crf", str(quality)] # for h264 else: # Many codecs accept q:v # q:v range can vary, 1-31, 31 being worst # But q:v does not always have the same range. # May need a way to find range for any codec. quality = int(quality * 30) + 1 cmd += ["-qscale:v", str(quality)] # for others # Note, for most codecs, the image dimensions must be divisible by # 16 the default for the macro_block_size is 16. Check if image is # divisible, if not have ffmpeg upsize to nearest size and warn # user they should correct input image if this is not desired. if macro_block_size > 1: if size[0] % macro_block_size > 0 or size[1] % macro_block_size > 0: out_w = size[0] out_h = size[1] if size[0] % macro_block_size > 0: out_w += macro_block_size - (size[0] % macro_block_size) if size[1] % macro_block_size > 0: out_h += macro_block_size - (size[1] % macro_block_size) cmd += ["-vf", "scale={}:{}".format(out_w, out_h)] logger.warning( "IMAGEIO FFMPEG_WRITER WARNING: input image is not" " divisible by macro_block_size={}, resizing from {} " "to {} to ensure video compatibility with most codecs " "and players. To prevent resizing, make your input " "image divisible by the macro_block_size or set the " "macro_block_size to 1 (risking incompatibility).".format( macro_block_size, size[:2], (out_w, out_h) ) ) # Rather than redirect stderr to a pipe, just set minimal # output from ffmpeg by default. That way if there are warnings # the user will see them. cmd += ["-v", ffmpeg_log_level] cmd += output_params cmd.append(path) cmd_str = " ".join(cmd) if any( [level in ffmpeg_log_level for level in ("info", "verbose", "debug", "trace")] ): logger.info("RUNNING FFMPEG COMMAND: " + cmd_str) # Launch process p = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, **_popen_kwargs(prevent_sigint=True) ) # Note that directing stderr to a pipe on windows will cause ffmpeg # to hang if the buffer is not periodically cleared using # StreamCatcher or other means. # Setting bufsize to 0 or a small value does not seem to have much effect # (tried on Windows and Linux). I suspect that ffmpeg buffers # multiple frames (before encoding in a batch). # Init policy by which to terminate ffmpeg. May be set to "kill" later. stop_policy = "timeout" if not ffmpeg_timeout: stop_policy = "wait" # ----- Write frames # Enter try block directly after opening the process. # We terminate ffmpeg in the finally clause. # Generators are automatically closed when they get deleted, # so the finally block is guaranteed to run. try: # Just keep going until the generator.close() is called (raises GeneratorExit). # This could also happen when the generator is deleted somehow. nframes = 0 while True: # Get frame bb = yield # framesize = size[0] * size[1] * depth * bpp # assert isinstance(bb, bytes), "Frame must be send as bytes" # assert len(bb) == framesize, "Frame must have width*height*depth*bpp bytes" # Actually, we accept anything that can be written to file. # This e.g. allows writing numpy arrays without having to make a copy ... # Write try: p.stdin.write(bb) except Exception as err: # Show the command and stderr from pipe msg = ( "{0:}\n\nFFMPEG COMMAND:\n{1:}\n\nFFMPEG STDERR " "OUTPUT:\n".format(err, cmd_str) ) raise IOError(msg) nframes += 1 except GeneratorExit: # Note that GeneratorExit does not inherit from Exception but BaseException # Detect premature closing if nframes == 0: logger.warning("No frames have been written; the written video is invalid.") except Exception: # Normal exceptions fall through raise except BaseException: # Detect KeyboardInterrupt / SystemExit: don't wait for ffmpeg to quit stop_policy = "kill" raise finally: # Make sure that ffmpeg is terminated. if p.poll() is None: # Tell ffmpeg that we're done try: p.stdin.close() except Exception as err: # pragma: no cover logger.warning("Error while attempting stop ffmpeg (w): " + str(err)) if stop_policy == "timeout": # Wait until timeout, produce a warning and kill if it still exists try: etime = time.time() + ffmpeg_timeout while (time.time() < etime) and p.poll() is None: time.sleep(0.01) finally: if p.poll() is None: # pragma: no cover logger.warning( "We had to kill ffmpeg to stop it. " + "Consider increasing ffmpeg_timeout, " + "or setting it to zero (no timeout)." ) p.kill() elif stop_policy == "wait": # Wait forever, kill if it if we're interrupted try: while p.poll() is None: time.sleep(0.01) finally: # the above can raise e.g. by ctrl-c or systemexit if p.poll() is None: # pragma: no cover p.kill() else: # stop_policy == "kill": # Just kill it p.kill() # Just to be safe, wrap in try/except try: p.stdout.close() except Exception: pass imageio-ffmpeg-0.6.0/imageio_ffmpeg/_parsing.py000066400000000000000000000152701474227542300215220ustar00rootroot00000000000000import re import threading import time from ._utils import logger class LogCatcher(threading.Thread): """Thread to keep reading from stderr so that the buffer does not fill up and stalls the ffmpeg process. On stderr a message is send on every few frames with some meta information. We only keep the last ones. """ def __init__(self, file): self._file = file self._header = "" self._lines = [] self._remainder = b"" threading.Thread.__init__(self) self.daemon = True # do not let this thread hold up Python shutdown self._should_stop = False self.start() def stop_me(self): self._should_stop = True @property def header(self): """Get header text. Empty string if the header is not yet parsed.""" return self._header def get_text(self, timeout=0): """Get the whole text written to stderr so far. To preserve memory, only the last 50 to 100 frames are kept. If a timeout is given, wait for this thread to finish. When something goes wrong, we stop ffmpeg and want a full report of stderr, but this thread might need a tiny bit more time. """ # Wait? if timeout > 0: etime = time.time() + timeout while self.is_alive() and time.time() < etime: # pragma: no cover time.sleep(0.01) # Return str lines = b"\n".join(self._lines) return self._header + "\n" + lines.decode("utf-8", "ignore") def run(self): # Create ref here so it still exists even if Py is shutting down limit_lines_local = limit_lines while not self._should_stop: time.sleep(0) # Read one line. Detect when closed, and exit try: line = self._file.read(20) except ValueError: # pragma: no cover break if not line: break # Process to divide in lines line = line.replace(b"\r", b"\n").replace(b"\n\n", b"\n") lines = line.split(b"\n") lines[0] = self._remainder + lines[0] self._remainder = lines.pop(-1) # Process each line self._lines.extend(lines) if not self._header: if get_output_video_line(self._lines): header = b"\n".join(self._lines) self._header += header.decode("utf-8", "ignore") elif self._lines: self._lines = limit_lines_local(self._lines) # Close the file when we're done # See #61 and #69 try: self._file.close() except Exception: pass def get_output_video_line(lines): """Get the line that defines the video stream that ffmpeg outputs, and which we read. """ in_output = False for line in lines: sline = line.lstrip() if sline.startswith(b"Output "): in_output = True elif in_output: if sline.startswith(b"Stream ") and b" Video:" in sline: return line def limit_lines(lines, N=32): """When number of lines > 2*N, reduce to N.""" if len(lines) > 2 * N: lines = [b"... showing only last few lines ..."] + lines[-N:] return lines def cvsecs(*args): """converts a time to second. Either cvsecs(min, secs) or cvsecs(hours, mins, secs). """ if len(args) == 1: return float(args[0]) elif len(args) == 2: return 60 * float(args[0]) + float(args[1]) elif len(args) == 3: return 3600 * float(args[0]) + 60 * float(args[1]) + float(args[2]) def parse_ffmpeg_header(text): lines = text.splitlines() meta = {} # meta["header"] = text # Can enable this for debugging # Get version ver = lines[0].split("version", 1)[-1].split("Copyright")[0] meta["ffmpeg_version"] = ver.strip() + " " + lines[1].strip() # get the output line that speaks about video videolines = [ l for l in lines if l.lstrip().startswith("Stream ") and " Video: " in l ] # Codec and pix_fmt hint line = videolines[0] meta["codec"] = line.split("Video: ", 1)[-1].lstrip().split(" ", 1)[0].strip() meta["pix_fmt"] = re.split( # use a negative lookahead regexp to ignore commas that are contained # within a parenthesis # this helps consider a pix_fmt of the kind # yuv420p(tv, progressive) # as what it is, instead of erroneously reporting as # yuv420p(tv r",\s*(?![^()]*\))", line.split("Video: ", 1)[-1], )[1].strip() # get the output line that speaks about audio audiolines = [ l for l in lines if l.lstrip().startswith("Stream ") and " Audio: " in l ] if len(audiolines) > 0: audio_line = audiolines[0] meta["audio_codec"] = ( audio_line.split("Audio: ", 1)[-1].lstrip().split(" ", 1)[0].strip() ) # get the frame rate. # matches can be empty, see #171, assume nframes = inf # the regexp omits values of "1k tbr" which seems a specific edge-case #262 # it seems that tbr is generally to be preferred #262 fps = 0 for line in [videolines[0]]: matches = re.findall(r" ([0-9]+\.?[0-9]*) (fps)", line) if matches: fps = float(matches[0][0].strip()) meta["fps"] = fps # get the size of the original stream, of the form 460x320 (w x h) line = videolines[0] match = re.search(" [0-9]*x[0-9]*(,| )", line) parts = line[match.start() : match.end() - 1].split("x") meta["source_size"] = tuple(map(int, parts)) # get the size of what we receive, of the form 460x320 (w x h) line = videolines[-1] # Pipe output match = re.search(" [0-9]*x[0-9]*(,| )", line) parts = line[match.start() : match.end() - 1].split("x") meta["size"] = tuple(map(int, parts)) # Check the two sizes if meta["source_size"] != meta["size"]: logger.warning( "The frame size for reading {} is " "different from the source frame size {}.".format( meta["size"], meta["source_size"] ) ) # get the rotate metadata reo_rotate = re.compile(r"rotate\s+:\s([0-9]+)") match = reo_rotate.search(text) rotate = 0 if match is not None: rotate = match.groups()[0] meta["rotate"] = int(rotate) # get duration (in seconds) line = [l for l in lines if "Duration: " in l][0] match = re.search(" [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]", line) duration = 0 if match is not None: hms = line[match.start() + 1 : match.end()].split(":") duration = cvsecs(*hms) meta["duration"] = duration return meta imageio-ffmpeg-0.6.0/imageio_ffmpeg/_utils.py000066400000000000000000000073711474227542300212220ustar00rootroot00000000000000import logging import os import subprocess import sys from functools import lru_cache import importlib.resources from ._definitions import FNAME_PER_PLATFORM, get_platform logger = logging.getLogger("imageio_ffmpeg") def get_ffmpeg_exe(): """ Get the ffmpeg executable file. This can be the binary defined by the IMAGEIO_FFMPEG_EXE environment variable, the binary distributed with imageio-ffmpeg, an ffmpeg binary installed with conda, or the system ffmpeg (in that order). A RuntimeError is raised if no valid ffmpeg could be found. """ # 1. Try environment variable. - Dont test it: the user is explicit here! exe = os.getenv("IMAGEIO_FFMPEG_EXE", None) if exe: return exe # Auto-detect exe = _get_ffmpeg_exe() if exe: return exe # Nothing was found raise RuntimeError( "No ffmpeg exe could be found. Install ffmpeg on your system, " "or set the IMAGEIO_FFMPEG_EXE environment variable." ) @lru_cache() def _get_ffmpeg_exe(): plat = get_platform() # 2. Try from here exe = os.path.join(_get_bin_dir(), FNAME_PER_PLATFORM.get(plat, "")) if exe and os.path.isfile(exe) and _is_valid_exe(exe): return exe # 3. Try binary from conda package # (installed e.g. via `conda install ffmpeg -c conda-forge`) if plat.startswith("win"): exe = os.path.join(sys.prefix, "Library", "bin", "ffmpeg.exe") else: exe = os.path.join(sys.prefix, "bin", "ffmpeg") if exe and os.path.isfile(exe) and _is_valid_exe(exe): return exe # 4. Try system ffmpeg command exe = "ffmpeg" if _is_valid_exe(exe): return exe return None def _get_bin_dir(): if sys.version_info < (3, 9): context = importlib.resources.path("imageio_ffmpeg.binaries", "__init__.py") else: ref = importlib.resources.files("imageio_ffmpeg.binaries") / "__init__.py" context = importlib.resources.as_file(ref) with context as path: pass # Return the dir. We assume that the data files are on a normal dir on the fs. return str(path.parent) def _popen_kwargs(prevent_sigint=False): startupinfo = None preexec_fn = None creationflags = 0 if sys.platform.startswith("win"): # Stops executable from flashing on Windows (see #22) startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW if prevent_sigint: # Prevent propagation of sigint (see #4) # https://stackoverflow.com/questions/5045771 if sys.platform.startswith("win"): creationflags = 0x00000200 else: preexec_fn = os.setpgrp # the _pre_exec does not seem to work falsy = ("", "0", "false", "no") if os.getenv("IMAGEIO_FFMPEG_NO_PREVENT_SIGINT", "").lower() not in falsy: # Unset preexec_fn to work around a strange hang on fork() (see #58) preexec_fn = None return { "startupinfo": startupinfo, "creationflags": creationflags, "preexec_fn": preexec_fn, } def _is_valid_exe(exe): cmd = [exe, "-version"] try: with open(os.devnull, "w") as null: subprocess.check_call( cmd, stdout=null, stderr=subprocess.STDOUT, **_popen_kwargs() ) return True except (OSError, ValueError, subprocess.CalledProcessError): return False def get_ffmpeg_version(): """ Get the version of the used ffmpeg executable (as a string). """ exe = get_ffmpeg_exe() line = subprocess.check_output([exe, "-version"], **_popen_kwargs()).split( b"\n", 1 )[0] line = line.decode(errors="ignore").strip() version = line.split("version", 1)[-1].lstrip().split(" ", 1)[0].strip() return version imageio-ffmpeg-0.6.0/imageio_ffmpeg/binaries/000077500000000000000000000000001474227542300211355ustar00rootroot00000000000000imageio-ffmpeg-0.6.0/imageio_ffmpeg/binaries/README.md000066400000000000000000000000551474227542300224140ustar00rootroot00000000000000Exes are dropped here by the release script. imageio-ffmpeg-0.6.0/imageio_ffmpeg/binaries/__init__.py000066400000000000000000000000551474227542300232460ustar00rootroot00000000000000# Just here to make importlib.resources work imageio-ffmpeg-0.6.0/setup.py000066400000000000000000000051161474227542300161200ustar00rootroot00000000000000""" Setup script for imageio-ffmpeg. """ import os import sys from setuptools import setup this_dir = os.path.dirname(os.path.abspath(__file__)) # Get version sys.path.insert(0, os.path.join(this_dir, "imageio_ffmpeg")) try: from _definitions import __version__ finally: sys.path.pop(0) # Disallow releasing via setup.py if "upload" in sys.argv: raise RuntimeError("Running setup.py upload is not the proper release procedure!") # If making a source dist, clear the binaries directory if "sdist" in sys.argv: target_dir = os.path.abspath(os.path.join(this_dir, "imageio_ffmpeg", "binaries")) for fname in os.listdir(target_dir): if fname not in ["README.md", "__init__.py"]: os.remove(os.path.join(target_dir, fname)) long_description = """ FFMPEG wrapper for Python. Note that the platform-specific wheels contain the binary executable of ffmpeg, which makes this package around 60 MiB in size. I guess that's the cost for being able to read/write video files. For Linux users: the above is not the case when installing via your Linux package manager (if that is possible), because this package would simply depend on ffmpeg in that case. """.lstrip() setup( name="imageio-ffmpeg", version=__version__, author="imageio contributors", author_email="almar.klein@gmail.com", license="BSD-2-Clause", url="https://github.com/imageio/imageio-ffmpeg", download_url="http://pypi.python.org/pypi/imageio-ffmpeg", keywords="video ffmpeg", description="FFMPEG wrapper for Python", long_description=long_description, platforms="any", provides=["imageio_ffmpeg"], python_requires=">=3.9", setup_requires=[], install_requires=[], packages=["imageio_ffmpeg", "imageio_ffmpeg.binaries"], package_data={"imageio_ffmpeg.binaries": ["*.*"]}, include_package_data=True, zip_safe=False, classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Intended Audience :: Education", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ], ) imageio-ffmpeg-0.6.0/tasks.py000066400000000000000000000225401474227542300161050ustar00rootroot00000000000000""" Invoke tasks for imageio-ffmpeg """ import importlib import os import shutil import subprocess import sys from urllib.request import urlopen from invoke import task # ---------- Per project config ---------- NAME = "imageio-ffmpeg" LIBNAME = NAME.replace("-", "_") PY_PATHS = [LIBNAME, "tests", "tasks.py", "setup.py"] # for linting/formatting # ---------------------------------------- ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) if not os.path.isdir(os.path.join(ROOT_DIR, LIBNAME)): sys.exit("package NAME seems to be incorrect.") @task def test(ctx, cover=False): """Perform unit tests. Use --cover to open a webbrowser to show coverage.""" cmd = [sys.executable, "-m", "pytest", "tests", "-v"] cmd += ["--cov=" + LIBNAME, "--cov-report=term", "--cov-report=html"] ret_code = subprocess.call(cmd, cwd=ROOT_DIR) if ret_code: sys.exit(ret_code) if cover: import webbrowser webbrowser.open(os.path.join(ROOT_DIR, "htmlcov", "index.html")) @task def lint(ctx): """Validate the code style (e.g. undefined names)""" try: importlib.import_module("flake8") except ImportError: sys.exit("You need to ``pip install flake8`` to lint") print("Checking linting errors with flake8:") # We use flake8 with minimal settings # http://pep8.readthedocs.io/en/latest/intro.html#error-codes cmd = [sys.executable, "-m", "flake8"] + PY_PATHS + ["--select=F,E11"] ret_code = subprocess.call(cmd, cwd=ROOT_DIR) if ret_code == 0: print("No linting errors found") else: sys.exit(ret_code) @task def checkformat(ctx): """Check whether the code adheres to the style rules. Use autoformat to fix.""" print("Checking format with black (also see invoke autoformat):") black_wrapper(False) @task def autoformat(ctx): """Automatically format the code (using black).""" print("Auto-formatting with black:") black_wrapper(True) @task def clean(ctx): """Clean the repo of temp files etc.""" for root, dirs, files in os.walk(ROOT_DIR): for dname in dirs: if dname in ( "__pycache__", ".cache", "htmlcov", ".hypothesis", ".pytest_cache", "dist", "build", LIBNAME + ".egg-info", ): shutil.rmtree(os.path.join(root, dname)) print("Removing", dname) for fname in files: if fname.endswith((".pyc", ".pyo")) or fname in (".coverage"): os.remove(os.path.join(root, fname)) print("Removing", fname) ## @task def get_ffmpeg_binary(ctx): """Download/copy ffmpeg binary for local development.""" # Get ffmpeg fname sys.path.insert(0, os.path.join(ROOT_DIR, "imageio_ffmpeg")) try: from _definitions import FNAME_PER_PLATFORM, get_platform finally: sys.path.pop(0) fname = FNAME_PER_PLATFORM[get_platform()] # Clear clear_binaries_dir(os.path.join(ROOT_DIR, "imageio_ffmpeg", "binaries")) # Use local if we can (faster) source_dir = os.path.abspath( os.path.join(ROOT_DIR, "..", "imageio-binaries", "ffmpeg") ) if os.path.isdir(source_dir): copy_binaries(os.path.join(ROOT_DIR, "imageio_ffmpeg", "binaries"), fname) return # Download from Github base_url = "https://github.com/imageio/imageio-binaries/raw/master/ffmpeg/" filename = os.path.join(ROOT_DIR, "imageio_ffmpeg", "binaries", fname) print("Downloading", fname, "...", end="") with urlopen(base_url + fname, timeout=5) as f1: with open(filename, "wb") as f2: shutil.copyfileobj(f1, f2) # Mark executable os.chmod(filename, os.stat(filename).st_mode | 64) print("done") @task def build(ctx): """Build packages for different platforms. Dont release yet.""" # Get version and more sys.path.insert(0, os.path.join(ROOT_DIR, "imageio_ffmpeg")) try: from _definitions import FNAME_PER_PLATFORM, WHEEL_BUILDS, __version__ finally: sys.path.pop(0) # Clear up any build artifacts clean(ctx) # Clear binaries, we don't want them in the reference release clear_binaries_dir( os.path.abspath(os.path.join(ROOT_DIR, "imageio_ffmpeg", "binaries")) ) # Now build a universal wheel print("Using setup.py to generate wheel... ", end="") subprocess.check_output( [sys.executable, "setup.py", "sdist", "bdist_wheel"], cwd=ROOT_DIR ) print("done") # Prepare dist_dir = os.path.join(ROOT_DIR, "dist") fname = "imageio_ffmpeg-" + __version__ + "-py3-none-any.whl" packdir = "imageio_ffmpeg-" + __version__ infodir = "imageio_ffmpeg-" + __version__ + ".dist-info" wheelfile = os.path.join(dist_dir, packdir, infodir, "WHEEL") assert os.path.isfile(os.path.join(dist_dir, fname)) # Unpack print("Unpacking ... ", end="") subprocess.check_output( [sys.executable, "-m", "wheel", "unpack", fname], cwd=dist_dir ) os.remove(os.path.join(dist_dir, packdir, infodir, "RECORD")) print("done") # Build for different platforms for wheeltag, platform in WHEEL_BUILDS.items(): ffmpeg_fname = FNAME_PER_PLATFORM[platform] # Edit print("Edit for {} ({})".format(platform, wheeltag)) copy_binaries( os.path.join(dist_dir, packdir, "imageio_ffmpeg", "binaries"), ffmpeg_fname ) make_platform_specific(wheelfile, wheeltag) # Pack print("Pack ... ", end="") subprocess.check_output( [sys.executable, "-m", "wheel", "pack", packdir], cwd=dist_dir ) print("done") # Clean up os.remove(os.path.join(dist_dir, fname)) shutil.rmtree(os.path.join(dist_dir, packdir)) # Show overview print("Dist folder:") for fname in sorted(os.listdir(dist_dir)): s = os.stat(os.path.join(dist_dir, fname)).st_size print(" {:0.0f} KiB {}".format(s / 2**10, fname)) if sys.platform.startswith("win"): print("Note that the exes for Linux/OSX are not chmodded properly!") @task def release(ctx): """Release the packages to Pypi!""" dist_dir = os.path.join(ROOT_DIR, "dist") if not os.path.isdir(dist_dir): sys.exit("Dist directory does not exist. Build first?") print("This is what you are about to upload:") for fname in sorted(os.listdir(dist_dir)): s = os.stat(os.path.join(dist_dir, fname)).st_size print(" {:0.0f} KiB {}".format(s / 2**10, fname)) while True: x = input("Are you sure you want to upload now? [Y/N]: ") if x.upper() == "N": return elif x.upper() == "Y": break if sys.platform.startswith("win"): sys.exit("Cannot release from Windows: the exes wont be chmodded properly!") subprocess.check_call([sys.executable, "-m", "twine", "upload", "dist/*"]) @task def update_readme(ctx): """Update readme to include the latest API docs.""" text = open(os.path.join(ROOT_DIR, "README.md"), "rb").read().decode() text = text.split("\n## API\n")[0] + "\n## API\n\n" import inspect import imageio_ffmpeg for func in ( imageio_ffmpeg.read_frames, imageio_ffmpeg.write_frames, imageio_ffmpeg.count_frames_and_secs, imageio_ffmpeg.get_ffmpeg_exe, imageio_ffmpeg.get_ffmpeg_version, ): source = inspect.getsourcelines(func)[0] stripped = [x.strip() for x in source] end = stripped.index('"""', stripped.index('"""') + 1) + 1 text += "```py\n" + "".join(source[:end]) + "```\n\n" with open(os.path.join(ROOT_DIR, "README.md"), "wb") as f: f.write(text.encode()) ## def black_wrapper(writeback): """Helper function to invoke black programatically.""" check = [] if writeback else ["--check"] exclude = "|".join([".git"]) sys.argv[1:] = check + ["--exclude", exclude, ROOT_DIR] import black black.main() def clear_binaries_dir(target_dir): assert os.path.isdir(target_dir) for fname in os.listdir(target_dir): if fname not in ["README.md", "__init__.py"]: print("Removing", fname, "...", end="") os.remove(os.path.join(target_dir, fname)) print("done") def copy_binaries(target_dir, fname): # Get source dir - the imageio-binaries repo must be present source_dir = os.path.abspath( os.path.join(ROOT_DIR, "..", "imageio-binaries", "ffmpeg") ) if not os.path.isdir(source_dir): sys.exit("Need to clone imageio-binaries next to this repo to do a release!") clear_binaries_dir(target_dir) print("Copying", fname, "...", end="") filename = os.path.join(target_dir, fname) shutil.copy2(os.path.join(source_dir, fname), filename) # Mark as exe. This does not actually do anything on Windows. os.chmod(filename, os.stat(filename).st_mode | 64) print("done") def make_platform_specific(filename, tag): with open(filename, "rb") as f: text = f.read().decode() lines = [] for line in text.splitlines(): if line.startswith("Root-Is-Purelib:"): line = "Root-Is-Purelib: true" elif line.startswith("Tag:"): line = "Tag: " + tag lines.append(line) text = "\n".join(lines).strip() + "\n" with open(filename, "wb") as f: f.write(text.encode()) imageio-ffmpeg-0.6.0/tests/000077500000000000000000000000001474227542300155455ustar00rootroot00000000000000imageio-ffmpeg-0.6.0/tests/snippets.py000066400000000000000000000024201474227542300177620ustar00rootroot00000000000000""" Snippets of code that are hard to bring under test, but that can be used to manually test the behavior of imageip-ffmpeg in certain use-cases. Some may depend on imageio. """ # %% Write a series of large frames # In earlier versions of imageio-ffmpeg, the ffmpeg process was given a timeout # to complete, but this timeout must be longer for longer movies. The default # is now to wait for ffmpeg. import os import numpy as np import imageio_ffmpeg ims = [ np.random.uniform(0, 255, size=(1000, 1000, 3)).astype(np.uint8) for i in range(10) ] filename = os.path.expanduser("~/Desktop/foo.mp4") w = imageio_ffmpeg.write_frames(filename, (1000, 1000), ffmpeg_timeout=0) w.send(None) for i in range(200): w.send(ims[i % 10]) print(i) w.close() # %% Behavior of KeyboardInterrupt / Ctrl+C import os import imageio_ffmpeg filename = os.path.expanduser("~/.imageio/images/cockatoo.mp4") reader = imageio_ffmpeg.read_frames(filename) meta = reader.__next__() try: input("Do a manual KeyboardInterrupt now [Ctrl]+[c]") # Note: Raising an error with code won't trigger the original error. except BaseException as err: print(err) print("out1", len(reader.__next__())) print("out2", len(reader.__next__())) print("closing") reader.close() print("closed") imageio-ffmpeg-0.6.0/tests/test_io.py000066400000000000000000000327101474227542300175700ustar00rootroot00000000000000""" The main tests for the public API. """ import tempfile import time import types import warnings from pytest import raises, skip from testutils import ( ensure_test_files, no_warnings_allowed, test_dir, test_file1, test_file2, test_file3, ) import imageio_ffmpeg from imageio_ffmpeg._io import ( ffmpeg_test_encoder, get_compiled_h264_encoders, get_first_available_h264_encoder, ) def setup_module(): ensure_test_files() @no_warnings_allowed def test_ffmpeg_version(): version = imageio_ffmpeg.get_ffmpeg_version() print("ffmpeg version", version) assert version > "3.0" @no_warnings_allowed def test_read_nframes(): nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file1) assert nframes == 280 assert 13.80 < nsecs <= 14.00 def test_read_frames_resource_warning(): """ Test issue #61: ensure no warnings are raised when the generator is closed todo: use pytest.does_not_warn() as soon as it becomes available (see https://github.com/pytest-dev/pytest/issues/9404) """ with warnings.catch_warnings(record=True) as warnings_: gen = imageio_ffmpeg.read_frames(test_file1) next(gen) gen.close() # there should not be any warnings, but show warning messages if there are assert not [w.message for w in warnings_] @no_warnings_allowed def test_reading1(): # Calling returns a generator gen = imageio_ffmpeg.read_frames(test_file1) assert isinstance(gen, types.GeneratorType) # First yield is a meta dict meta = gen.__next__() assert isinstance(meta, dict) for key in ("size", "fps", "duration"): assert key in meta # Read frames framesize = meta["size"][0] * meta["size"][1] * 3 assert framesize == 1280 * 720 * 3 count = 0 for frame in gen: assert isinstance(frame, bytes) and len(frame) == framesize count += 1 assert count == 280 @no_warnings_allowed def test_reading2(): # Same as 1, but using other pixel format gen = imageio_ffmpeg.read_frames(test_file1, pix_fmt="gray", bpp=1) meta = gen.__next__() framesize = meta["size"][0] * meta["size"][1] * 1 assert framesize == 1280 * 720 * 1 count = 0 for frame in gen: count += 1 assert isinstance(frame, bytes) and len(frame) == framesize assert count == 280 @no_warnings_allowed def test_reading3(): # Same as 1, but using other fps gen = imageio_ffmpeg.read_frames(test_file1, output_params=["-r", "5.0"]) meta = gen.__next__() framesize = meta["size"][0] * meta["size"][1] * 3 assert framesize == 1280 * 720 * 3 count = 0 for frame in gen: count += 1 assert isinstance(frame, bytes) and len(frame) == framesize assert 50 < count < 100 # because smaller fps, same duration @no_warnings_allowed def test_reading4(): # Same as 1, but wrong, using an insane bpp, to invoke eof halfway a frame gen = imageio_ffmpeg.read_frames(test_file1, bpp=13) gen.__next__() # == meta with raises(RuntimeError) as info: for frame in gen: pass msg = str(info.value).lower() assert "end of file reached before full frame could be read" in msg assert "ffmpeg version" in msg # The log is included @no_warnings_allowed def test_reading5(): # Same as 1, but using other pixel format and bits_per_pixel bits_per_pixel = 12 bits_per_bytes = 8 gen = imageio_ffmpeg.read_frames( test_file3, pix_fmt="yuv420p", bits_per_pixel=bits_per_pixel ) meta = gen.__next__() assert isinstance(meta, dict) for key in ("size", "fps", "duration"): assert key in meta # Read frames framesize = meta["size"][0] * meta["size"][1] * bits_per_pixel / bits_per_bytes assert framesize == 320 * 240 * bits_per_pixel / bits_per_bytes count = 0 for frame in gen: assert isinstance(frame, bytes) and len(frame) == framesize count += 1 assert count == 36 @no_warnings_allowed def test_reading_invalid_video(): """ Check whether invalid video is handled correctly without timeouts """ # empty file as an example of invalid video _, test_invalid_file = tempfile.mkstemp(dir=test_dir) gen = imageio_ffmpeg.read_frames(test_invalid_file) start = time.time() with raises(OSError): gen.__next__() end = time.time() # check if metadata extraction doesn't hang # for a timeout period assert end - start < 1, "Metadata extraction hangs" @no_warnings_allowed def test_write1(): for n in (1, 9, 14, 279, 280, 281): # Prepare for writing gen = imageio_ffmpeg.write_frames(test_file2, (64, 64)) assert isinstance(gen, types.GeneratorType) gen.send(None) # seed # Write n frames for i in range(n): data = bytes([min(255, 100 + i * 10)] * 64 * 64 * 3) gen.send(data) gen.close() # Check that number of frames is correct nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == n # Check again by actually reading gen2 = imageio_ffmpeg.read_frames(test_file2) gen2.__next__() # == meta count = 0 for frame in gen2: count += 1 assert count == n @no_warnings_allowed def test_write_pix_fmt_in(): sizes = [] for pixfmt, bpp in [("gray", 1), ("rgb24", 3), ("rgba", 4)]: # Prepare for writing gen = imageio_ffmpeg.write_frames(test_file2, (64, 64), pix_fmt_in=pixfmt) gen.send(None) # seed for i in range(9): data = bytes([min(255, 100 + i * 10)] * 64 * 64 * bpp) gen.send(data) gen.close() with open(test_file2, "rb") as f: sizes.append(len(f.read())) # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 assert sizes[0] <= sizes[1] <= sizes[2] @no_warnings_allowed def test_write_pix_fmt_out(): sizes = [] for pixfmt in ["gray", "yuv420p"]: # Prepare for writing gen = imageio_ffmpeg.write_frames(test_file2, (64, 64), pix_fmt_out=pixfmt) gen.send(None) # seed for i in range(9): data = bytes([min(255, 100 + i * 10)] * 64 * 64 * 3) gen.send(data) gen.close() with open(test_file2, "rb") as f: sizes.append(len(f.read())) # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 assert sizes[0] < sizes[1] @no_warnings_allowed def test_write_wmv(): # Switch to MS friendly codec when writing .wmv files for ext, codec in [("", "h264"), (".wmv", "msmpeg4")]: fname = test_file2 + ext gen = imageio_ffmpeg.write_frames(fname, (64, 64)) gen.send(None) # seed for i in range(9): data = bytes([min(255, 100 + i * 10)] * 64 * 64 * 3) gen.send(data) gen.close() # meta = imageio_ffmpeg.read_frames(fname).__next__() assert meta["codec"].startswith(codec) @no_warnings_allowed def test_write_quality(): try: import numpy as np except ImportError: return skip("Missing 'numpy' test dependency") sizes = [] for quality in [2, 5, 9]: # Prepare for writing gen = imageio_ffmpeg.write_frames(test_file2, (64, 64), quality=quality) gen.send(None) # seed for i in range(9): data = np.random.randint(0, 255, (64, 64, 3), dtype="uint8") gen.send(data) gen.close() with open(test_file2, "rb") as f: sizes.append(len(f.read())) # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 assert sizes[0] <= sizes[1] <= sizes[2] # Add a test compression with lossless mode with ffmpeg gen = imageio_ffmpeg.write_frames( test_file2, (64, 64), # Setting the quality to None should disable # any premade settings quality=None, output_params=["-qp", "0"], ) gen.send(None) # seed for i in range(9): data = np.random.randint(0, 255, (64, 64, 3), dtype="uint8") gen.send(data) gen.close() with open(test_file2, "rb") as f: size_lossless = len(f.read()) # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 assert sizes[2] < size_lossless @no_warnings_allowed def test_write_bitrate(): # Mind that we send uniform images, so the difference is marginal sizes = [] for bitrate in ["1k", "10k", "100k"]: # Prepare for writing gen = imageio_ffmpeg.write_frames(test_file2, (64, 64), bitrate=bitrate) gen.send(None) # seed for i in range(9): data = bytes([min(255, 100 + i * 10)] * 64 * 64 * 3) gen.send(data) gen.close() with open(test_file2, "rb") as f: sizes.append(len(f.read())) # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 assert sizes[0] < sizes[1] < sizes[2] # @no_warnings_allowed --> will generate warnings abiut macro block size def test_write_macro_block_size(): frame_sizes = [] for mbz in [None, 10]: # None is default == 16 # Prepare for writing gen = imageio_ffmpeg.write_frames(test_file2, (40, 50), macro_block_size=mbz) gen.send(None) # seed for i in range(9): data = bytes([min(255, 100 + i * 10)] * 40 * 50 * 3) gen.send(data) gen.close() # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 # Check size meta = imageio_ffmpeg.read_frames(test_file2).__next__() frame_sizes.append(meta["size"]) assert frame_sizes[0] == (48, 64) assert frame_sizes[1] == (40, 50) # @no_warnings_allowed --> Will generate a warning about killing ffmpeg def test_write_big_frames(): """Test that we give ffmpeg enough time to finish.""" try: import numpy as np except ImportError: return skip("Missing 'numpy' test dependency") n = 9 def _write_frames(pixfmt, bpp, tout): gen = imageio_ffmpeg.write_frames( test_file2, (2048, 2048), pix_fmt_in=pixfmt, ffmpeg_timeout=tout ) gen.send(None) # seed for i in range(n): data = (255 * np.random.rand(2048 * 2048 * bpp)).astype(np.uint8) data = bytes(data) gen.send(data) gen.close() # Short timeout is not enough time # Note that on Windows, if we wait a bit before calling count_frames_and_secs(), # it *does* work. Probably because killing a process on Windows is not instant (?) # and ffmpeg is able to still process the frames. _write_frames("rgb24", 3, 1.0) raises(RuntimeError, imageio_ffmpeg.count_frames_and_secs, test_file2) _write_frames("gray", 1, 15.0) nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == n _write_frames("rgb24", 3, 15.0) nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == n _write_frames("rgba", 4, 15.0) nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == n _write_frames("rgba", 4, None) # the default os to wait (since v0.4.0) nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == n @no_warnings_allowed def test_write_audio_path(): # Provide an audio gen = imageio_ffmpeg.write_frames( test_file2, (64, 64), audio_path=test_file3, audio_codec="aac" ) gen.send(None) # seed for i in range(9): data = bytes([min(255, 100 + i * 10)] * 64 * 64 * 3) gen.send(data) gen.close() # Check nframes nframes, nsecs = imageio_ffmpeg.count_frames_and_secs(test_file2) assert nframes == 9 # Check size meta = imageio_ffmpeg.read_frames(test_file2).__next__() audio_codec = meta["audio_codec"] assert nframes == 9 assert audio_codec == "aac" def test_get_compiled_h264_encoders(): available_encoders = get_compiled_h264_encoders() # Assert it is not a mutable type assert isinstance(available_encoders, tuple) # Software encoders like libx264 should work regardless of hardware for encoder in ["libx264", "libopenh264", "libx264rgb"]: if encoder in available_encoders: assert ffmpeg_test_encoder(encoder) else: assert not ffmpeg_test_encoder(encoder) assert not ffmpeg_test_encoder("not_a_real_encoder") def test_prefered_encoder(): available_encoders = get_compiled_h264_encoders() # historically, libx264 was the preferred encoder for imageio # However, the user (or distribution) may not have it installed in their # implementation of ffmpeg. if "libx264" in available_encoders: assert "libx264" == get_first_available_h264_encoder() if __name__ == "__main__": setup_module() test_ffmpeg_version() test_read_nframes() test_read_frames_resource_warning() test_reading1() test_reading2() test_reading3() test_reading4() test_reading_invalid_video() test_write1() test_write_pix_fmt_in() test_write_pix_fmt_out() test_write_wmv() test_write_quality() test_write_bitrate() test_write_macro_block_size() test_write_big_frames() test_write_audio_path() imageio-ffmpeg-0.6.0/tests/test_parsing.py000066400000000000000000000271041474227542300206250ustar00rootroot00000000000000# styletest: ignore E501 """ Tests specific to parsing ffmpeg header. """ from imageio_ffmpeg._parsing import cvsecs, limit_lines, parse_ffmpeg_header def dedent(text, dedent=8): lines = [line[dedent:] for line in text.splitlines()] text = "\n".join(lines) return text.strip() + "\n" def test_cvsecs(): assert cvsecs(20) == 20 assert cvsecs(2, 20) == (2 * 60) + 20 assert cvsecs(2, 3, 20) == (2 * 3600) + (3 * 60) + 20 def test_limit_lines(): lines = ["foo"] * 10 assert len(limit_lines(lines)) == 10 lines = ["foo"] * 50 assert len(limit_lines(lines)) == 50 # < 2 * N lines = ["foo"] * 70 + ["bar"] lines2 = limit_lines(lines) assert len(lines2) == 33 # > 2 * N assert b"last few lines" in lines2[0] assert "bar" == lines2[-1] def test_get_correct_fps1(): # from issue imageio#262 sample = dedent( r""" fmpeg version 3.2.2 Copyright (c) 2000-2016 the FFmpeg developers built with Apple LLVM version 8.0.0 (clang-800.0.42.1) configuration: --prefix=/usr/local/Cellar/ffmpeg/3.2.2 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-frei0r --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopus --enable-librtmp --enable-libschroedinger --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-opencl --disable-lzma --enable-libopenjpeg --disable-decoder=jpeg2000 --extra-cflags=-I/usr/local/Cellar/openjpeg/2.1.2/include/openjpeg-2.1 --enable-nonfree --enable-vda libavutil 55. 34.100 / 55. 34.100 libavcodec 57. 64.101 / 57. 64.101 libavformat 57. 56.100 / 57. 56.100 libavdevice 57. 1.100 / 57. 1.100 libavfilter 6. 65.100 / 6. 65.100 libavresample 3. 1. 0 / 3. 1. 0 libswscale 4. 2.100 / 4. 2.100 libswresample 2. 3.100 / 2. 3.100 libpostproc 54. 1.100 / 54. 1.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/echeng/video.mp4': Metadata: major_brand : mp42 minor_version : 1 compatible_brands: isom3gp43gp5 Duration: 00:16:05.80, start: 0.000000, bitrate: 1764 kb/s Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 8000 Hz, mono, fltp, 40 kb/s (default) Metadata: handler_name : soun Stream #0:1(eng): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p, 640x480 [SAR 1:1 DAR 4:3], 1720 kb/s, 29.46 fps, 26.58 tbr, 90k tbn, 1k tbc (default) Metadata: handler_name : vide Output #0, image2pipe, to 'pipe:': Metadata: major_brand : mp42 minor_version : 1 compatible_brands: isom3gp43gp5 encoder : Lavf57.56.100 Stream #0:0(eng): Video: rawvideo (RGB[24] / 0x18424752), rgb24, 640x480 [SAR 1:1 DAR 4:3], q=2-31, 200 kb/s, 26.58 fps, 26.58 tbn, 26.58 tbc (default) Metadata: handler_name : vide encoder : Lavc57.64.101 rawvideo Stream mapping: """ ) info = parse_ffmpeg_header(sample) assert info["fps"] == 29.46 def test_get_correct_fps2(): # from issue imageio#262 sample = dedent( r""" ffprobe version 3.2.2 Copyright (c) 2007-2016 the FFmpeg developers built with Apple LLVM version 8.0.0 (clang-800.0.42.1) configuration: --prefix=/usr/local/Cellar/ffmpeg/3.2.2 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-frei0r --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopus --enable-librtmp --enable-libschroedinger --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-opencl --disable-lzma --enable-libopenjpeg --disable-decoder=jpeg2000 --extra-cflags=-I/usr/local/Cellar/openjpeg/2.1.2/include/openjpeg-2.1 --enable-nonfree --enable-vda libavutil 55. 34.100 / 55. 34.100 libavcodec 57. 64.101 / 57. 64.101 libavformat 57. 56.100 / 57. 56.100 libavdevice 57. 1.100 / 57. 1.100 libavfilter 6. 65.100 / 6. 65.100 libavresample 3. 1. 0 / 3. 1. 0 libswscale 4. 2.100 / 4. 2.100 libswresample 2. 3.100 / 2. 3.100 libpostproc 54. 1.100 / 54. 1.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video.mp4': Metadata: major_brand : mp42 minor_version : 1 compatible_brands: isom3gp43gp5 Duration: 00:08:44.53, start: 0.000000, bitrate: 1830 kb/s Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 8000 Hz, mono, fltp, 40 kb/s (default) Metadata: handler_name : soun Stream #0:1(eng): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p, 640x480 [SAR 1:1 DAR 4:3], 1785 kb/s, 29.27 fps, 1k tbr, 90k tbn, 1k tbc (default) Metadata: handler_name : vide """ ) info = parse_ffmpeg_header(sample) assert info["fps"] == 29.27 def test_get_correct_rotation(): # from issue imageio-ffmpeg#38 sample = dedent( r""" ffmpeg version 4.2.2 Copyright (c) 2000-2019 the FFmpeg developers built with Apple clang version 11.0.0 (clang-1100.0.33.8) configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-appkit --enable-avfoundation --enable-coreimage --enable-audiotoolbox libavutil 56. 31.100 / 56. 31.100 libavcodec 58. 54.100 / 58. 54.100 libavformat 58. 29.100 / 58. 29.100 libavdevice 58. 8.100 / 58. 8.100 libavfilter 7. 57.100 / 7. 57.100 libswscale 5. 5.100 / 5. 5.100 libswresample 3. 5.100 / 3. 5.100 libpostproc 55. 5.100 / 55. 5.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/var/folders/82/6_ww__k94ms56ldtsph50pqc0000gn/T/imageio_sievk8ws': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isommp42 creation_time : 2020-06-18T12:31:32.000000Z com.android.version: 10 Duration: 00:00:01.07, start: 0.000000, bitrate: 2661 kb/s Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc, bt470bg/bt470bg/smpte170m), 720x480, 2636 kb/s, SAR 1:1 DAR 3:2, 30.01 fps, 120 tbr, 90k tbn, 180k tbc (default) Metadata: rotate : 270 creation_time : 2020-06-18T12:31:32.000000Z handler_name : VideoHandle Side data: displaymatrix: rotation of 90.00 degrees Stream mapping: Stream #0:0 -> #0:0 (h264 (native) -> rawvideo (native)) Press [q] to stop, [?] for help [swscaler @ 0x7f95cd92da00] deprecated pixel format used, make sure you did set range correctly Output #0, image2pipe, to 'pipe:': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isommp42 com.android.version: 10 encoder : Lavf58.29.100 Stream #0:0(eng): Video: rawvideo (RGB[24] / 0x18424752), rgb24, 480x720 [SAR 1:1 DAR 2:3], q=2-31, 995328 kb/s, 120 fps, 120 tbn, 120 tbc (default) """ ) info = parse_ffmpeg_header(sample) assert info["rotate"] == 270 def test_comma_in_pixel_format(): sample = dedent( r""" ffmpeg version 5.1.2 Copyright (c) 2000-2022 the FFmpeg developers built with gcc 11.3.0 (conda-forge gcc 11.3.0-19) configuration: --prefix=/home/conda/feedstock_root/build_artifacts/ffmpeg_1671040268898/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_plac --cc=/home/conda/feedstock_root/build_artifacts/ffmpeg_1671040268898/_build_env/bin/x86_64-conda-linux-gnu-cc --cxx=/home/conda/feedstock_root/build_artifacts/ffmpeg_1671040268898/_build_env/bin/x86_64-conda-linux-gnu-c++ --nm=/home/conda/feedstock_root/build_artifacts/ffmpeg_1671040268898/_build_env/bin/x86_64-conda-linux-gnu-nm --ar=/home/conda/feedstock_root/build_artifacts/ffmpeg_1671040268898/_build_env/bin/x86_64-conda-linux-gnu-ar --disable-doc --disable-openssl --enable-demuxer=dash --enable-hardcoded-tables --enable-libfreetype --enable-libfontconfig --enable-libopenh264 --enable-gnutls --enable-libmp3lame --enable-libvpx --enable-pthreads --enable-vaapi --disable-gpl --enable-libaom --enable-libsvtav1 --enable-libxml2 --enable-pic --enable-shared --disable-static --enable-version3 --enable-zlib --pkg-config=/home/conda/feedstock_root/build_artifacts/ffmpeg_1671040268898/_build_env/bin/pkg-config libavutil 57. 28.100 / 57. 28.100 libavcodec 59. 37.100 / 59. 37.100 libavformat 59. 27.100 / 59. 27.100 libavdevice 59. 7.100 / 59. 7.100 libavfilter 8. 44.100 / 8. 44.100 libswscale 6. 7.100 / 6. 7.100 libswresample 4. 7.100 / 4. 7.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/tmp/pytest-of-mark/pytest-39/test_writer_pixelformat_size_v0/test.mp4': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 encoder : Lavf59.27.100 Duration: 00:00:00.40, start: 0.000000, bitrate: 18 kb/s Stream #0:0[0x1](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(tv, progressive), 64x64, 1 kb/s, 10 fps, 10 tbr, 10240 tbn (default) Metadata: handler_name : VideoHandler vendor_id : [0][0][0][0] encoder : Lavc59.37.100 libopenh264 Stream mapping: Stream #0:0 -> #0:0 (h264 (native) -> rawvideo (native)) Press [q] to stop, [?] for help Output #0, image2pipe, to 'pipe:': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 encoder : Lavf59.27.100 Stream #0:0(und): Video: rawvideo (RGB[24] / 0x18424752), rgb24(pc, gbr/unknown/unknown, progressive), 64x64, q=2-31, 983 kb/s, 10 fps, 10 tbn (default) """ ) info = parse_ffmpeg_header(sample) assert info["pix_fmt"] == "yuv420p(tv, progressive)" if __name__ == "__main__": test_cvsecs() test_limit_lines() test_get_correct_fps1() test_get_correct_fps2() test_get_correct_rotation() test_comma_in_pixel_format() imageio-ffmpeg-0.6.0/tests/test_special.py000066400000000000000000000020741474227542300206010ustar00rootroot00000000000000""" Tests more special use-cases. """ import gc import sys import queue import threading from testutils import ensure_test_files, test_file1 import pytest import imageio_ffmpeg IS_PYPY = "__pypy__" in sys.builtin_module_names def setup_module(): ensure_test_files() def make_iterator(q, n): for i in range(n): gen = imageio_ffmpeg.read_frames(test_file1) gen.__next__() # meta data q.put(gen.__next__()) # first frame def test_threading(): # See issue #20 if IS_PYPY: pytest.xfail("These threads hang on pypy for some reason.") num_threads = 16 num_frames = 5 q = queue.Queue() threads = [] for i in range(num_threads): t = threading.Thread(target=make_iterator, args=(q, num_frames)) t.daemon = True t.start() threads.append(t) for i in range(num_threads * num_frames): print(i, end=" ") gc.collect() # this seems to help invoke the segfault earlier q.get(timeout=20) if __name__ == "__main__": setup_module() test_threading() imageio-ffmpeg-0.6.0/tests/test_terminate.py000066400000000000000000000105351474227542300211520ustar00rootroot00000000000000""" Tests specifically for ensuring that we dont have daemon ffmpeg processes. We should also run this test as a script, so we can confirm that ffmpeg quits nicely (instead of being killed). """ import gc import subprocess import sys from testutils import ( ensure_test_files, get_ffmpeg_pids, no_warnings_allowed, test_file1, test_file2, ) import imageio_ffmpeg N = 2 # number of times to perform each test def setup_module(): ensure_test_files() @no_warnings_allowed def test_ffmpeg_version(): version = imageio_ffmpeg.get_ffmpeg_version() print("ffmpeg version", version) assert version > "3.0" @no_warnings_allowed def test_reader_done(): """Test that the reader is done after reading all frames""" for _ in range(N): pids0 = get_ffmpeg_pids() r = imageio_ffmpeg.read_frames(test_file1) pids1 = get_ffmpeg_pids().difference(pids0) # generator has not started r.__next__() # == meta pids2 = get_ffmpeg_pids().difference(pids0) # now ffmpeg is running for frame in r: pass pids3 = get_ffmpeg_pids().difference(pids0) # now its not assert len(pids1) == 0 assert len(pids2) == 1 assert len(pids3) == 0 @no_warnings_allowed def test_reader_close(): """Test that the reader is done after closing it""" for _ in range(N): pids0 = get_ffmpeg_pids() r = imageio_ffmpeg.read_frames(test_file1) pids1 = get_ffmpeg_pids().difference(pids0) # generator has not started r.__next__() # == meta pids2 = get_ffmpeg_pids().difference(pids0) # now ffmpeg is running r.close() pids3 = get_ffmpeg_pids().difference(pids0) # now its not assert len(pids1) == 0 assert len(pids2) == 1 assert len(pids3) == 0 @no_warnings_allowed def test_reader_del(): """Test that the reader is done after deleting it""" for _ in range(N): pids0 = get_ffmpeg_pids() r = imageio_ffmpeg.read_frames(test_file1) pids1 = get_ffmpeg_pids().difference(pids0) # generator has not started r.__next__() # == meta pids2 = get_ffmpeg_pids().difference(pids0) # now ffmpeg is running del r gc.collect() pids3 = get_ffmpeg_pids().difference(pids0) # now its not assert len(pids1) == 0 assert len(pids2) == 1 assert len(pids3) == 0 @no_warnings_allowed def test_write_close(): """Test that the writer is done after closing it""" for _ in range(N): pids0 = get_ffmpeg_pids() w = imageio_ffmpeg.write_frames(test_file2, (64, 64)) pids1 = get_ffmpeg_pids().difference(pids0) # generator has not started w.send(None) w.send(b"x" * 64 * 64 * 3) pids2 = get_ffmpeg_pids().difference(pids0) # now ffmpeg is running w.close() pids3 = get_ffmpeg_pids().difference(pids0) # now its not assert len(pids1) == 0 assert len(pids2) == 1 assert len(pids3) == 0 @no_warnings_allowed def test_write_del(): for _ in range(N): pids0 = get_ffmpeg_pids() w = imageio_ffmpeg.write_frames(test_file2, (64, 64)) pids1 = get_ffmpeg_pids().difference(pids0) # generator has not started w.send(None) w.send(b"x" * 64 * 64 * 3) pids2 = get_ffmpeg_pids().difference(pids0) # now ffmpeg is running del w gc.collect() pids3 = get_ffmpeg_pids().difference(pids0) # now its not assert len(pids1) == 0 assert len(pids2) == 1 assert len(pids3) == 0 def test_partial_read(): # Case: https://github.com/imageio/imageio-ffmpeg/issues/69 template = ( "import sys; import imageio_ffmpeg; f=sys.argv[1]; print(f); " "r=imageio_ffmpeg.read_frames(f);" ) for i in range(4): code = template + " r.__next__();" * i cmd = [sys.executable, "-c", code, test_file1] result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, timeout=5, ) print(result.stdout) assert not result.returncode if __name__ == "__main__": setup_module() test_ffmpeg_version() test_reader_done() test_reader_close() test_reader_del() test_write_close() test_write_del() test_partial_read() imageio-ffmpeg-0.6.0/tests/testutils.py000066400000000000000000000030031474227542300201530ustar00rootroot00000000000000import logging.handlers import os import time import tempfile from urllib.request import urlopen import psutil import imageio_ffmpeg test_dir = tempfile.gettempdir() test_url1 = "https://raw.githubusercontent.com/imageio/imageio-binaries/master/images/cockatoo.mp4" test_url2 = "https://raw.githubusercontent.com/imageio/imageio-binaries/master/images/realshort.mp4" test_file1 = os.path.join(test_dir, "cockatoo.mp4") test_file2 = os.path.join(test_dir, "test.mp4") test_file3 = os.path.join(test_dir, "realshort.mp4") have_downloaded = False def ensure_test_files(): global have_downloaded if not have_downloaded: bb = urlopen(test_url1, timeout=5).read() with open(test_file1, "wb") as f: f.write(bb) bb = urlopen(test_url2, timeout=5).read() with open(test_file3, "wb") as f: f.write(bb) have_downloaded = True class OurMemoryHandler(logging.handlers.MemoryHandler): pass def no_warnings_allowed(f): logger = imageio_ffmpeg._utils.logger def wrapper(): handler = OurMemoryHandler(99, logging.WARNING) logger.addHandler(handler) f() logger.removeHandler(handler) assert not handler.buffer wrapper.__name__ = f.__name__ return wrapper def get_ffmpeg_pids(): time.sleep(0.01) pids = set() for p in psutil.process_iter(): try: if "ffmpeg" in p.name().lower(): pids.add(p.pid) except psutil.NoSuchProcess: pass return pids